from __future__ import annotations import json from pathlib import Path import pytest from apps.web.routers import intraday MOMENTUM_CONFIG = """\ _meta: name: Leader Intraday Momentum description: Test momentum config id: 7 strategy_mode: momentum strategy: entry_minutes_after_open: 10 exit_minutes_before_close: 5 stop_loss_pct: null trailing_stop_pct: -0.06 min_morning_gain_pct: 0.02 max_morning_gain_pct: null min_entry_volume: 250000 ticker_cooldown_days: 0 top_n: 5 initial_capital: 10000.0 slippage_bps: 5.0 market_regime_spy_threshold: null universe: source: midcap min_price: 10.0 backtest: lookback_trading_days: 200 pre_screen_threshold: 0.02 cache: enabled: true dir: data/cache/intraday output: dir: runs/intraday verbose: false """ MOMENTUM_YAML_UNIVERSE_CONFIG = """\ _meta: name: Leader Intraday Momentum description: Test momentum config id: 7 strategy_mode: momentum strategy: entry_minutes_after_open: 10 exit_minutes_before_close: 5 stop_loss_pct: null trailing_stop_pct: -0.06 min_morning_gain_pct: 0.02 max_morning_gain_pct: null min_entry_volume: 250000 ticker_cooldown_days: 0 top_n: 5 initial_capital: 10000.0 slippage_bps: 5.0 market_regime_spy_threshold: null universe: source: yaml symbols_file: configs/symbols_midcap_smallmid.yaml min_price: 10.0 backtest: lookback_trading_days: 200 pre_screen_threshold: 0.02 cache: enabled: true dir: data/cache/intraday output: dir: runs/intraday verbose: false """ ORB_ADVANCED_CONFIG = """\ _meta: name: ORB Advanced description: Advanced ORB config id: 42 strategy_mode: orb orb_strategy: orb_minutes: 5 sim_bar_minutes: 5 entry_direction: long_only order_timeout_minutes: 45 min_price: 15.0 min_avg_dollar_volume: 40000000 min_premarket_dollar_vol: 2500000 min_breakout_rel_vol: 1.2 min_atr_14: 0.50 min_rvol: 1.5 max_candidates: 20 risk_per_trade_pct: 0.05 max_position_pct: 0.70 atr_stop_multiplier: 0.75 breakeven_at_r: 1.0 trailing_at_r: 1.0 trailing_stop_atr_multiplier: 0.8 daily_max_loss_pct: 0.05 max_stops_per_day: 5 settlement_days: 1 max_gap_pct: 0.04 compound_returns: false daily_budget_reset: true daily_bar_snapshot_id: orb_daily_v46_20260423 prior_event_snapshot_id: orb_pead_d10_v46_20260423 universe: source: broad backtest: lookback_trading_days: 200 cache: enabled: true dir: data/cache/intraday output: dir: runs/intraday_orb verbose: false """ ORB_EXTENDS_CHILD_CONFIG = """\ extends: orb_advanced_base.yaml _meta: name: ORB Extends Child description: Child config id: 43 orb_strategy: min_rvol: 3.0 """ def _write_config(tmp_path: Path, slug: str, text: str = MOMENTUM_CONFIG) -> Path: strategies_dir = tmp_path / "configs" / "intraday" / "strategies" strategies_dir.mkdir(parents=True, exist_ok=True) path = strategies_dir / f"{slug}.yaml" path.write_text(text) return path def test_load_user_strategy_supports_momentum(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "leader_intraday_momentum") monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) payload = intraday._load_user_strategy("leader_intraday_momentum") assert payload is not None assert payload["strategy_mode"] == "momentum" assert payload["output_dir"] == "runs/intraday" assert payload["top_n"] == 5 assert payload["min_morning_gain_pct"] == 0.02 def test_load_user_strategy_exposes_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) payload = intraday._load_user_strategy("orb_advanced") assert payload is not None assert payload["strategy_mode"] == "orb" assert payload["universe"] == "broad" assert payload["min_price"] == 15.0 assert payload["min_avg_dollar_volume"] == 40000000 assert payload["min_premarket_dollar_vol"] == 2500000 assert payload["min_breakout_rel_vol"] == 1.2 assert payload["daily_budget_reset"] is True def test_create_comp_strategy_materializes_deployment_overlay(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) generated = intraday.create_comp_strategy("orb_advanced") generated_path = tmp_path / "configs" / "intraday" / "strategies" / f"{generated['slug']}.yaml" raw = intraday.yaml.safe_load(generated_path.read_text()) assert generated["slug"] == "orb_advanced_comp" assert generated["name"] == "ORB Advanced Comp" assert generated["compound_returns"] is True assert generated["daily_budget_reset"] is False assert generated["single_trade_loss_cap_basis"] == "equity" assert generated["is_comp_equity_deployment"] is True assert raw["orb_strategy"]["compound_returns"] is True assert raw["orb_strategy"]["daily_budget_reset"] is False assert raw["orb_strategy"]["single_trade_loss_cap_basis"] == "equity" assert raw["_meta"]["parent"] is None assert raw["_meta"]["deployment_kind"] == "compound_equity_losscap" assert raw["_meta"]["source_slug"] == "orb_advanced" def test_load_strategy_yaml_rejects_extends(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG) _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) with pytest.raises(ValueError, match="extends.*no longer supported"): intraday._load_strategy_yaml_standalone( tmp_path / "configs" / "intraday" / "strategies" / "orb_extends_child.yaml" ) def test_create_comp_strategy_refreshes_existing_generated_variant(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) first = intraday.create_comp_strategy("orb_advanced") first_path = tmp_path / "configs" / "intraday" / "strategies" / f"{first['slug']}.yaml" raw = intraday.yaml.safe_load(first_path.read_text()) raw["orb_strategy"]["min_rvol"] = 9.9 first_path.write_text(intraday.yaml.dump(raw, default_flow_style=False, sort_keys=False)) second = intraday.create_comp_strategy("orb_advanced") refreshed = intraday.yaml.safe_load(first_path.read_text()) assert second["slug"] == first["slug"] assert second["id"] == first["id"] assert refreshed["orb_strategy"]["min_rvol"] == 1.5 assert refreshed["orb_strategy"]["single_trade_loss_cap_basis"] == "equity" def test_load_user_strategy_does_not_load_extends(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG) _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) payload = intraday._load_user_strategy("orb_extends_child") assert payload is None def test_update_strategy_preserves_unedited_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None: path = _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) updated = intraday.update_strategy( "orb_advanced", intraday.UpdateStrategyRequest(name="ORB Advanced Updated"), ) raw = intraday.yaml.safe_load(path.read_text()) assert updated["name"] == "ORB Advanced Updated" assert raw["orb_strategy"]["min_price"] == 15.0 assert raw["orb_strategy"]["min_avg_dollar_volume"] == 40000000 assert raw["orb_strategy"]["min_premarket_dollar_vol"] == 2500000 assert raw["orb_strategy"]["daily_bar_snapshot_id"] == "orb_daily_v46_20260423" assert raw["orb_strategy"]["prior_event_snapshot_id"] == "orb_pead_d10_v46_20260423" def test_copy_strategy_preserves_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) copied = intraday.copy_strategy("orb_advanced") copied_path = tmp_path / "configs" / "intraday" / "strategies" / f"{copied['slug']}.yaml" raw = intraday.yaml.safe_load(copied_path.read_text()) assert copied["name"] == "ORB Advanced (copy)" assert raw["orb_strategy"]["min_price"] == 15.0 assert raw["orb_strategy"]["min_avg_dollar_volume"] == 40000000 assert raw["orb_strategy"]["min_premarket_dollar_vol"] == 2500000 assert raw["orb_strategy"]["daily_bar_snapshot_id"] == "orb_daily_v46_20260423" def test_copy_strategy_rejects_extends(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG) _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) with pytest.raises(intraday.HTTPException) as exc_info: intraday.copy_strategy("orb_extends_child") assert exc_info.value.status_code == 400 def test_submit_intraday_backtest_uses_config_strategy_mode(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "leader_intraday_momentum") monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs") intraday._tasks.clear() intraday._tasks_initialized = True captured: dict[str, object] = {} class DummyProc: pid = 4242 def fake_popen(cmd, stdout=None, stderr=None, cwd=None): # noqa: ANN001 captured["cmd"] = cmd captured["cwd"] = cwd return DummyProc() class DummyThread: def __init__(self, target=None, args=None, daemon=None): # noqa: ANN001 captured["thread_target"] = target captured["thread_args"] = args def start(self) -> None: captured["thread_started"] = True monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen) monkeypatch.setattr(intraday.threading, "Thread", DummyThread) req = intraday.IntradayBacktestRequest( config="leader_intraday_momentum", universe="midcap", start_date="2026-01-02", end_date="2026-03-31", compound_returns=False, ) resp = intraday.submit_intraday_backtest(req) task = intraday._tasks[resp["task_id"]] cmd = captured["cmd"] assert task["strategy_mode"] == "momentum" assert task["output_dir"] == "runs/intraday" assert "--strategy" in cmd assert cmd[cmd.index("--strategy") + 1] == "momentum" assert "--output-dir" in cmd assert cmd[cmd.index("--output-dir") + 1] == str(tmp_path / "runs" / "intraday") def test_submit_intraday_backtest_preserves_yaml_strategy_universe(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "leader_intraday_momentum_yaml", MOMENTUM_YAML_UNIVERSE_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs") intraday._tasks.clear() intraday._tasks_initialized = True captured: dict[str, object] = {} class DummyProc: pid = 4242 def fake_popen(cmd, stdout=None, stderr=None, cwd=None): # noqa: ANN001 captured["cmd"] = cmd captured["cwd"] = cwd return DummyProc() class DummyThread: def __init__(self, target=None, args=None, daemon=None): # noqa: ANN001 captured["thread_target"] = target captured["thread_args"] = args def start(self) -> None: captured["thread_started"] = True monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen) monkeypatch.setattr(intraday.threading, "Thread", DummyThread) req = intraday.IntradayBacktestRequest( config="leader_intraday_momentum_yaml", universe="yaml", start_date="2026-01-02", end_date="2026-03-31", compound_returns=False, ) resp = intraday.submit_intraday_backtest(req) task = intraday._tasks[resp["task_id"]] cmd = captured["cmd"] assert task["universe"] == "yaml" assert task["universe_label"] == "yaml:symbols_midcap_smallmid.yaml" assert "--strategy" in cmd assert cmd[cmd.index("--strategy") + 1] == "momentum" assert "--universe" not in cmd def test_submit_intraday_backtest_accepts_broad_universe(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "leader_intraday_momentum") monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs") intraday._tasks.clear() intraday._tasks_initialized = True captured: dict[str, object] = {} class DummyProc: pid = 4242 def fake_popen(cmd, stdout=None, stderr=None, cwd=None): # noqa: ANN001 captured["cmd"] = cmd captured["cwd"] = cwd return DummyProc() class DummyThread: def __init__(self, target=None, args=None, daemon=None): # noqa: ANN001 captured["thread_target"] = target captured["thread_args"] = args def start(self) -> None: captured["thread_started"] = True monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen) monkeypatch.setattr(intraday.threading, "Thread", DummyThread) req = intraday.IntradayBacktestRequest( config="leader_intraday_momentum", universe="broad", start_date="2026-01-02", end_date="2026-03-31", compound_returns=False, ) resp = intraday.submit_intraday_backtest(req) task = intraday._tasks[resp["task_id"]] cmd = captured["cmd"] assert task["universe"] == "broad" assert task["universe_label"] == "broad" assert "--universe" in cmd assert cmd[cmd.index("--universe") + 1] == "broad" def test_submit_intraday_backtest_records_command_and_compound_flags(monkeypatch, tmp_path: Path) -> None: _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG) monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs") intraday._tasks.clear() intraday._tasks_initialized = True captured: dict[str, object] = {} class DummyProc: pid = 4242 def fake_popen(cmd, stdout=None, stderr=None, cwd=None): # noqa: ANN001 captured["cmd"] = cmd captured["cwd"] = cwd return DummyProc() class DummyThread: def __init__(self, target=None, args=None, daemon=None): # noqa: ANN001 captured["thread_target"] = target captured["thread_args"] = args def start(self) -> None: captured["thread_started"] = True monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen) monkeypatch.setattr(intraday.threading, "Thread", DummyThread) req = intraday.IntradayBacktestRequest( config="orb_advanced", universe="midlarge", days=200, compound_returns=True, daily_budget_reset=False, ) resp = intraday.submit_intraday_backtest(req) task = intraday._tasks[resp["task_id"]] cmd = captured["cmd"] log_text = (tmp_path / "runs" / ".intraday_tasks" / f"{resp['task_id']}.log").read_text() assert task["command"] == cmd assert "--compound-returns" in cmd assert "--no-daily-budget-reset" in cmd assert "Command:" in log_text assert "--compound-returns" in log_text assert "--no-daily-budget-reset" in log_text def test_result_summary_from_file_includes_loss_containment_fields(tmp_path: Path) -> None: result_path = tmp_path / "intraday_result.json" result_path.write_text( """{ "metrics": { "total_return_pct": 0.0688, "max_drawdown_pct": -0.1325, "sharpe_ratio": 1.23, "win_rate": 0.569, "total_trades": 116, "final_equity": 10688.0, "calmar_ratio": 0.52, "loss_containment_score": 43.78, "avg_loss_day_pct": -0.0168, "tail_loss_20_pct": -0.0377, "worst_day_return_pct": -0.0672 } }""" ) summary = intraday._result_summary_from_file(result_path) assert summary is not None assert summary["return_pct"] == 0.0688 assert summary["loss_containment_score"] == 43.78 assert summary["avg_loss_day_pct"] == -0.0168 assert summary["tail_loss_20_pct"] == -0.0377 assert summary["worst_day_return_pct"] == -0.0672 def test_detect_result_file_from_log_prefers_explicit_saved_path(tmp_path: Path) -> None: good = tmp_path / "runs" / "intraday_orb" / "intraday_good.json" good.parent.mkdir(parents=True, exist_ok=True) good.write_text("{}") bad = tmp_path / "runs" / "intraday_orb" / "intraday_bad.json" bad.write_text("{}") log_path = tmp_path / "runs" / ".intraday_tasks" / "task.log" log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text( "\n".join([ f"Results saved to: {bad}", f"Results saved to: {good}", ]) ) assert intraday._detect_result_file_from_log(log_path) == good def test_try_resolve_dead_task_repairs_completed_result_from_log(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path) monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs") intraday._tasks.clear() intraday._tasks_initialized = True results_dir = tmp_path / "runs" / "intraday_orb" results_dir.mkdir(parents=True, exist_ok=True) wrong_result = results_dir / "intraday_wrong.json" wrong_result.write_text( json.dumps({"metrics": {"total_return_pct": -0.1177, "max_drawdown_pct": -0.1765}}) ) right_result = results_dir / "intraday_right.json" right_result.write_text( json.dumps({"metrics": {"total_return_pct": 1.302, "max_drawdown_pct": -0.11}}) ) task_id = "task-123" log_path = tmp_path / "runs" / ".intraday_tasks" / f"{task_id}.log" log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text(f"Results saved to: {right_result}\n") intraday._tasks[task_id] = { "task_id": task_id, "status": "completed", "pid": None, "started_at": "2026-04-22T23:28:35.765827+00:00", "finished_at": "2026-04-23T00:33:45.996122+00:00", "returncode": None, "error": None, "result_file": str(wrong_result), "result_summary": {"return_pct": -0.1177, "max_dd_pct": -0.1765}, "output_dir": "runs/intraday_orb", } intraday._persist_task(intraday._tasks[task_id]) intraday._try_resolve_dead_task(task_id) task = intraday._tasks[task_id] persisted = json.loads((tmp_path / "runs" / ".intraday_tasks" / f"{task_id}.task.json").read_text()) assert task["result_file"] == str(right_result) assert task["result_summary"]["return_pct"] == 1.302 assert task["result_summary"]["max_dd_pct"] == -0.11 assert task["returncode"] == 0 assert persisted["result_file"] == str(right_result) assert persisted["result_summary"]["return_pct"] == 1.302