dragon_alpha_branch_governance.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. import pandas as pd
  5. def _load_csv(base_dir: Path, name: str) -> pd.DataFrame:
  6. return pd.read_csv(base_dir / name, encoding="utf-8-sig")
  7. def _format_pct(value: float) -> str:
  8. if pd.isna(value):
  9. return "NA"
  10. if value == float("inf"):
  11. return "inf"
  12. return f"{value:.2%}"
  13. def _format_num(value: float) -> str:
  14. if pd.isna(value):
  15. return "NA"
  16. if value == float("inf"):
  17. return "inf"
  18. return f"{value:.2f}"
  19. def _wf_stats(df: pd.DataFrame, branch: str, scheme: str) -> tuple[int, int, float]:
  20. view = df[(df["branch"] == branch) & (df["scheme"] == scheme)].copy()
  21. positive = int((view["test_avg_return"] > 0).sum()) if not view.empty else 0
  22. total = int(len(view))
  23. avg_test = float(view["test_avg_return"].mean()) if not view.empty else float("nan")
  24. return positive, total, avg_test
  25. def main() -> None:
  26. base_dir = Path(__file__).resolve().parent
  27. branch_summary = _load_csv(base_dir, "dragon_glued_refined_branch_summary.csv")
  28. walk_forward = _load_csv(base_dir, "dragon_glued_refined_branch_walk_forward.csv")
  29. removed = _load_csv(base_dir, "dragon_glued_refined_removed_trade_attribution.csv")
  30. alpha = branch_summary[branch_summary["branch"] == "alpha_first_selective_veto"].iloc[0]
  31. refined = branch_summary[branch_summary["branch"] == "alpha_first_glued_refined_hot_cap"].iloc[0]
  32. af_anchor_pos, af_anchor_total, af_anchor_avg = _wf_stats(walk_forward, "alpha_first_selective_veto", "anchored_expanding")
  33. ref_anchor_pos, ref_anchor_total, ref_anchor_avg = _wf_stats(walk_forward, "alpha_first_glued_refined_hot_cap", "anchored_expanding")
  34. af_roll_pos, af_roll_total, af_roll_avg = _wf_stats(walk_forward, "alpha_first_selective_veto", "rolling_3y")
  35. ref_roll_pos, ref_roll_total, ref_roll_avg = _wf_stats(walk_forward, "alpha_first_glued_refined_hot_cap", "rolling_3y")
  36. removed_trades = int(len(removed))
  37. removed_avg_return = float(removed["return_pct"].mean()) if not removed.empty else float("nan")
  38. removed_win_rate = float((removed["return_pct"] > 0).mean()) if not removed.empty else float("nan")
  39. removed_keep = int((removed["recommendation"] == "KEEP_REMOVAL").sum())
  40. removed_observe = int((removed["recommendation"] == "OBSERVE_REMOVAL").sum())
  41. removed_over = int((removed["recommendation"] == "OVER_REMOVAL").sum())
  42. avg_return_delta = float(refined["avg_return"] - alpha["avg_return"])
  43. profit_factor_delta = float(refined["profit_factor"] - alpha["profit_factor"])
  44. buy_overlap_delta = int(refined["real_buy_overlap"] - alpha["real_buy_overlap"])
  45. sell_overlap_delta = int(refined["real_sell_overlap"] - alpha["real_sell_overlap"])
  46. short_00_05d_delta = float(refined["short_00_05d_avg_return"] - alpha["short_00_05d_avg_return"])
  47. short_06_10d_delta = float(refined["short_06_10d_avg_return"] - alpha["short_06_10d_avg_return"])
  48. headline_quality_gate = (
  49. avg_return_delta >= 0.003
  50. and profit_factor_delta >= 0.50
  51. and short_00_05d_delta >= 0
  52. and short_06_10d_delta >= 0
  53. )
  54. stability_gate = (
  55. ref_anchor_pos >= af_anchor_pos
  56. and ref_roll_pos >= af_roll_pos
  57. and ref_anchor_avg >= af_anchor_avg
  58. and ref_roll_avg >= af_roll_avg
  59. )
  60. removal_quality_gate = removed.empty or (
  61. removed_over == 0
  62. and removed_observe <= 1
  63. and removed_win_rate <= 0.05
  64. and removed_avg_return < 0
  65. )
  66. # Automatic promotion should not occur if the candidate loses more than 8
  67. # additional aligned BUYs or SELLs versus the current formal alpha branch.
  68. # Positive deltas are improvements and should not block promotion.
  69. alignment_cost_gate = (buy_overlap_delta >= -8 and sell_overlap_delta >= -8)
  70. if headline_quality_gate and stability_gate and removal_quality_gate and alignment_cost_gate:
  71. final_decision = "PROMOTE_REFINED_ALPHA_BASELINE"
  72. elif headline_quality_gate and stability_gate and removal_quality_gate:
  73. final_decision = "DUAL_TRACK_GOVERNANCE"
  74. else:
  75. final_decision = "KEEP_CURRENT_ALPHA_BASELINE"
  76. matrix_rows = [
  77. {
  78. "branch": "alpha_first_selective_veto",
  79. "role": "current_formal_alpha",
  80. "trades": int(alpha["trades"]),
  81. "win_rate": float(alpha["win_rate"]),
  82. "avg_return": float(alpha["avg_return"]),
  83. "median_return": float(alpha["median_return"]),
  84. "profit_factor": float(alpha["profit_factor"]),
  85. "avg_mfe": float(alpha["avg_mfe"]),
  86. "avg_mae": float(alpha["avg_mae"]),
  87. "short_00_05d_avg_return": float(alpha["short_00_05d_avg_return"]),
  88. "short_06_10d_avg_return": float(alpha["short_06_10d_avg_return"]),
  89. "real_buy_overlap": int(alpha["real_buy_overlap"]),
  90. "real_sell_overlap": int(alpha["real_sell_overlap"]),
  91. "anchored_positive_years": af_anchor_pos,
  92. "anchored_total_years": af_anchor_total,
  93. "anchored_avg_test_return": af_anchor_avg,
  94. "rolling_positive_years": af_roll_pos,
  95. "rolling_total_years": af_roll_total,
  96. "rolling_avg_test_return": af_roll_avg,
  97. "removed_trades_vs_current_alpha": 0,
  98. "removed_avg_return_vs_current_alpha": float("nan"),
  99. "removed_win_rate_vs_current_alpha": float("nan"),
  100. "over_removal_count_vs_current_alpha": 0,
  101. "observe_removal_count_vs_current_alpha": 0,
  102. "keep_removal_count_vs_current_alpha": 0,
  103. "headline_quality_gate": None,
  104. "stability_gate": None,
  105. "removal_quality_gate": None,
  106. "alignment_cost_gate": None,
  107. "governance_decision": "CURRENT_BASELINE",
  108. },
  109. {
  110. "branch": "alpha_first_glued_refined_hot_cap",
  111. "role": "leading_candidate_alpha",
  112. "trades": int(refined["trades"]),
  113. "win_rate": float(refined["win_rate"]),
  114. "avg_return": float(refined["avg_return"]),
  115. "median_return": float(refined["median_return"]),
  116. "profit_factor": float(refined["profit_factor"]),
  117. "avg_mfe": float(refined["avg_mfe"]),
  118. "avg_mae": float(refined["avg_mae"]),
  119. "short_00_05d_avg_return": float(refined["short_00_05d_avg_return"]),
  120. "short_06_10d_avg_return": float(refined["short_06_10d_avg_return"]),
  121. "real_buy_overlap": int(refined["real_buy_overlap"]),
  122. "real_sell_overlap": int(refined["real_sell_overlap"]),
  123. "anchored_positive_years": ref_anchor_pos,
  124. "anchored_total_years": ref_anchor_total,
  125. "anchored_avg_test_return": ref_anchor_avg,
  126. "rolling_positive_years": ref_roll_pos,
  127. "rolling_total_years": ref_roll_total,
  128. "rolling_avg_test_return": ref_roll_avg,
  129. "removed_trades_vs_current_alpha": removed_trades,
  130. "removed_avg_return_vs_current_alpha": removed_avg_return,
  131. "removed_win_rate_vs_current_alpha": removed_win_rate,
  132. "over_removal_count_vs_current_alpha": removed_over,
  133. "observe_removal_count_vs_current_alpha": removed_observe,
  134. "keep_removal_count_vs_current_alpha": removed_keep,
  135. "headline_quality_gate": headline_quality_gate,
  136. "stability_gate": stability_gate,
  137. "removal_quality_gate": removal_quality_gate,
  138. "alignment_cost_gate": alignment_cost_gate,
  139. "governance_decision": final_decision,
  140. },
  141. ]
  142. matrix = pd.DataFrame(matrix_rows)
  143. matrix.to_csv(base_dir / "dragon_alpha_branch_governance_matrix.csv", index=False, encoding="utf-8-sig")
  144. decision_payload = {
  145. "current_formal_alpha": "alpha_first_selective_veto",
  146. "leading_candidate_alpha": "alpha_first_glued_refined_hot_cap",
  147. "avg_return_delta_vs_current": avg_return_delta,
  148. "profit_factor_delta_vs_current": profit_factor_delta,
  149. "buy_overlap_delta_vs_current": buy_overlap_delta,
  150. "sell_overlap_delta_vs_current": sell_overlap_delta,
  151. "headline_quality_gate": headline_quality_gate,
  152. "stability_gate": stability_gate,
  153. "removal_quality_gate": removal_quality_gate,
  154. "alignment_cost_gate": alignment_cost_gate,
  155. "final_decision": final_decision,
  156. }
  157. (base_dir / "dragon_alpha_branch_governance_decision.json").write_text(
  158. json.dumps(decision_payload, indent=2, ensure_ascii=False) + "\n",
  159. encoding="utf-8",
  160. )
  161. lines = [
  162. "# Dragon Alpha Branch Governance",
  163. "",
  164. "## Scope",
  165. "- Current formal alpha branch: `alpha_first_selective_veto`.",
  166. "- Leading candidate branch: `alpha_first_glued_refined_hot_cap`.",
  167. "- Goal: decide whether the refined branch should replace the current formal alpha branch or remain a governed candidate.",
  168. "",
  169. "## Headline Metrics",
  170. f"- current alpha: trades `{int(alpha['trades'])}`, avg_return `{_format_pct(float(alpha['avg_return']))}`, profit_factor `{_format_num(float(alpha['profit_factor']))}`, real BUY / SELL `{int(alpha['real_buy_overlap'])}/{int(alpha['real_sell_overlap'])}`",
  171. f"- refined candidate: trades `{int(refined['trades'])}`, avg_return `{_format_pct(float(refined['avg_return']))}`, profit_factor `{_format_num(float(refined['profit_factor']))}`, real BUY / SELL `{int(refined['real_buy_overlap'])}/{int(refined['real_sell_overlap'])}`",
  172. f"- delta: avg_return `{_format_pct(avg_return_delta)}`, profit_factor `{_format_num(profit_factor_delta)}`, BUY overlap `{buy_overlap_delta}`, SELL overlap `{sell_overlap_delta}`",
  173. "",
  174. "## Risk And Quality",
  175. f"- avg MFE / MAE: current `{_format_pct(float(alpha['avg_mfe']))}` / `{_format_pct(float(alpha['avg_mae']))}` vs refined `{_format_pct(float(refined['avg_mfe']))}` / `{_format_pct(float(refined['avg_mae']))}`",
  176. f"- short `00-05d`: current `{_format_pct(float(alpha['short_00_05d_avg_return']))}` vs refined `{_format_pct(float(refined['short_00_05d_avg_return']))}`",
  177. f"- short `06-10d`: current `{_format_pct(float(alpha['short_06_10d_avg_return']))}` vs refined `{_format_pct(float(refined['short_06_10d_avg_return']))}`",
  178. "",
  179. "## Walk-Forward",
  180. f"- anchored expanding: current `{af_anchor_pos}/{af_anchor_total}`, avg `{_format_pct(af_anchor_avg)}` vs refined `{ref_anchor_pos}/{ref_anchor_total}`, avg `{_format_pct(ref_anchor_avg)}`",
  181. f"- rolling 3Y: current `{af_roll_pos}/{af_roll_total}`, avg `{_format_pct(af_roll_avg)}` vs refined `{ref_roll_pos}/{ref_roll_total}`, avg `{_format_pct(ref_roll_avg)}`",
  182. "",
  183. "## Removed-Trade Attribution",
  184. f"- removed trades vs current alpha: `{removed_trades}`",
  185. f"- removed-set avg_return / win_rate: `{_format_pct(removed_avg_return)}` / `{_format_pct(removed_win_rate)}`",
  186. f"- recommendation mix KEEP / OBSERVE / OVER: `{removed_keep}/{removed_observe}/{removed_over}`",
  187. "- Interpretation: the refined branch now improves by removing only weak, losing short-holding glued trades; it does not rely on deleting profitable samples.",
  188. "",
  189. "## Upgrade Gates",
  190. "- `headline_quality_gate`: requires avg_return delta `>= +0.30%`, profit_factor delta `>= +0.50`, and no short-bucket deterioration.",
  191. f" result: `{'PASS' if headline_quality_gate else 'FAIL'}`",
  192. "- `stability_gate`: requires anchored and rolling walk-forward to be no worse than the current formal alpha branch.",
  193. f" result: `{'PASS' if stability_gate else 'FAIL'}`",
  194. "- `removal_quality_gate`: requires `OVER_REMOVAL = 0`, `OBSERVE_REMOVAL <= 1`, removed-set win_rate `<= 5%`, and removed-set avg_return `< 0`.",
  195. f" result: `{'PASS' if removal_quality_gate else 'FAIL'}`",
  196. "- `alignment_cost_gate`: automatic promotion only if incremental overlap loss is no more than `8` additional BUYs and `8` additional SELLs versus the current formal alpha branch.",
  197. f" result: `{'PASS' if alignment_cost_gate else 'FAIL'}`",
  198. "",
  199. "## Final Decision",
  200. f"- governance_decision: `{final_decision}`",
  201. ]
  202. if final_decision == "PROMOTE_REFINED_ALPHA_BASELINE":
  203. lines.extend(
  204. [
  205. "- Decision: promote `alpha_first_glued_refined_hot_cap` to the new formal alpha-first baseline.",
  206. "- Reason: quality, stability, removal quality, and alignment cost all pass together.",
  207. ]
  208. )
  209. elif final_decision == "DUAL_TRACK_GOVERNANCE":
  210. lines.extend(
  211. [
  212. "- Decision: keep `alpha_first_selective_veto` as the formal alpha branch and keep `alpha_first_glued_refined_hot_cap` as the governed leading candidate.",
  213. "- Reason: quality, stability, and removal quality all pass, but alignment loss is still large enough that promotion should be explicit rather than automatic.",
  214. ]
  215. )
  216. else:
  217. lines.extend(
  218. [
  219. "- Decision: keep `alpha_first_selective_veto` as the formal alpha branch.",
  220. "- Reason: the refined candidate does not pass enough upgrade gates to justify even governed promotion.",
  221. ]
  222. )
  223. lines.extend(["", "## Recommendation"])
  224. if final_decision == "PROMOTE_REFINED_ALPHA_BASELINE":
  225. lines.extend(
  226. [
  227. "- Promote `alpha_first_glued_refined_hot_cap` into the formal alpha branch and freeze a new governed release snapshot.",
  228. "- Keep `alpha_first_selective_veto` as the immediate control branch for post-promotion monitoring.",
  229. ]
  230. )
  231. elif final_decision == "DUAL_TRACK_GOVERNANCE":
  232. lines.extend(
  233. [
  234. "- Keep `alpha_first_selective_veto` as the current formal alpha branch for now.",
  235. "- Keep `alpha_first_glued_refined_hot_cap` as the first governed promotion candidate if the governance objective explicitly shifts toward stronger alpha.",
  236. ]
  237. )
  238. else:
  239. lines.extend(
  240. [
  241. "- Keep `alpha_first_selective_veto` as the formal alpha branch.",
  242. "- Re-open refined promotion only after a new candidate improves quality without failing the current governance gates.",
  243. ]
  244. )
  245. (base_dir / "dragon_alpha_branch_governance.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  246. if __name__ == "__main__":
  247. main()