"""Quarter-robustness research CLI for leader_intraday_momentum strategies.""" from __future__ import annotations import argparse import asyncio import json from datetime import datetime from pathlib import Path from typing import Any from libs.common.config import get_settings from apps.intraday_bt.momentum_research import ( build_momentum_research_context, build_momentum_strategy, evaluate_momentum_quarterly_candidate, ) from apps.intraday_bt.momentum_wfv import DEFAULT_CONFIG from apps.intraday_bt.oracle import make_intraday_oracle_client from apps.intraday_bt.run import load_config def _quarterly_candidate_overrides() -> list[tuple[str, dict[str, Any]]]: """Curated nearby candidates around the current leader champion.""" return [ ("control", {}), ("top18", {"top_n": 18}), ("top22", {"top_n": 22}), ("gain15", {"min_morning_gain_pct": 0.015}), ("gain17", {"min_morning_gain_pct": 0.017}), ("trail70", {"trailing_stop_pct": -0.07}), ("trail80", {"trailing_stop_pct": -0.08}), ("exit0", {"exit_minutes_before_close": 0}), ("exit10", {"exit_minutes_before_close": 10}), ("vol75k", {"min_entry_volume": 75000}), ("vol125k", {"min_entry_volume": 125000}), ("vix28", {"max_vix": 28.0}), ("vix32", {"max_vix": 32.0}), ("top22_gain17", {"top_n": 22, "min_morning_gain_pct": 0.017}), ("top22_vol125k", {"top_n": 22, "min_entry_volume": 125000}), ] def _rank_results(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: rows.sort( key=lambda row: ( row["score"]["quarterly_selection_score"], row["score"]["holdout_return_pct"] or float("-inf"), row["score"]["quarter_worst_return_pct"] or float("-inf"), ), reverse=True, ) for idx, row in enumerate(rows, start=1): row["rank"] = idx return rows def _condensed_row(label: str, payload: dict[str, Any]) -> dict[str, Any]: score = payload["score"] return { "label": label, "strategy": payload["strategy"], "score": score, "quarter_metrics": payload["quarter_metrics"], "walk_forward_summary": payload["walk_forward_summary"], "holdout_metrics": payload["holdout_metrics"], } async def main_async() -> None: parser = argparse.ArgumentParser(description="Quarter robustness for leader intraday momentum") parser.add_argument("--config", default=DEFAULT_CONFIG) parser.add_argument("--wfv-start", default="2025-01-02") parser.add_argument("--wfv-end", default="2025-12-31") parser.add_argument("--holdout-start", default="2026-01-02") parser.add_argument("--holdout-end", default="2026-03-31") parser.add_argument("--train-days", type=int, default=84) parser.add_argument("--test-days", type=int, default=21) parser.add_argument("--step-days", type=int, default=21) parser.add_argument("--output-dir", default="runs/intraday/research") args = parser.parse_args() config = load_config(args.config) if config.strategy_mode != "momentum": raise ValueError("momentum_quarterly only supports momentum configs") settings = get_settings() async with make_intraday_oracle_client(settings) as client: print("[1/3] Building 2025 context...") context_2025 = await build_momentum_research_context( config, args.wfv_start, args.wfv_end, client, print_progress=True, ) print("[2/3] Building 2026 Q1 holdout context...") holdout_context = await build_momentum_research_context( config, args.holdout_start, args.holdout_end, client, print_progress=True, ) print("[3/3] Evaluating quarter-robust candidates...") rows: list[dict[str, Any]] = [] candidates = _quarterly_candidate_overrides() for idx, (label, overrides) in enumerate(candidates, start=1): print(f"\n Candidate {idx}/{len(candidates)}: {label}") strategy = build_momentum_strategy(config, overrides) payload = evaluate_momentum_quarterly_candidate( context_2025, strategy, train_days=args.train_days, test_days=args.test_days, step_days=args.step_days, holdout_context=holdout_context, ) row = _condensed_row(label, payload) rows.append(row) print( " " f"Quarter score {row['score']['quarterly_selection_score'] or 0:.2f} | " f"Qmean {row['score']['quarter_mean_return_pct'] or 0:.2f}% | " f"Qworst {row['score']['quarter_worst_return_pct'] or 0:.2f}% | " f"WF {row['score']['mean_test_return_pct'] or 0:.2f}% | " f"Q1 {row['score']['holdout_return_pct'] or 0:.2f}%" ) ranked = _rank_results(rows) print("\n=== 2025 Quarterly Robustness Ranking ===") for row in ranked: score = row["score"] print( f"{row['rank']:>2}. {row['label']:<16} " f"qscore {score['quarterly_selection_score']:>7.2f} | " f"qmean {score['quarter_mean_return_pct'] or 0:>6.2f}% | " f"qworst {score['quarter_worst_return_pct'] or 0:>6.2f}% | " f"q+ {score['quarter_positive_rate_pct'] or 0:>5.1f}% | " f"qstd {score['quarter_return_stdev_pct'] or 0:>5.2f} | " f"Q1 {score['holdout_return_pct'] or 0:>6.2f}%" ) out_dir = Path(args.output_dir) out_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") out_path = out_dir / f"leader_momentum_quarterly_{ts}.json" out_path.write_text( json.dumps( { "config": args.config, "wfv_period": [args.wfv_start, args.wfv_end], "holdout_period": [args.holdout_start, args.holdout_end], "train_days": args.train_days, "test_days": args.test_days, "step_days": args.step_days, "rows": ranked, "winner_label": ranked[0]["label"] if ranked else None, "winner_strategy": ranked[0]["strategy"] if ranked else None, }, indent=2, ensure_ascii=True, default=str, ) ) print(f"\nSaved quarterly report to: {out_path}") def main() -> None: asyncio.run(main_async()) if __name__ == "__main__": main()