dragon_weak_family_experiments.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. import pandas as pd
  5. from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
  6. from dragon_execution_common import apply_execution_model, summary
  7. from dragon_rc1_golden_baseline import _load_indicator_snapshot
  8. from dragon_shared import END_DATE, START_DATE, format_num, format_pct
  9. from dragon_strategy import DragonRuleEngine
  10. from dragon_strategy_config import StrategyConfig
  11. def _bounded_trades(trades: pd.DataFrame) -> pd.DataFrame:
  12. return trades[
  13. (trades["buy_date"] >= START_DATE)
  14. & (trades["buy_date"] <= END_DATE)
  15. & (trades["sell_date"] >= START_DATE)
  16. & (trades["sell_date"] <= END_DATE)
  17. ].copy()
  18. def _trade_key(frame: pd.DataFrame) -> pd.Series:
  19. return (
  20. frame["buy_date"].astype(str)
  21. + "|"
  22. + frame["sell_date"].astype(str)
  23. + "|"
  24. + frame["buy_reason"].astype(str)
  25. + "|"
  26. + frame["sell_reason"].astype(str)
  27. )
  28. def _trade_diff(baseline: pd.DataFrame, candidate: pd.DataFrame, label: str) -> pd.DataFrame:
  29. base = baseline.copy()
  30. cand = candidate.copy()
  31. base["trade_key"] = _trade_key(base)
  32. cand["trade_key"] = _trade_key(cand)
  33. removed = base.loc[~base["trade_key"].isin(cand["trade_key"])].copy()
  34. removed["diff_type"] = "REMOVED"
  35. added = cand.loc[~cand["trade_key"].isin(base["trade_key"])].copy()
  36. added["diff_type"] = "ADDED"
  37. diff = pd.concat([removed, added], ignore_index=True)
  38. diff.insert(0, "experiment", label)
  39. return diff
  40. def _add_execution_prices(trades: pd.DataFrame, indicators: pd.DataFrame) -> pd.DataFrame:
  41. result = trades.copy()
  42. ind = indicators.copy().sort_values("date").reset_index(drop=True)
  43. ind["date"] = pd.to_datetime(ind["date"])
  44. open_col = "open" if "open" in ind.columns else "close"
  45. close_col = "close"
  46. next_by_date: dict[pd.Timestamp, pd.Series] = {}
  47. for idx in range(len(ind) - 1):
  48. next_by_date[pd.Timestamp(ind.iloc[idx]["date"])] = ind.iloc[idx + 1]
  49. next_open_entry: list[float] = []
  50. next_open_exit: list[float] = []
  51. next_close_entry: list[float] = []
  52. next_close_exit: list[float] = []
  53. same_close_entry: list[float] = []
  54. same_close_exit: list[float] = []
  55. for _, row in result.iterrows():
  56. buy_date = pd.Timestamp(row["buy_date"])
  57. sell_date = pd.Timestamp(row["sell_date"])
  58. buy_next = next_by_date.get(buy_date)
  59. sell_next = next_by_date.get(sell_date)
  60. next_open_entry.append(float("nan") if buy_next is None else float(buy_next[open_col]))
  61. next_open_exit.append(float("nan") if sell_next is None else float(sell_next[open_col]))
  62. next_close_entry.append(float("nan") if buy_next is None else float(buy_next[close_col]))
  63. next_close_exit.append(float("nan") if sell_next is None else float(sell_next[close_col]))
  64. same_close_entry.append(float(row["buy_price"]))
  65. same_close_exit.append(float(row["sell_price"]))
  66. result["exec_same_close_entry"] = same_close_entry
  67. result["exec_same_close_exit"] = same_close_exit
  68. result["exec_next_open_entry"] = next_open_entry
  69. result["exec_next_open_exit"] = next_open_exit
  70. result["exec_next_close_entry"] = next_close_entry
  71. result["exec_next_close_exit"] = next_close_exit
  72. result["entry_family"] = result["buy_reason_family"].astype(str)
  73. return result
  74. def _run_variant(indexed: pd.DataFrame, config: StrategyConfig) -> pd.DataFrame:
  75. _, trades = DragonRuleEngine(config=config).run(indexed)
  76. return _bounded_trades(trades)
  77. def main() -> None:
  78. base_dir = Path(__file__).resolve().parent
  79. indexed, source = _load_indicator_snapshot(base_dir)
  80. indicators = indexed.reset_index(drop=False).rename(columns={"index": "date"})
  81. baseline_cfg = alpha_first_glued_refined_hot_cap_config()
  82. deep_oversold_cfg = baseline_cfg.with_updates(
  83. deep_oversold_confirm_weak_with_ql=True,
  84. deep_oversold_block_shallow_false_start_without_ql=True,
  85. deep_oversold_selective_positive_b1_c1_max=15.3,
  86. )
  87. predictive_bridge_only_cfg = baseline_cfg.with_updates(
  88. disabled_rules=frozenset(set(baseline_cfg.disabled_rules) | {"predictive_b1_break_exit", "predictive_error_reentry_buy"}),
  89. )
  90. variants: dict[str, StrategyConfig] = {
  91. "rc1_baseline": baseline_cfg,
  92. "deep_oversold_confirmation_v2": deep_oversold_cfg,
  93. "predictive_bridge_only": predictive_bridge_only_cfg,
  94. }
  95. trades_by_variant = {name: _run_variant(indexed, cfg) for name, cfg in variants.items()}
  96. summary_rows: list[dict[str, object]] = []
  97. for name, trades in trades_by_variant.items():
  98. returns = trades["return_pct"].astype(float)
  99. summary_rows.append(
  100. {
  101. "experiment": name,
  102. "trades": int(len(trades)),
  103. "win_rate": float((returns > 0).mean()) if not trades.empty else float("nan"),
  104. "avg_return": float(returns.mean()) if not trades.empty else float("nan"),
  105. "median_return": float(returns.median()) if not trades.empty else float("nan"),
  106. "compounded_return": float((1.0 + returns).prod() - 1.0) if not trades.empty else float("nan"),
  107. }
  108. )
  109. summary_df = pd.DataFrame(summary_rows).sort_values("experiment").reset_index(drop=True)
  110. summary_df.to_csv(base_dir / "dragon_weak_family_experiment_summary.csv", index=False, encoding="utf-8-sig")
  111. baseline_trades = trades_by_variant["rc1_baseline"]
  112. diff_rows = []
  113. for name, trades in trades_by_variant.items():
  114. if name == "rc1_baseline":
  115. continue
  116. diff_rows.append(_trade_diff(baseline_trades, trades, label=name))
  117. diff_df = pd.concat(diff_rows, ignore_index=True) if diff_rows else pd.DataFrame()
  118. if not diff_df.empty:
  119. diff_df.to_csv(base_dir / "dragon_weak_family_trade_diff.csv", index=False, encoding="utf-8-sig")
  120. stress_rows: list[dict[str, object]] = []
  121. for name, trades in trades_by_variant.items():
  122. enriched = _add_execution_prices(trades, indicators)
  123. for model in ["same_close", "next_open", "next_close"]:
  124. model_trades = apply_execution_model(enriched, model, 0.0)
  125. stress_rows.append(summary(name, model_trades))
  126. if model == "next_open":
  127. stressed = apply_execution_model(enriched, model, 20.0)
  128. stress_rows.append(summary(name, stressed))
  129. stress_df = pd.DataFrame(stress_rows).rename(columns={"branch": "experiment"})
  130. stress_df = stress_df.sort_values(["experiment", "execution_model", "cost_bps_side"]).reset_index(drop=True)
  131. stress_df.to_csv(base_dir / "dragon_weak_family_execution_stress.csv", index=False, encoding="utf-8-sig")
  132. (base_dir / "dragon_weak_family_experiment_config_snapshot.json").write_text(
  133. json.dumps(
  134. {
  135. "indicator_source": source,
  136. "evaluation_window": {"start": START_DATE, "end": END_DATE},
  137. "variants": {name: cfg.__dict__ for name, cfg in variants.items()},
  138. },
  139. ensure_ascii=False,
  140. indent=2,
  141. default=list,
  142. ),
  143. encoding="utf-8",
  144. )
  145. lines = [
  146. "# Dragon Weak Family Experiment Review",
  147. "",
  148. f"- indicator source: `{source}`",
  149. f"- window: `{START_DATE} -> {END_DATE}`",
  150. "",
  151. "## Summary",
  152. ]
  153. for _, row in summary_df.iterrows():
  154. lines.append(
  155. "- "
  156. f"{row['experiment']}: "
  157. f"trades `{int(row['trades'])}`, "
  158. f"win_rate `{format_pct(float(row['win_rate']))}`, "
  159. f"avg_return `{format_pct(float(row['avg_return']))}`, "
  160. f"compounded `{format_pct(float(row['compounded_return']))}`"
  161. )
  162. if not diff_df.empty:
  163. lines.extend(["", "## Added/Removed Trades vs RC1"])
  164. for experiment, group in diff_df.groupby("experiment", dropna=False):
  165. added = int((group["diff_type"] == "ADDED").sum())
  166. removed = int((group["diff_type"] == "REMOVED").sum())
  167. avg_added = float(group.loc[group["diff_type"] == "ADDED", "return_pct"].mean()) if added > 0 else float("nan")
  168. avg_removed = float(group.loc[group["diff_type"] == "REMOVED", "return_pct"].mean()) if removed > 0 else float("nan")
  169. lines.append(
  170. "- "
  171. f"{experiment}: added `{added}` (avg `{format_pct(avg_added)}`), "
  172. f"removed `{removed}` (avg `{format_pct(avg_removed)}`)"
  173. )
  174. lines.extend(["", "## Execution Stress (next_open + 20 bps/side)"])
  175. stress_20 = stress_df[(stress_df["execution_model"] == "next_open") & (stress_df["cost_bps_side"] == 20.0)]
  176. for _, row in stress_20.iterrows():
  177. lines.append(
  178. "- "
  179. f"{row['experiment']}: "
  180. f"avg_return `{format_pct(float(row['avg_return']))}`, "
  181. f"PF `{format_num(float(row['profit_factor']))}`, "
  182. f"max_dd `{format_pct(float(row['max_drawdown']))}`"
  183. )
  184. (base_dir / "dragon_weak_family_experiment.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  185. if __name__ == "__main__":
  186. main()