You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
6.4 KiB
Python

from __future__ import annotations
from pathlib import Path
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
"""
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_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_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