dragon_deep_oversold_selective_veto_experiments.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. from dragon_strategy import DragonRuleEngine
  5. from dragon_strategy_config import StrategyConfig
  6. def _load_indicator_snapshot(base_dir: Path) -> pd.DataFrame:
  7. df = pd.read_csv(base_dir / "dragon_indicator_snapshot.csv", encoding="utf-8-sig")
  8. df["date"] = pd.to_datetime(df["date"])
  9. return df.set_index("date", drop=False)
  10. def _load_true_trade_events(base_dir: Path) -> pd.DataFrame:
  11. return pd.read_csv(base_dir / "true_trade_events.csv", encoding="utf-8-sig")
  12. def _profit_factor(series: pd.Series) -> float:
  13. gross_profit = series[series > 0].sum()
  14. gross_loss = -series[series < 0].sum()
  15. if gross_loss == 0:
  16. return float("inf") if gross_profit > 0 else 0.0
  17. return float(gross_profit / gross_loss)
  18. def _event_match(strategy_events: pd.DataFrame, workbook_events: pd.DataFrame, side: str) -> tuple[int, int, int]:
  19. wb = set(workbook_events[(workbook_events["side"] == side) & (workbook_events["layer"] == "real_trade")]["date"])
  20. st = set(strategy_events[(strategy_events["side"] == side) & (strategy_events["layer"] == "real_trade")]["date"])
  21. return len(wb & st), len(wb - st), len(st - wb)
  22. def _run_experiment(
  23. label: str,
  24. config: StrategyConfig,
  25. indicator_df: pd.DataFrame,
  26. workbook_events: pd.DataFrame,
  27. first_date: str,
  28. last_date: str,
  29. ) -> tuple[dict[str, object], pd.DataFrame]:
  30. engine = DragonRuleEngine(config=config)
  31. events, trades = engine.run(indicator_df)
  32. events = events[(events["date"] >= first_date) & (events["date"] <= last_date)].copy()
  33. trades = trades[
  34. (trades["buy_date"] >= first_date)
  35. & (trades["buy_date"] <= last_date)
  36. & (trades["sell_date"] >= first_date)
  37. & (trades["sell_date"] <= last_date)
  38. ].copy()
  39. buy_overlap, buy_missing, buy_extra = _event_match(events, workbook_events, "BUY")
  40. sell_overlap, sell_missing, sell_extra = _event_match(events, workbook_events, "SELL")
  41. deep_trades = trades[trades["buy_reason"].str.startswith("deep_oversold_rebound_buy")].copy()
  42. weak_trades = deep_trades[
  43. deep_trades["buy_reason"].isin(
  44. [
  45. "deep_oversold_rebound_buy:positive_b1_rebound",
  46. "deep_oversold_rebound_buy:shallow_false_start",
  47. "deep_oversold_rebound_buy:mixed_oversold",
  48. ]
  49. )
  50. ].copy()
  51. event_slice = events[
  52. (events["layer"] == "real_trade")
  53. & events["reason"].str.startswith("deep_oversold_rebound_buy")
  54. ].copy()
  55. event_slice["experiment"] = label
  56. row = {
  57. "experiment": label,
  58. "trades": int(len(trades)),
  59. "win_rate": float((trades["return_pct"] > 0).mean()) if not trades.empty else float("nan"),
  60. "avg_return": float(trades["return_pct"].mean()) if not trades.empty else float("nan"),
  61. "median_return": float(trades["return_pct"].median()) if not trades.empty else float("nan"),
  62. "profit_factor": _profit_factor(trades["return_pct"]) if not trades.empty else float("nan"),
  63. "real_buy_overlap": int(buy_overlap),
  64. "real_buy_missing": int(buy_missing),
  65. "real_buy_extra": int(buy_extra),
  66. "real_sell_overlap": int(sell_overlap),
  67. "real_sell_missing": int(sell_missing),
  68. "real_sell_extra": int(sell_extra),
  69. "deep_trade_count": int(len(deep_trades)),
  70. "deep_weak_trade_count": int(len(weak_trades)),
  71. "deep_avg_return": float(deep_trades["return_pct"].mean()) if not deep_trades.empty else float("nan"),
  72. "deep_weak_avg_return": float(weak_trades["return_pct"].mean()) if not weak_trades.empty else float("nan"),
  73. }
  74. return row, event_slice
  75. def main() -> None:
  76. base_dir = Path(__file__).resolve().parent
  77. indicator_df = _load_indicator_snapshot(base_dir)
  78. workbook_events = _load_true_trade_events(base_dir)
  79. first_date = workbook_events["date"].min()
  80. last_date = workbook_events["date"].max()
  81. baseline = StrategyConfig()
  82. experiments = [
  83. ("baseline", baseline),
  84. (
  85. "selective_veto_positive_c1_lt_15_3",
  86. baseline.with_updates(deep_oversold_selective_positive_b1_c1_max=15.3),
  87. ),
  88. (
  89. "selective_veto_shallow_jan_style",
  90. baseline.with_updates(
  91. deep_oversold_selective_shallow_c1_min=12.0,
  92. deep_oversold_selective_shallow_b1_min=-0.025,
  93. ),
  94. ),
  95. (
  96. "selective_veto_positive_and_shallow",
  97. baseline.with_updates(
  98. deep_oversold_selective_positive_b1_c1_max=15.3,
  99. deep_oversold_selective_shallow_c1_min=12.0,
  100. deep_oversold_selective_shallow_b1_min=-0.025,
  101. ),
  102. ),
  103. (
  104. "selective_veto_plus_mixed_c1_lt_10_2_no_ql",
  105. baseline.with_updates(
  106. deep_oversold_selective_positive_b1_c1_max=15.3,
  107. deep_oversold_selective_shallow_c1_min=12.0,
  108. deep_oversold_selective_shallow_b1_min=-0.025,
  109. deep_oversold_selective_mixed_c1_max=10.2,
  110. deep_oversold_selective_mixed_require_no_ql=True,
  111. ),
  112. ),
  113. (
  114. "block_all_remaining_weak_subtypes",
  115. baseline.with_updates(
  116. deep_oversold_block_positive_b1_rebound=True,
  117. deep_oversold_block_shallow_false_start_without_ql=True,
  118. ),
  119. ),
  120. ]
  121. rows: list[dict[str, object]] = []
  122. event_frames: list[pd.DataFrame] = []
  123. for label, config in experiments:
  124. row, event_slice = _run_experiment(label, config, indicator_df, workbook_events, first_date, last_date)
  125. rows.append(row)
  126. event_frames.append(event_slice)
  127. result_df = pd.DataFrame(rows)
  128. baseline_row = result_df[result_df["experiment"] == "baseline"].iloc[0]
  129. for col in [
  130. "trades",
  131. "win_rate",
  132. "avg_return",
  133. "median_return",
  134. "profit_factor",
  135. "real_buy_overlap",
  136. "real_sell_overlap",
  137. "deep_trade_count",
  138. "deep_weak_trade_count",
  139. "deep_avg_return",
  140. "deep_weak_avg_return",
  141. ]:
  142. result_df[f"delta_{col}"] = result_df[col] - baseline_row[col]
  143. event_df = pd.concat(event_frames, ignore_index=True)
  144. result_df.to_csv(base_dir / "dragon_deep_oversold_selective_veto_experiments.csv", index=False, encoding="utf-8-sig")
  145. event_df.to_csv(base_dir / "dragon_deep_oversold_selective_veto_event_changes.csv", index=False, encoding="utf-8-sig")
  146. lines = [
  147. "# Dragon Deep Oversold Selective Veto Experiments",
  148. "",
  149. "- Goal: test whether weak deep-oversold alpha can be improved by vetoing only the most pathological local patterns, instead of blocking whole subtypes.",
  150. "- Baseline objective is not preserved here; this is an alpha-first research pack.",
  151. "",
  152. "## Summary",
  153. ]
  154. for _, row in result_df.iterrows():
  155. lines.append(
  156. f"- `{row['experiment']}`: trades `{int(row['trades'])}`, avg_return `{row['avg_return']:.2%}`, "
  157. f"profit_factor `{row['profit_factor']:.2f}`, real BUY `{int(row['real_buy_overlap'])}`, real SELL `{int(row['real_sell_overlap'])}`, "
  158. f"deep weak trades `{int(row['deep_weak_trade_count'])}`"
  159. )
  160. lines.extend(["", "## Delta Vs Baseline"])
  161. for _, row in result_df[result_df["experiment"] != "baseline"].iterrows():
  162. lines.append(
  163. f"- `{row['experiment']}`: delta_avg_return `{row['delta_avg_return']:.2%}`, "
  164. f"delta_profit_factor `{row['delta_profit_factor']:.2f}`, "
  165. f"delta_deep_weak_avg_return `{row['delta_deep_weak_avg_return']:.2%}`, "
  166. f"real BUY `{int(row['real_buy_overlap'])}`, real SELL `{int(row['real_sell_overlap'])}`"
  167. )
  168. best = result_df[result_df["experiment"] != "baseline"].sort_values(
  169. ["avg_return", "profit_factor"], ascending=[False, False]
  170. ).head(1)
  171. if not best.empty:
  172. row = best.iloc[0]
  173. lines.extend(
  174. [
  175. "",
  176. "## Quant Judgment",
  177. f"- Best branch in this pack: `{row['experiment']}` with avg_return `{row['avg_return']:.2%}` and profit_factor `{row['profit_factor']:.2f}`.",
  178. "- Compare this result to `block_all_remaining_weak_subtypes` to see whether narrow veto meaningfully preserves useful edge while still removing obvious losers.",
  179. "- If a narrow veto matches most of the broad-block benefit with smaller date loss, it is the better alpha-first redesign candidate.",
  180. ]
  181. )
  182. (base_dir / "dragon_deep_oversold_selective_veto_experiments.md").write_text(
  183. "\n".join(lines) + "\n",
  184. encoding="utf-8",
  185. )
  186. if __name__ == "__main__":
  187. main()