dragon_refined_execution_validation.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config, alpha_first_selective_veto_config
  5. from dragon_execution_common import apply_execution_model as _apply_execution_model, risk_cluster as _risk_cluster, summary as _summary
  6. from dragon_shared import END_DATE, START_DATE, format_num as _format_num, format_pct as _format_pct
  7. from dragon_strategy import DragonRuleEngine
  8. def _load_indicator_snapshot(base_dir: Path) -> pd.DataFrame:
  9. df = pd.read_csv(base_dir / "dragon_indicator_snapshot.csv", encoding="utf-8-sig")
  10. df["date"] = pd.to_datetime(df["date"])
  11. return df.sort_values("date").reset_index(drop=True)
  12. def _entry_family(reason: str) -> str:
  13. return str(reason).split(":", 1)[0]
  14. def _run_branch(indicators: pd.DataFrame, config) -> pd.DataFrame:
  15. indexed = indicators.set_index("date", drop=False)
  16. engine = DragonRuleEngine(config=config)
  17. _, trades = engine.run(indexed)
  18. trades = trades[
  19. (trades["buy_date"] >= START_DATE)
  20. & (trades["buy_date"] <= END_DATE)
  21. & (trades["sell_date"] >= START_DATE)
  22. & (trades["sell_date"] <= END_DATE)
  23. ].copy()
  24. trades["buy_dt"] = pd.to_datetime(trades["buy_date"])
  25. trades["sell_dt"] = pd.to_datetime(trades["sell_date"])
  26. trades["sell_year"] = trades["sell_dt"].dt.year.astype(int)
  27. trades["entry_family"] = trades["buy_reason"].map(_entry_family)
  28. return trades
  29. def _add_execution_prices(trades: pd.DataFrame, indicators: pd.DataFrame) -> pd.DataFrame:
  30. trades = trades.copy()
  31. lookup = indicators.set_index(indicators["date"].dt.date)
  32. next_by_date = {
  33. indicators.iloc[idx]["date"].date().isoformat(): indicators.iloc[idx + 1]
  34. for idx in range(len(indicators) - 1)
  35. }
  36. same_entry: list[float] = []
  37. same_exit: list[float] = []
  38. next_open_entry: list[float] = []
  39. next_open_exit: list[float] = []
  40. next_close_entry: list[float] = []
  41. next_close_exit: list[float] = []
  42. for _, trade in trades.iterrows():
  43. buy_key = trade["buy_date"]
  44. sell_key = trade["sell_date"]
  45. buy_row = lookup.loc[pd.Timestamp(trade["buy_date"]).date()]
  46. sell_row = lookup.loc[pd.Timestamp(trade["sell_date"]).date()]
  47. buy_next = next_by_date.get(buy_key)
  48. sell_next = next_by_date.get(sell_key)
  49. same_entry.append(float(buy_row["close"]))
  50. same_exit.append(float(sell_row["close"]))
  51. next_open_entry.append(float("nan") if buy_next is None else float(buy_next["open"]))
  52. next_open_exit.append(float("nan") if sell_next is None else float(sell_next["open"]))
  53. next_close_entry.append(float("nan") if buy_next is None else float(buy_next["close"]))
  54. next_close_exit.append(float("nan") if sell_next is None else float(sell_next["close"]))
  55. trades["exec_same_close_entry"] = same_entry
  56. trades["exec_same_close_exit"] = same_exit
  57. trades["exec_next_open_entry"] = next_open_entry
  58. trades["exec_next_open_exit"] = next_open_exit
  59. trades["exec_next_close_entry"] = next_close_entry
  60. trades["exec_next_close_exit"] = next_close_exit
  61. return trades
  62. def main() -> None:
  63. base_dir = Path(__file__).resolve().parent
  64. indicators = _load_indicator_snapshot(base_dir)
  65. branches = {
  66. "alpha_first_selective_veto": _add_execution_prices(
  67. _run_branch(indicators, alpha_first_selective_veto_config()),
  68. indicators,
  69. ),
  70. "alpha_first_glued_refined_hot_cap": _add_execution_prices(
  71. _run_branch(indicators, alpha_first_glued_refined_hot_cap_config()),
  72. indicators,
  73. ),
  74. }
  75. execution_models = ["same_close", "next_open", "next_close"]
  76. cost_levels = [0.0, 5.0, 10.0, 20.0]
  77. stress_rows: list[dict[str, object]] = []
  78. latency_rows: list[dict[str, object]] = []
  79. risk_rows: list[dict[str, object]] = []
  80. for branch, trades in branches.items():
  81. for model in execution_models:
  82. model_trades = _apply_execution_model(trades, model, 0.0)
  83. latency_rows.append(_summary(branch, model_trades))
  84. if model in {"same_close", "next_open"}:
  85. risk_rows.append(_risk_cluster(branch, model_trades))
  86. for cost in cost_levels:
  87. stressed = _apply_execution_model(trades, model, cost)
  88. stress_rows.append(_summary(branch, stressed))
  89. stress_df = pd.DataFrame(stress_rows).sort_values(["execution_model", "cost_bps_side", "branch"]).reset_index(drop=True)
  90. latency_df = pd.DataFrame(latency_rows).sort_values(["execution_model", "branch"]).reset_index(drop=True)
  91. risk_df = pd.DataFrame(risk_rows).sort_values(["execution_model", "branch"]).reset_index(drop=True)
  92. stress_df.to_csv(base_dir / "dragon_refined_execution_stress.csv", index=False, encoding="utf-8-sig")
  93. latency_df.to_csv(base_dir / "dragon_refined_latency_review.csv", index=False, encoding="utf-8-sig")
  94. risk_df.to_csv(base_dir / "dragon_refined_risk_cluster_review.csv", index=False, encoding="utf-8-sig")
  95. same_close = latency_df[latency_df["execution_model"] == "same_close"].set_index("branch")
  96. next_open = latency_df[latency_df["execution_model"] == "next_open"].set_index("branch")
  97. next_close = latency_df[latency_df["execution_model"] == "next_close"].set_index("branch")
  98. stress_20 = stress_df[(stress_df["execution_model"] == "next_open") & (stress_df["cost_bps_side"] == 20.0)].set_index("branch")
  99. risk_next_open = risk_df[risk_df["execution_model"] == "next_open"].set_index("branch")
  100. refined_key = "alpha_first_glued_refined_hot_cap"
  101. control_key = "alpha_first_selective_veto"
  102. lines = [
  103. "# Dragon Refined Stability Review",
  104. "",
  105. "## Scope",
  106. "- branches: `alpha_first_selective_veto` vs `alpha_first_glued_refined_hot_cap`",
  107. "- execution models: `same_close`, `next_open`, `next_close`",
  108. "- costs: `0`, `5`, `10`, `20 bps/side`",
  109. "",
  110. "## Latency Review",
  111. f"- same_close control vs refined: avg_return `{_format_pct(float(same_close.loc[control_key, 'avg_return']))}` -> `{_format_pct(float(same_close.loc[refined_key, 'avg_return']))}`, PF `{_format_num(float(same_close.loc[control_key, 'profit_factor']))}` -> `{_format_num(float(same_close.loc[refined_key, 'profit_factor']))}`",
  112. f"- next_open control vs refined: avg_return `{_format_pct(float(next_open.loc[control_key, 'avg_return']))}` -> `{_format_pct(float(next_open.loc[refined_key, 'avg_return']))}`, PF `{_format_num(float(next_open.loc[control_key, 'profit_factor']))}` -> `{_format_num(float(next_open.loc[refined_key, 'profit_factor']))}`",
  113. f"- next_close control vs refined: avg_return `{_format_pct(float(next_close.loc[control_key, 'avg_return']))}` -> `{_format_pct(float(next_close.loc[refined_key, 'avg_return']))}`, PF `{_format_num(float(next_close.loc[control_key, 'profit_factor']))}` -> `{_format_num(float(next_close.loc[refined_key, 'profit_factor']))}`",
  114. "",
  115. "## Cost + Next-Open Stress",
  116. f"- next_open + 20 bps/side control CAGR `{_format_pct(float(stress_20.loc[control_key, 'cagr']))}` vs refined `{_format_pct(float(stress_20.loc[refined_key, 'cagr']))}`",
  117. f"- next_open + 20 bps/side control PF `{_format_num(float(stress_20.loc[control_key, 'profit_factor']))}` vs refined `{_format_num(float(stress_20.loc[refined_key, 'profit_factor']))}`",
  118. f"- next_open + 20 bps/side control max DD `{_format_pct(float(stress_20.loc[control_key, 'max_drawdown']))}` vs refined `{_format_pct(float(stress_20.loc[refined_key, 'max_drawdown']))}`",
  119. "",
  120. "## Risk Cluster Review",
  121. f"- next_open control max loss streak `{int(risk_next_open.loc[control_key, 'max_loss_streak'])}` vs refined `{int(risk_next_open.loc[refined_key, 'max_loss_streak'])}`",
  122. f"- next_open control worst 5-trade sum `{_format_pct(float(risk_next_open.loc[control_key, 'worst_5trade_sum']))}` vs refined `{_format_pct(float(risk_next_open.loc[refined_key, 'worst_5trade_sum']))}`",
  123. f"- next_open control short-loss share `{_format_pct(float(risk_next_open.loc[control_key, 'short_loss_share']))}` vs refined `{_format_pct(float(risk_next_open.loc[refined_key, 'short_loss_share']))}`",
  124. f"- next_open control worst loss family `{risk_next_open.loc[control_key, 'worst_loss_family']}` vs refined `{risk_next_open.loc[refined_key, 'worst_loss_family']}`",
  125. "",
  126. "## Judgment",
  127. "- If refined still leads after next-bar execution and cost drag, its edge is less likely to be a same-bar backtest artifact.",
  128. "- If refined also keeps loss clustering and drawdown no worse than control, the branch is moving closer to a deployable research baseline.",
  129. ]
  130. (base_dir / "dragon_refined_stability_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  131. if __name__ == "__main__":
  132. main()