dragon_rc1_release.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. from __future__ import annotations
  2. import json
  3. from dataclasses import asdict
  4. from pathlib import Path
  5. import pandas as pd
  6. from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
  7. from dragon_indicators import DragonIndicatorConfig, DragonIndicatorEngine
  8. from dragon_shared import END_DATE, START_DATE, evaluation_years, format_num as _format_num, format_pct as _format_pct, profit_factor
  9. from dragon_strategy import DragonRuleEngine
  10. RC_VERSION = "RC1"
  11. RC_BRANCH = "alpha_first_glued_refined_hot_cap"
  12. def _max_drawdown(returns: pd.Series) -> tuple[float, int]:
  13. equity = (1.0 + returns.astype(float)).cumprod()
  14. peak = equity.cummax()
  15. dd = equity / peak - 1.0
  16. max_dd = float(dd.min()) if not dd.empty else float("nan")
  17. duration = 0
  18. max_duration = 0
  19. for value in dd:
  20. if value < 0:
  21. duration += 1
  22. max_duration = max(max_duration, duration)
  23. else:
  24. duration = 0
  25. return max_dd, max_duration
  26. def _load_indicator_snapshot(base_dir: Path) -> pd.DataFrame:
  27. path = base_dir / "dragon_indicator_snapshot.csv"
  28. if path.exists():
  29. df = pd.read_csv(path, encoding="utf-8-sig")
  30. df["date"] = pd.to_datetime(df["date"])
  31. return df.sort_values("date").reset_index(drop=True)
  32. engine = DragonIndicatorEngine(DragonIndicatorConfig(start_date="2015-01-01", end_date="2026-01-31"))
  33. df = engine.compute(engine.fetch_daily_data()).reset_index(drop=False).rename(columns={"index": "date"})
  34. df.to_csv(path, index=False, encoding="utf-8-sig")
  35. return df
  36. def main() -> None:
  37. base_dir = Path(__file__).resolve().parent
  38. indicators = _load_indicator_snapshot(base_dir)
  39. indexed = indicators.set_index("date", drop=False)
  40. config = alpha_first_glued_refined_hot_cap_config()
  41. engine = DragonRuleEngine(config=config)
  42. events, trades = engine.run(indexed)
  43. trades = trades[
  44. (trades["buy_date"] >= START_DATE)
  45. & (trades["buy_date"] <= END_DATE)
  46. & (trades["sell_date"] >= START_DATE)
  47. & (trades["sell_date"] <= END_DATE)
  48. ].copy()
  49. returns = trades["return_pct"].astype(float)
  50. compounded = float((1.0 + returns).prod() - 1.0)
  51. years = evaluation_years(START_DATE, END_DATE)
  52. cagr = float((1.0 + compounded) ** (1.0 / years) - 1.0)
  53. max_dd, dd_duration = _max_drawdown(returns)
  54. snapshot = {
  55. "release_version": RC_VERSION,
  56. "branch_name": RC_BRANCH,
  57. "evaluation_window": {"start": START_DATE, "end": END_DATE},
  58. "trade_count": int(len(trades)),
  59. "win_rate": float((returns > 0).mean()),
  60. "avg_return": float(returns.mean()),
  61. "median_return": float(returns.median()),
  62. "profit_factor": profit_factor(returns),
  63. "compounded_return": compounded,
  64. "cagr": cagr,
  65. "max_drawdown": max_dd,
  66. "drawdown_duration_trades": dd_duration,
  67. "config": {
  68. **asdict(config),
  69. "disabled_rules": sorted(config.disabled_rules),
  70. },
  71. }
  72. (base_dir / "dragon_rc1_config_snapshot.json").write_text(
  73. json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n",
  74. encoding="utf-8",
  75. )
  76. lines = [
  77. "# Dragon RC1 Release",
  78. "",
  79. f"- Release version: `{RC_VERSION}`",
  80. f"- Strategy branch: `{RC_BRANCH}`",
  81. f"- Freeze date: `{pd.Timestamp.now().date().isoformat()}`",
  82. f"- Evaluation window: `{START_DATE}` to `{END_DATE}`",
  83. "",
  84. "## Freeze Intent",
  85. "- `RC1` is the first frozen release candidate of the refined alpha branch.",
  86. "- Its purpose is not to continue blind optimization, but to serve as the tracked strategy candidate for daily signal production and monitoring.",
  87. "",
  88. "## Frozen Headline",
  89. f"- trades: `{int(len(trades))}`",
  90. f"- win_rate: `{_format_pct(float((returns > 0).mean()))}`",
  91. f"- avg_return: `{_format_pct(float(returns.mean()))}`",
  92. f"- median_return: `{_format_pct(float(returns.median()))}`",
  93. f"- profit_factor: `{_format_num(profit_factor(returns))}`",
  94. f"- compounded_return: `{_format_pct(compounded)}`",
  95. f"- CAGR: `{_format_pct(cagr)}`",
  96. f"- max_drawdown: `{_format_pct(max_dd)}`",
  97. f"- drawdown_duration_trades: `{dd_duration}`",
  98. "",
  99. "## Operating Rule",
  100. "- Treat `RC1` as the forward default branch.",
  101. "- Do not change frozen core parameters inside `RC1` directly.",
  102. "- Any future core-threshold change must create a new named branch and go through attribution plus execution-aware stress again.",
  103. "",
  104. "## Artifacts",
  105. "- `dragon_rc1_config_snapshot.json`",
  106. "- `dragon_formal_strategy_governance.md`",
  107. "- `dragon_parameter_governance.md`",
  108. "- `dragon_formal_strategy_memo.md`",
  109. ]
  110. (base_dir / "dragon_rc1_release.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  111. if __name__ == "__main__":
  112. main()