dragon_deep_oversold_experiments.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. from dragon_indicators import DragonIndicatorConfig, DragonIndicatorEngine
  5. from dragon_strategy import DragonRuleEngine
  6. from dragon_strategy_config import StrategyConfig
  7. from dragon_workbook import DragonWorkbook
  8. def _find_workbook(base_dir: Path) -> Path:
  9. matches = sorted(base_dir.glob("*.xlsx"))
  10. if not matches:
  11. raise FileNotFoundError(f"No workbook found in {base_dir}")
  12. return matches[0]
  13. def _load_workbook_events(workbook_path: Path) -> pd.DataFrame:
  14. workbook = DragonWorkbook(workbook_path)
  15. return pd.DataFrame(
  16. [{"date": e.date.isoformat(), "side": e.side, "layer": e.layer} for e in workbook.split_layers()]
  17. )
  18. def _overlap(workbook_events: pd.DataFrame, strategy_events: pd.DataFrame, side: str, layer: str) -> tuple[int, int, int]:
  19. wb = set(workbook_events[(workbook_events["side"] == side) & (workbook_events["layer"] == layer)]["date"])
  20. st = set(strategy_events[(strategy_events["side"] == side) & (strategy_events["layer"] == layer)]["date"])
  21. hit = wb & st
  22. return len(hit), len(wb - st), len(st - wb)
  23. def _profit_factor(series: pd.Series) -> float:
  24. gross_profit = series[series > 0].sum()
  25. gross_loss = -series[series < 0].sum()
  26. if gross_loss == 0:
  27. return float("inf") if gross_profit > 0 else 0.0
  28. return float(gross_profit / gross_loss)
  29. def _run(label: str, config: StrategyConfig, workbook_events: pd.DataFrame, indicator_df: pd.DataFrame, first_date: str, last_date: str) -> dict[str, object]:
  30. events, trades = DragonRuleEngine(config=config).run(indicator_df)
  31. events = events[(events["date"] >= first_date) & (events["date"] <= last_date)].copy()
  32. trades = trades[
  33. (trades["buy_date"] >= first_date)
  34. & (trades["buy_date"] <= last_date)
  35. & (trades["sell_date"] >= first_date)
  36. & (trades["sell_date"] <= last_date)
  37. ].copy()
  38. buy_overlap, buy_missing, buy_extra = _overlap(workbook_events, events, "BUY", "real_trade")
  39. sell_overlap, sell_missing, sell_extra = _overlap(workbook_events, events, "SELL", "real_trade")
  40. deep_trades = trades[trades["buy_reason"].astype(str).str.startswith("deep_oversold_rebound_buy")]
  41. return {
  42. "experiment": label,
  43. "trades": int(len(trades)),
  44. "avg_return": float(trades["return_pct"].mean()),
  45. "profit_factor": _profit_factor(trades["return_pct"]),
  46. "real_buy_overlap": int(buy_overlap),
  47. "real_buy_missing": int(buy_missing),
  48. "real_buy_extra": int(buy_extra),
  49. "real_sell_overlap": int(sell_overlap),
  50. "real_sell_missing": int(sell_missing),
  51. "real_sell_extra": int(sell_extra),
  52. "deep_oversold_trade_count": int(len(deep_trades)),
  53. "deep_oversold_avg_return": float(deep_trades["return_pct"].mean()) if not deep_trades.empty else float("nan"),
  54. }
  55. def main() -> None:
  56. base_dir = Path(__file__).resolve().parent
  57. workbook_events = _load_workbook_events(_find_workbook(base_dir))
  58. first_date = pd.to_datetime(workbook_events["date"]).min().date().isoformat()
  59. last_date = pd.to_datetime(workbook_events["date"]).max().date().isoformat()
  60. engine = DragonIndicatorEngine(DragonIndicatorConfig(start_date="2015-01-01", end_date="2026-01-31"))
  61. indicator_df = engine.compute(engine.fetch_daily_data())
  62. baseline = StrategyConfig()
  63. experiments = [
  64. ("baseline", baseline),
  65. (
  66. "block_positive_b1_rebound",
  67. baseline.with_updates(deep_oversold_block_positive_b1_rebound=True),
  68. ),
  69. (
  70. "block_shallow_false_start_without_ql",
  71. baseline.with_updates(deep_oversold_block_shallow_false_start_without_ql=True),
  72. ),
  73. (
  74. "block_both_remaining_weak_subtypes",
  75. baseline.with_updates(
  76. deep_oversold_block_positive_b1_rebound=True,
  77. deep_oversold_block_shallow_false_start_without_ql=True,
  78. ),
  79. ),
  80. ]
  81. rows = [_run(label, cfg, workbook_events, indicator_df, first_date, last_date) for label, cfg in experiments]
  82. df = pd.DataFrame(rows)
  83. base_row = df[df["experiment"] == "baseline"].iloc[0]
  84. for col in ["trades", "avg_return", "profit_factor", "real_buy_overlap", "real_sell_overlap", "deep_oversold_trade_count", "deep_oversold_avg_return"]:
  85. df[f"delta_{col}"] = df[col] - base_row[col]
  86. df.to_csv(base_dir / "dragon_deep_oversold_experiments.csv", index=False, encoding="utf-8-sig")
  87. lines = [
  88. "# Dragon Deep Oversold Experiments",
  89. "",
  90. f"- Baseline deep-oversold trade count: `{int(base_row['deep_oversold_trade_count'])}`",
  91. f"- Baseline real BUY / SELL overlap: `{int(base_row['real_buy_overlap'])}` / `{int(base_row['real_sell_overlap'])}`",
  92. "",
  93. "## Experiment Summary",
  94. ]
  95. for _, row in df.iterrows():
  96. lines.append(
  97. f"- `{row['experiment']}`: deep trades `{int(row['deep_oversold_trade_count'])}`, "
  98. f"delta_avg_return `{row['delta_avg_return']:.2%}`, real BUY `{int(row['real_buy_overlap'])}`, real SELL `{int(row['real_sell_overlap'])}`"
  99. )
  100. (base_dir / "dragon_deep_oversold_experiments.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  101. if __name__ == "__main__":
  102. main()