#!/usr/bin/env python3 """Integration test for ORB paper trading system. Runs all 5 daily phases against MockORBBroker (no Alpaca API calls). Tests: 1. Multiple sessions with different budgets 2. Session creation + budget persistence in SQLite 3. ORB detection + candidate ranking (all 971 tickers at once) 4. Breakout check + position entry (Oracle snapshot mock) 5. Stop checks (sim_bar_minutes interval) 6. EOD exit + P&L recording 7. Post-close equity snapshot (compound returns) Usage: python scripts/test_orb_paper.py """ from __future__ import annotations import os import sys import tempfile import traceback from pathlib import Path from unittest.mock import patch # ── Repo root on path ───────────────────────────────────────────────────────── ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT)) os.chdir(ROOT) import datetime as dt from zoneinfo import ZoneInfo _ET = ZoneInfo("America/New_York") # ── Minimal ORB config YAML ─────────────────────────────────────────────────── _TEST_CONFIG_YAML = """\ strategy_mode: orb orb_strategy: orb_minutes: 10 sim_bar_minutes: 90 entry_direction: long_only order_timeout_minutes: 45 min_price: 5.0 min_avg_dollar_volume: 1000000 min_atr_14: 0.10 min_rvol: 0.1 max_candidates: 10 min_candidates_to_trade: 1 weight_rvol: 0.60 weight_gap: 0.25 weight_dollar_vol: 0.15 atr_stop_multiplier: 0.05 breakeven_at_r: 1.0 trailing_at_r: 10.0 trailing_stop_atr_multiplier: 0.3 risk_per_trade_pct: 0.01 max_position_pct: 0.25 daily_max_loss_pct: 0.05 max_stops_per_day: 5 exit_minutes_before_close: 5 universe: source: midlarge """ # Thresholds are intentionally loose so mock data always passes screening. # ── Helpers ─────────────────────────────────────────────────────────────────── SEP = "─" * 60 def section(title: str) -> None: print(f"\n{SEP}") print(f" {title}") print(SEP) def ok(msg: str) -> None: print(f" ✓ {msg}") def fail(msg: str) -> None: print(f" ✗ {msg}") sys.exit(1) def assert_eq(label: str, got: Any, expected: Any) -> None: if got == expected: ok(f"{label}: {got!r}") else: fail(f"{label}: expected {expected!r}, got {got!r}") from typing import Any # ── Setup ───────────────────────────────────────────────────────────────────── def setup_test_env(tmp_dir: str) -> tuple[str, str]: """Write test config and return (config_path, db_path).""" config_path = os.path.join(tmp_dir, "test_orb.yaml") db_path = os.path.join(tmp_dir, "orb_test.db") with open(config_path, "w") as f: f.write(_TEST_CONFIG_YAML) return config_path, db_path # ── Phase runner ────────────────────────────────────────────────────────────── def run_phases( engine: Any, broker: Any, date_str: str, session_name: str, ) -> dict[str, Any]: """Run all 5 engine phases and return phase results dict.""" from apps.orb_trader.mock_broker import make_mock_snapshots tickers = broker._tickers results: dict[str, Any] = {} print(f" [{session_name}] Phase 1: orb_detection") with patch("apps.orb_trader.engine.load_universe", return_value=tickers): r = engine.run_orb_detection(date_str) results["orb_detection"] = r print(f" → {r}") print(f" [{session_name}] Phase 2: breakout_check") mock_snaps = make_mock_snapshots(tickers, date_str, price_mult=1.05) with patch("libs.oracle_client.alpaca.get_snapshots", return_value=mock_snaps): r = engine.run_breakout_check(date_str) results["breakout_check"] = r print(f" → {r}") print(f" [{session_name}] Phase 3: stop_check") r = engine.run_stop_check(date_str) results["stop_check"] = r print(f" → {r}") print(f" [{session_name}] Phase 4: eod_exit") r = engine.run_eod_exit(date_str) results["eod_exit"] = r print(f" → {r}") print(f" [{session_name}] Phase 5: post_close") r = engine.run_post_close(date_str) results["post_close"] = r print(f" → {r}") return results # ── Main test ───────────────────────────────────────────────────────────────── def main() -> None: print("\nORB Paper Trading — Integration Test") print("=" * 60) with tempfile.TemporaryDirectory() as tmp_dir: config_path, db_path = setup_test_env(tmp_dir) # ── 1. Session creation + budget tracking ───────────────────────────── section("1. Session creation + budget persistence") from apps.orb_trader.state import ORBStateManager state = ORBStateManager(db_path) SESSIONS = [ ("alpha_session", 10_000.0), ("beta_session", 25_000.0), ] for name, capital in SESSIONS: sid = state.create_session(name, config_path, capital) ok(f"Created '{name}' (id={sid}, budget=${capital:,.0f})") sessions = state.list_sessions() assert_eq("session count", len(sessions), 2) for s in sessions: expected_budget = dict(SESSIONS)[s.session_name] assert_eq( f" {s.session_name}.initial_equity", s.initial_equity, expected_budget, ) # ── 2. Duplicate session name rejected ──────────────────────────────── section("2. Duplicate name check") try: state.create_session("alpha_session", config_path, 999.0) fail("Should have raised IntegrityError for duplicate name") except Exception as exc: ok(f"Duplicate rejected: {type(exc).__name__}") # ── 3. Run all phases per session ───────────────────────────────────── section("3. Full trading day simulation (2 sessions)") # Use the most recent past trading day today = dt.date.today() # Find a weekday (Mon-Fri) that's in the past test_date = today - dt.timedelta(days=1) while test_date.weekday() >= 5: test_date -= dt.timedelta(days=1) date_str = test_date.isoformat() print(f"\n Simulating trading day: {date_str}") from apps.orb_trader.mock_broker import MockORBBroker from apps.orb_trader.engine import make_orb_engine all_results: dict[str, dict] = {} for s in sessions: print(f"\n {'─' * 40}") print(f" Session: {s.session_name} | Budget: ${s.initial_equity:,.0f}") print(f" {'─' * 40}") broker = MockORBBroker( initial_equity=s.initial_equity, tickers=["AAPL", "NVDA", "MSFT"], ) engine = make_orb_engine(s, db_path, broker_override=broker) results = run_phases(engine, broker, date_str, s.session_name) all_results[s.session_name] = results # ── 4. Verify DB state after simulation ─────────────────────────────── section("4. Verify DB state") for s in sessions: trades = state.list_trades(s.session_id) snapshots = state.list_snapshots(s.session_id) equity = state.get_equity(s.session_id) print(f"\n [{s.session_name}]") ok(f"Trades recorded: {len(trades)}") ok(f"Equity snapshots: {len(snapshots)}") if snapshots: final_eq = snapshots[-1]["equity"] ok(f"Final equity: ${final_eq:,.2f} (initial: ${s.initial_equity:,.0f})") else: ok("No equity snapshot (no positions entered today — OK if no candidates)") assert_eq("equity from get_equity()", equity, snapshots[-1]["equity"] if snapshots else None) # ── 5. Verify budget independence between sessions ───────────────────── section("5. Budget independence check") eq_alpha = state.get_equity("alpha_session") or dict(SESSIONS)["alpha_session"] eq_beta = state.get_equity("beta_session") or dict(SESSIONS)["beta_session"] if eq_alpha != eq_beta: ok(f"Sessions have independent equity: alpha=${eq_alpha:,.2f}, beta=${eq_beta:,.2f}") else: ok(f"Sessions both at ${eq_alpha:,.2f} (no trades today — budgets remain independent)") # ── 6. Pause / resume / delete ──────────────────────────────────────── section("6. Session lifecycle (pause / resume / delete)") alpha = state.get_session("alpha_session") state.set_session_status(alpha.session_id, "paused") refreshed = state.get_session("alpha_session") assert_eq("status after pause", refreshed.status, "paused") state.set_session_status(alpha.session_id, "active") refreshed = state.get_session("alpha_session") assert_eq("status after resume", refreshed.status, "active") state.delete_session(alpha.session_id) deleted = state.get_session("alpha_session") assert_eq("session after delete", deleted, None) ok("alpha_session deleted") remaining = state.list_sessions() assert_eq("remaining sessions", len(remaining), 1) assert_eq("remaining session name", remaining[0].session_name, "beta_session") print(f"\n{'=' * 60}") print(" ALL TESTS PASSED") print(f"{'=' * 60}\n") if __name__ == "__main__": try: main() except SystemExit: raise except Exception: traceback.print_exc() sys.exit(1)