dragon_glued_refined_sensitivity.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. from __future__ import annotations
  2. from itertools import product
  3. from pathlib import Path
  4. import pandas as pd
  5. from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config, alpha_first_selective_veto_config
  6. from dragon_shared import END_DATE, START_DATE, profit_factor
  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.set_index("date", drop=False)
  12. def _load_true_trade_events(base_dir: Path) -> pd.DataFrame:
  13. return pd.read_csv(base_dir / "true_trade_events.csv", encoding="utf-8-sig")
  14. def _event_match(strategy_events: pd.DataFrame, workbook_events: pd.DataFrame, side: str) -> tuple[int, int]:
  15. wb = set(workbook_events[(workbook_events["side"] == side) & (workbook_events["layer"] == "real_trade")]["date"])
  16. st = set(strategy_events[(strategy_events["side"] == side) & (strategy_events["layer"] == "real_trade")]["date"])
  17. return len(wb & st), len(st - wb)
  18. def _holding_bucket(days: int) -> str:
  19. if days <= 5:
  20. return "00-05d"
  21. if days <= 10:
  22. return "06-10d"
  23. if days <= 20:
  24. return "11-20d"
  25. if days <= 40:
  26. return "21-40d"
  27. return "41d+"
  28. def _run(label: str, indicator_df: pd.DataFrame, workbook_events: pd.DataFrame, config) -> dict[str, object]:
  29. engine = DragonRuleEngine(config=config)
  30. events, trades = engine.run(indicator_df)
  31. events = events[(events["date"] >= START_DATE) & (events["date"] <= END_DATE)].copy()
  32. trades = trades[
  33. (trades["buy_date"] >= START_DATE)
  34. & (trades["buy_date"] <= END_DATE)
  35. & (trades["sell_date"] >= START_DATE)
  36. & (trades["sell_date"] <= END_DATE)
  37. ].copy()
  38. trades["holding_bucket"] = trades["holding_days"].astype(int).map(_holding_bucket)
  39. buy_overlap, buy_extra = _event_match(events, workbook_events, "BUY")
  40. sell_overlap, sell_extra = _event_match(events, workbook_events, "SELL")
  41. return {
  42. "label": label,
  43. "hot_c1_max": getattr(config, "glued_selective_hot_c1_max"),
  44. "hot_b1_min": getattr(config, "glued_selective_hot_b1_min"),
  45. "low_c1_min": getattr(config, "glued_selective_low_c1_min"),
  46. "low_b1_max": getattr(config, "glued_selective_low_b1_max"),
  47. "trades": int(len(trades)),
  48. "win_rate": float((trades["return_pct"] > 0).mean()) if not trades.empty else float("nan"),
  49. "avg_return": float(trades["return_pct"].mean()) if not trades.empty else float("nan"),
  50. "profit_factor": profit_factor(trades["return_pct"]) if not trades.empty else float("nan"),
  51. "real_buy_overlap": int(buy_overlap),
  52. "real_buy_extra": int(buy_extra),
  53. "real_sell_overlap": int(sell_overlap),
  54. "real_sell_extra": int(sell_extra),
  55. "short_00_05d_avg_return": float(trades[trades["holding_bucket"] == "00-05d"]["return_pct"].mean()),
  56. "short_06_10d_avg_return": float(trades[trades["holding_bucket"] == "06-10d"]["return_pct"].mean()),
  57. }
  58. def main() -> None:
  59. base_dir = Path(__file__).resolve().parent
  60. indicator_df = _load_indicator_snapshot(base_dir)
  61. workbook_events = _load_true_trade_events(base_dir)
  62. baseline = alpha_first_selective_veto_config()
  63. candidate = alpha_first_glued_refined_hot_cap_config()
  64. rows = [
  65. _run("current_alpha_control", indicator_df, workbook_events, baseline),
  66. _run("refined_candidate_baseline", indicator_df, workbook_events, candidate),
  67. ]
  68. for hot_c1_max, hot_b1_min, low_c1_min, low_b1_max in product([72.0, 75.0, 78.0], [0.09, 0.10, 0.11], [22.0, 23.0, 24.0], [0.01, 0.02, 0.03]):
  69. cfg = baseline.with_updates(
  70. glued_selective_hot_c1_min=40.0,
  71. glued_selective_hot_c1_max=hot_c1_max,
  72. glued_selective_hot_b1_min=hot_b1_min,
  73. glued_selective_low_c1_min=low_c1_min,
  74. glued_selective_low_c1_max=28.0,
  75. glued_selective_low_b1_max=low_b1_max,
  76. )
  77. label = f"hcap{int(hot_c1_max)}_hb1_{hot_b1_min:.2f}_lmin{int(low_c1_min)}_lb1_{low_b1_max:.2f}"
  78. rows.append(_run(label, indicator_df, workbook_events, cfg))
  79. result = pd.DataFrame(rows)
  80. result.to_csv(base_dir / "dragon_glued_refined_sensitivity.csv", index=False, encoding="utf-8-sig")
  81. candidate_row = result[result["label"] == "refined_candidate_baseline"].iloc[0]
  82. neighborhood = result[~result["label"].isin(["current_alpha_control", "refined_candidate_baseline"])].copy()
  83. neighborhood["delta_avg_return"] = neighborhood["avg_return"] - candidate_row["avg_return"]
  84. neighborhood["delta_profit_factor"] = neighborhood["profit_factor"] - candidate_row["profit_factor"]
  85. neighborhood["delta_real_buy_overlap"] = neighborhood["real_buy_overlap"] - candidate_row["real_buy_overlap"]
  86. neighborhood["delta_real_sell_overlap"] = neighborhood["real_sell_overlap"] - candidate_row["real_sell_overlap"]
  87. summary = pd.DataFrame(
  88. [
  89. {
  90. "scope": "candidate_neighborhood",
  91. "cases": int(len(neighborhood)),
  92. "avg_return_min": float(neighborhood["avg_return"].min()),
  93. "avg_return_max": float(neighborhood["avg_return"].max()),
  94. "profit_factor_min": float(neighborhood["profit_factor"].min()),
  95. "profit_factor_max": float(neighborhood["profit_factor"].max()),
  96. "real_buy_overlap_min": int(neighborhood["real_buy_overlap"].min()),
  97. "real_sell_overlap_min": int(neighborhood["real_sell_overlap"].min()),
  98. "count_better_avg_return": int((neighborhood["avg_return"] >= candidate_row["avg_return"]).sum()),
  99. "count_better_profit_factor": int((neighborhood["profit_factor"] >= candidate_row["profit_factor"]).sum()),
  100. }
  101. ]
  102. )
  103. summary.to_csv(base_dir / "dragon_glued_refined_sensitivity_summary.csv", index=False, encoding="utf-8-sig")
  104. top_avg = neighborhood.sort_values(["avg_return", "profit_factor"], ascending=[False, False]).head(10)
  105. robust = neighborhood[
  106. (neighborhood["avg_return"] >= candidate_row["avg_return"] - 0.0015)
  107. & (neighborhood["profit_factor"] >= candidate_row["profit_factor"] - 0.20)
  108. & (neighborhood["real_buy_overlap"] >= candidate_row["real_buy_overlap"] - 1)
  109. & (neighborhood["real_sell_overlap"] >= candidate_row["real_sell_overlap"] - 1)
  110. ].copy()
  111. lines = [
  112. "# Dragon Glued Refined Sensitivity",
  113. "",
  114. "- Scope: local neighborhood around the refined glued candidate, not a broad black-box search.",
  115. "",
  116. "## Candidate Baseline",
  117. f"- avg_return `{candidate_row['avg_return']:.2%}`, profit_factor `{candidate_row['profit_factor']:.2f}`, real BUY / SELL `{int(candidate_row['real_buy_overlap'])}/{int(candidate_row['real_sell_overlap'])}`",
  118. "",
  119. "## Neighborhood Summary",
  120. f"- tested cases: `{int(len(neighborhood))}`",
  121. f"- avg_return range: `{neighborhood['avg_return'].min():.2%}` to `{neighborhood['avg_return'].max():.2%}`",
  122. f"- profit_factor range: `{neighborhood['profit_factor'].min():.2f}` to `{neighborhood['profit_factor'].max():.2f}`",
  123. f"- overlap floor: BUY `{int(neighborhood['real_buy_overlap'].min())}`, SELL `{int(neighborhood['real_sell_overlap'].min())}`",
  124. f"- cases with avg_return >= candidate: `{int((neighborhood['avg_return'] >= candidate_row['avg_return']).sum())}`",
  125. f"- cases with profit_factor >= candidate: `{int((neighborhood['profit_factor'] >= candidate_row['profit_factor']).sum())}`",
  126. f"- robust-nearby cases: `{int(len(robust))}`",
  127. "",
  128. "## Top Local Variants",
  129. ]
  130. for _, row in top_avg.iterrows():
  131. lines.append(
  132. f"- `{row['label']}`: avg_return `{row['avg_return']:.2%}`, profit_factor `{row['profit_factor']:.2f}`, "
  133. f"real BUY / SELL `{int(row['real_buy_overlap'])}/{int(row['real_sell_overlap'])}`"
  134. )
  135. lines.extend(
  136. [
  137. "",
  138. "## Quant Judgment",
  139. "- If the neighborhood remains strong around the candidate, the branch is locally stable rather than dependent on a single knife-edge threshold.",
  140. "- If only one exact setting dominates while nearby cases collapse, the branch still carries threshold fragility risk.",
  141. ]
  142. )
  143. (base_dir / "dragon_glued_refined_sensitivity.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  144. if __name__ == "__main__":
  145. main()