"""2-week paper trading simulation using real pipeline DB + MockBroker. Simulates what would have happened during March 8-21, 2026 for sessions v236 and v4.41. Uses real EventDetector (connects to PostgreSQL pipeline DB) but replaces Alpaca with a MockBroker that fills orders using Oracle historical price data. Usage: python -m apps.tools.simulate_2week """ from __future__ import annotations import asyncio import datetime as dt import os import sys import tempfile from pathlib import Path from typing import Any # ── Load .env ───────────────────────────────────────────────────────────────── _ENV_FILE = Path(__file__).parent.parent.parent / ".env" if _ENV_FILE.exists(): for _line in _ENV_FILE.read_text().splitlines(): _line = _line.strip() if _line and not _line.startswith("#") and "=" in _line: _k, _, _v = _line.partition("=") os.environ.setdefault(_k.strip(), _v.strip()) # ── Verify Oracle is reachable before heavy imports ─────────────────────────── _ORACLE_URL = os.environ.get("STOCK_ORACLE_URL", "http://localhost:18001") _POSTGRES_DSN = os.environ.get("POSTGRES_DSN", "") from rich.console import Console from rich.table import Table from rich import box as rbox console = Console(width=120) # ───────────────────────────────────────────────────────────────────────────── # MockBroker + helpers (imported from mock_broker module) # ───────────────────────────────────────────────────────────────────────────── from apps.paper_trader.mock_broker import MockBroker, get_event_symbols_from_db, prefetch_bars as _prefetch_bars async def prefetch_bars( symbols: list[str], start: dt.date, end: dt.date, oracle_url: str, ) -> dict[str, dict[dt.date, dict]]: return await _prefetch_bars(symbols, start, end, oracle_url, console=console) # ───────────────────────────────────────────────────────────────────────────── # Simulation runner # ───────────────────────────────────────────────────────────────────────────── async def run_simulation( session_name: str, config_path: str, initial_equity: float, trading_days: list[dt.date], bars_cache: dict[str, dict[dt.date, dict]], db_dsn: str, oracle_url: str, ) -> dict[str, Any]: """Run phased simulation for one session. Returns summary.""" import tempfile from apps.paper_trader.state import StateManager, SessionRow from apps.paper_trader.event_detector import EventDetector from apps.paper_trader.engine import PaperTradingEngine # Temp SQLite DB (won't touch production DB) tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) tmp.close() tmp_db = tmp.name state = StateManager(tmp_db) session_id = state.create_session( session_name=session_name, config_path=config_path, initial_equity=initial_equity, ) session = state.get_session(session_id) broker = MockBroker(initial_equity=initial_equity, bars_cache=bars_cache) detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url) engine = PaperTradingEngine( session=session, broker=broker, state=state, event_detector=detector, ) all_entries: list[dict] = [] all_exits: list[dict] = [] daily_summaries: list[dict] = [] console.print(f"\n[bold cyan]Session: {session_name}[/] config: {config_path}") console.print(f" Equity: ${initial_equity:,.0f} | Days: {trading_days[0]} → {trading_days[-1]}\n") for day in trading_days: # ── run-open: exits + after-close entries ────────────────────────── broker.set_sim_context(day, "open") open_result = await engine.run_next_open(target_date=day, force=True) # ── run-close: same-day entries ──────────────────────────────────── broker.set_sim_context(day, "close") close_result = await engine.run_reaction_close(target_date=day, force=True) acct = broker.get_account() exits_today = open_result.get("exits", []) entries_open = open_result.get("entries", []) entries_close = close_result.get("entries", []) candidates_open = open_result.get("candidates_detected", 0) candidates_close = close_result.get("candidates_detected", 0) rejected_open = open_result.get("rejected", []) rejected_close = close_result.get("rejected", []) for e in exits_today: e["exit_date"] = day.isoformat() for e in entries_open: e["entry_date"] = day.isoformat() e["convention"] = "next_open" for e in entries_close: e["entry_date"] = day.isoformat() e["convention"] = "reaction_close" all_exits.extend(exits_today) all_entries.extend(entries_open + entries_close) summary = { "date": day, "equity": acct.equity, "cash": acct.cash, "market_value": acct.long_market_value, "positions": len(broker.list_positions()), "exits": len(exits_today), "entries": len(entries_open) + len(entries_close), "candidates": candidates_open + candidates_close, "rejected": len(rejected_open) + len(rejected_close), } daily_summaries.append(summary) pnl_today = sum(e.get("pnl", 0) for e in exits_today) day_str = f"[bold]{day.strftime('%a %m/%d')}[/]" eq_str = f"${acct.equity:,.2f}" entries_str = f"{len(entries_open)+len(entries_close)} entries" exits_str = f"{len(exits_today)} exits" cand_str = f"{candidates_open + candidates_close} candidates" pos_str = f"{len(broker.list_positions())} pos" details = [] for e in entries_open + entries_close: details.append(f" [green] ↳ ENTER {e['symbol']} ({e.get('event_type','?')}, score={e.get('score', 0):.2f}) " f"× {e['shares']} shares [{e['convention']}][/]") for e in exits_today: pnl_col = "green" if e.get("pnl", 0) >= 0 else "red" details.append(f" [{pnl_col}] ↳ EXIT {e['symbol']} ({e['reason']}) " f"P&L=${e.get('pnl', 0):+,.2f}[/{pnl_col}]") for r in (rejected_open + rejected_close)[:3]: # show first 3 rejections details.append(f" [dim] ↳ skip {r['symbol']} → {r['reason']}[/]") console.print(f" {day_str} {eq_str} {entries_str} {exits_str} {cand_str} {pos_str}") for d in details: console.print(d) # ── Final summary ────────────────────────────────────────────────────── acct_final = broker.get_account() total_pnl = acct_final.equity - initial_equity pct = total_pnl / initial_equity * 100 console.print(f"\n [bold]Final:[/] equity=${acct_final.equity:,.2f} " f"P&L=${total_pnl:+,.2f} ({pct:+.2f}%) " f"trades={len(all_entries)} entries / {len(all_exits)} exits") # Print trade table if all_entries or all_exits: tbl = Table(box=rbox.SIMPLE, show_header=True, header_style="bold yellow", padding=(0, 1)) tbl.add_column("Date", style="dim") tbl.add_column("Action", style="bold") tbl.add_column("Symbol") tbl.add_column("Event Type") tbl.add_column("Shares", justify="right") tbl.add_column("Score", justify="right") tbl.add_column("Convention / Reason") for e in sorted(all_entries, key=lambda x: x["entry_date"]): tbl.add_row( e["entry_date"], "[green]ENTER[/]", e["symbol"], e.get("event_type", "?"), str(e.get("shares", "?")), f"{e.get('score', 0):.2f}", e.get("convention", "?"), ) for e in sorted(all_exits, key=lambda x: x.get("exit_date", "")): pnl = e.get("pnl", 0) pnl_col = "green" if pnl >= 0 else "red" tbl.add_row( e.get("exit_date", "?"), f"[{pnl_col}]EXIT[/{pnl_col}]", e["symbol"], "", str(abs(int(e.get("shares", 0)))), f"[{pnl_col}]{pnl:+,.2f}[/{pnl_col}]", e.get("reason", "?"), ) console.print(tbl) # Clean up temp DB try: Path(tmp_db).unlink() except Exception: pass return { "session": session_name, "total_pnl": total_pnl, "total_pnl_pct": pct, "entries": len(all_entries), "exits": len(all_exits), "final_equity": acct_final.equity, "daily": daily_summaries, } # ───────────────────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────────────────── SESSIONS = [ { "name": "v236", "config": "configs/experiments/return_max_long_v1.236.json", "equity": 10000.0, }, { "name": "v4.41", "config": "configs/experiments/return_max_long_v4.41.json", "equity": 10000.0, }, ] SIM_START = dt.date(2026, 2, 22) SIM_END = dt.date(2026, 3, 21) async def main() -> None: from libs.common.time_utils import is_trading_day console.print("[bold cyan]═══ 1-Month Paper Trading Simulation (Feb 22 – Mar 21, 2026) ═══[/]") console.print(f"Oracle URL : {_ORACLE_URL}") console.print(f"Postgres : {_POSTGRES_DSN[:50]}..." if len(_POSTGRES_DSN) > 50 else f"Postgres : {_POSTGRES_DSN}") # ── Trading days ─────────────────────────────────────────────────────── all_days = [ SIM_START + dt.timedelta(days=i) for i in range((SIM_END - SIM_START).days + 1) ] trading_days = [d for d in all_days if is_trading_day(d)] console.print(f"Trading days: {[d.isoformat() for d in trading_days]}\n") # ── Get symbols from pipeline DB ─────────────────────────────────────── console.print("[bold]Fetching event symbols from pipeline DB...[/]") if not _POSTGRES_DSN: console.print("[red]ERROR: POSTGRES_DSN not set! Cannot query pipeline DB.[/]") sys.exit(1) event_symbols = await get_event_symbols_from_db( _POSTGRES_DSN, start_date=SIM_START - dt.timedelta(days=7), # buffer for label lag end_date=SIM_END, console=console, ) console.print(f" Found {len(event_symbols)} unique symbols in events") # ── Pre-fetch bars ───────────────────────────────────────────────────── all_symbols = sorted(set(event_symbols + ["SPY", "QQQ"])) bar_start = SIM_START - dt.timedelta(days=90) # enough history for exits + macro SMA console.print(f"\n[bold]Pre-fetching bars for {len(all_symbols)} symbols ({bar_start} → {SIM_END})...[/]") bars_cache = await prefetch_bars(all_symbols, bar_start, SIM_END, _ORACLE_URL) fetched = sum(1 for v in bars_cache.values() if v) console.print(f" Fetched bars for {fetched}/{len(all_symbols)} symbols") if fetched == 0: console.print("[red]ERROR: No bars fetched. Is Oracle running at {_ORACLE_URL}?[/]") sys.exit(1) # ── Suppress verbose logs during simulation ──────────────────────────── import logging logging.getLogger("apps.paper_trader").setLevel(logging.WARNING) logging.getLogger("libs.backtest").setLevel(logging.WARNING) # ── Run simulations ──────────────────────────────────────────────────── console.print("\n[bold]Running simulations...[/]\n" + "═" * 80) results = [] for sess in SESSIONS: result = await run_simulation( session_name=sess["name"], config_path=sess["config"], initial_equity=sess["equity"], trading_days=trading_days, bars_cache=bars_cache, db_dsn=_POSTGRES_DSN, oracle_url=_ORACLE_URL, ) results.append(result) console.print("─" * 80) # ── Final comparison table ───────────────────────────────────────────── console.print("\n[bold cyan]═══ Final Results ═══[/]") tbl = Table(box=rbox.SIMPLE, show_header=True, header_style="bold yellow", padding=(0, 2)) tbl.add_column("Session") tbl.add_column("Final Equity", justify="right") tbl.add_column("P&L", justify="right") tbl.add_column("P&L %", justify="right") tbl.add_column("Entries", justify="right") tbl.add_column("Exits", justify="right") for r in results: pnl_col = "green" if r["total_pnl"] >= 0 else "red" tbl.add_row( r["session"], f"${r['final_equity']:,.2f}", f"[{pnl_col}]${r['total_pnl']:+,.2f}[/{pnl_col}]", f"[{pnl_col}]{r['total_pnl_pct']:+.2f}%[/{pnl_col}]", str(r["entries"]), str(r["exits"]), ) console.print(tbl) if __name__ == "__main__": asyncio.run(main())