dragon_deep_oversold_confirmation_experiments.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. df = df.set_index("date", drop=False)
  10. return df
  11. def _load_workbook_events(base_dir: Path) -> pd.DataFrame:
  12. return pd.read_csv(base_dir / "true_trade_events.csv", encoding="utf-8-sig")
  13. def _profit_factor(series: pd.Series) -> float:
  14. gross_profit = series[series > 0].sum()
  15. gross_loss = -series[series < 0].sum()
  16. if gross_loss == 0:
  17. return float("inf") if gross_profit > 0 else 0.0
  18. return float(gross_profit / gross_loss)
  19. def _event_match(strategy_events: pd.DataFrame, workbook_events: pd.DataFrame, side: str) -> tuple[int, int, int]:
  20. wb = set(workbook_events[(workbook_events["side"] == side) & (workbook_events["layer"] == "real_trade")]["date"])
  21. st = set(strategy_events[(strategy_events["side"] == side) & (strategy_events["layer"] == "real_trade")]["date"])
  22. return len(wb & st), len(wb - st), len(st - wb)
  23. def _run_experiment(
  24. label: str,
  25. config: StrategyConfig,
  26. indicator_df: pd.DataFrame,
  27. workbook_events: pd.DataFrame,
  28. first_date: str,
  29. last_date: str,
  30. ) -> tuple[dict[str, object], pd.DataFrame]:
  31. engine = DragonRuleEngine(config=config)
  32. events, trades = engine.run(indicator_df)
  33. events = events[(events["date"] >= first_date) & (events["date"] <= last_date)].copy()
  34. trades = trades[
  35. (trades["buy_date"] >= first_date)
  36. & (trades["buy_date"] <= last_date)
  37. & (trades["sell_date"] >= first_date)
  38. & (trades["sell_date"] <= last_date)
  39. ].copy()
  40. buy_overlap, buy_missing, buy_extra = _event_match(events, workbook_events, "BUY")
  41. sell_overlap, sell_missing, sell_extra = _event_match(events, workbook_events, "SELL")
  42. deep_trades = trades[trades["buy_reason"].str.startswith("deep_oversold_rebound_buy")].copy()
  43. weak_trades = deep_trades[
  44. deep_trades["buy_reason"].isin(
  45. [
  46. "deep_oversold_rebound_buy:positive_b1_rebound",
  47. "deep_oversold_rebound_buy:shallow_false_start",
  48. "deep_oversold_rebound_buy:confirmed_positive_b1_rebound",
  49. "deep_oversold_rebound_buy:confirmed_shallow_false_start",
  50. ]
  51. )
  52. ].copy()
  53. changed_events = events[
  54. (events["layer"] == "real_trade")
  55. & (
  56. events["reason"].str.startswith("deep_oversold_rebound_buy")
  57. | events["reason"].eq("predictive_error_reentry_buy")
  58. )
  59. ].copy()
  60. changed_events["experiment"] = label
  61. row = {
  62. "experiment": label,
  63. "confirm_window_bars": config.deep_oversold_confirm_window_bars,
  64. "trades": int(len(trades)),
  65. "win_rate": float((trades["return_pct"] > 0).mean()) if not trades.empty else float("nan"),
  66. "avg_return": float(trades["return_pct"].mean()) if not trades.empty else float("nan"),
  67. "median_return": float(trades["return_pct"].median()) if not trades.empty else float("nan"),
  68. "profit_factor": _profit_factor(trades["return_pct"]) if not trades.empty else float("nan"),
  69. "real_buy_overlap": int(buy_overlap),
  70. "real_buy_missing": int(buy_missing),
  71. "real_buy_extra": int(buy_extra),
  72. "real_sell_overlap": int(sell_overlap),
  73. "real_sell_missing": int(sell_missing),
  74. "real_sell_extra": int(sell_extra),
  75. "deep_trade_count": int(len(deep_trades)),
  76. "deep_weak_trade_count": int(len(weak_trades)),
  77. "deep_confirmed_trade_count": int(
  78. deep_trades["buy_reason"].str.contains(":confirmed_", regex=False).sum()
  79. ),
  80. "deep_avg_return": float(deep_trades["return_pct"].mean()) if not deep_trades.empty else float("nan"),
  81. "deep_weak_avg_return": float(weak_trades["return_pct"].mean()) if not weak_trades.empty else float("nan"),
  82. }
  83. return row, changed_events
  84. def main() -> None:
  85. base_dir = Path(__file__).resolve().parent
  86. indicator_df = _load_indicator_snapshot(base_dir)
  87. workbook_events = _load_workbook_events(base_dir)
  88. first_date = workbook_events["date"].min()
  89. last_date = workbook_events["date"].max()
  90. baseline_config = StrategyConfig()
  91. experiments = [
  92. ("baseline", baseline_config),
  93. (
  94. "confirm_weak_with_ql_1bar",
  95. baseline_config.with_updates(
  96. deep_oversold_confirm_weak_with_ql=True,
  97. deep_oversold_confirm_window_bars=1,
  98. ),
  99. ),
  100. (
  101. "confirm_weak_with_ql_2bar",
  102. baseline_config.with_updates(
  103. deep_oversold_confirm_weak_with_ql=True,
  104. deep_oversold_confirm_window_bars=2,
  105. ),
  106. ),
  107. (
  108. "confirm_weak_with_ql_3bar",
  109. baseline_config.with_updates(
  110. deep_oversold_confirm_weak_with_ql=True,
  111. deep_oversold_confirm_window_bars=3,
  112. ),
  113. ),
  114. ]
  115. rows: list[dict[str, object]] = []
  116. event_frames: list[pd.DataFrame] = []
  117. for label, config in experiments:
  118. row, events = _run_experiment(label, config, indicator_df, workbook_events, first_date, last_date)
  119. rows.append(row)
  120. event_frames.append(events)
  121. result_df = pd.DataFrame(rows)
  122. baseline = result_df[result_df["experiment"] == "baseline"].iloc[0]
  123. for col in [
  124. "trades",
  125. "win_rate",
  126. "avg_return",
  127. "median_return",
  128. "profit_factor",
  129. "real_buy_overlap",
  130. "real_sell_overlap",
  131. "deep_trade_count",
  132. "deep_weak_trade_count",
  133. "deep_confirmed_trade_count",
  134. "deep_avg_return",
  135. "deep_weak_avg_return",
  136. ]:
  137. result_df[f"delta_{col}"] = result_df[col] - baseline[col]
  138. event_df = pd.concat(event_frames, ignore_index=True)
  139. result_df.to_csv(base_dir / "dragon_deep_oversold_confirmation_experiments.csv", index=False, encoding="utf-8-sig")
  140. event_df.to_csv(base_dir / "dragon_deep_oversold_confirmation_event_changes.csv", index=False, encoding="utf-8-sig")
  141. lines = [
  142. "# Dragon Deep Oversold Confirmation Experiments",
  143. "",
  144. "- Goal: test an alpha-first branch for weak deep-oversold subtypes by replacing immediate entry with QL confirmation inside a short waiting window.",
  145. "- Scope: only `positive_b1_rebound` and `shallow_false_start`; default baseline behavior remains unchanged.",
  146. "",
  147. "## Summary",
  148. ]
  149. for _, row in result_df.iterrows():
  150. lines.append(
  151. f"- `{row['experiment']}`: trades `{int(row['trades'])}`, avg_return `{row['avg_return']:.2%}`, "
  152. f"profit_factor `{row['profit_factor']:.2f}`, real BUY `{int(row['real_buy_overlap'])}`, real SELL `{int(row['real_sell_overlap'])}`, "
  153. f"deep weak trades `{int(row['deep_weak_trade_count'])}`, confirmed deep trades `{int(row['deep_confirmed_trade_count'])}`"
  154. )
  155. lines.extend(["", "## Delta Vs Baseline"])
  156. for _, row in result_df[result_df["experiment"] != "baseline"].iterrows():
  157. lines.append(
  158. f"- `{row['experiment']}`: delta_avg_return `{row['delta_avg_return']:.2%}`, "
  159. f"delta_profit_factor `{row['delta_profit_factor']:.2f}`, "
  160. f"delta_deep_weak_avg_return `{row['delta_deep_weak_avg_return']:.2%}`, "
  161. f"real BUY `{int(row['real_buy_overlap'])}`, real SELL `{int(row['real_sell_overlap'])}`"
  162. )
  163. best = result_df[result_df["experiment"] != "baseline"].sort_values(
  164. ["avg_return", "profit_factor"], ascending=[False, False]
  165. ).head(1)
  166. if not best.empty:
  167. row = best.iloc[0]
  168. lines.extend(
  169. [
  170. "",
  171. "## Quant Judgment",
  172. f"- Best alpha-first branch in this pack: `{row['experiment']}` with avg_return `{row['avg_return']:.2%}` and profit_factor `{row['profit_factor']:.2f}`.",
  173. "- This pack is expected to lose workbook date alignment because entries are delayed by confirmation; treat it as a research branch, not a baseline patch.",
  174. "- If the best branch improves weak-subtype trade quality materially, the next step should be to audit shifted dates trade-by-trade rather than porting it directly into the baseline.",
  175. ]
  176. )
  177. (base_dir / "dragon_deep_oversold_confirmation_experiments.md").write_text(
  178. "\n".join(lines) + "\n",
  179. encoding="utf-8",
  180. )
  181. if __name__ == "__main__":
  182. main()