dragon_predictive_break_audit.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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. def _profit_factor(series: pd.Series) -> float:
  8. gross_profit = series[series > 0].sum()
  9. gross_loss = -series[series < 0].sum()
  10. if gross_loss == 0:
  11. return float("inf") if gross_profit > 0 else 0.0
  12. return float(gross_profit / gross_loss)
  13. def main() -> None:
  14. base_dir = Path(__file__).resolve().parent
  15. engine = DragonIndicatorEngine(DragonIndicatorConfig(start_date="2015-01-01", end_date="2026-01-31"))
  16. indicator_df = engine.compute(engine.fetch_daily_data())
  17. baseline_events, baseline_trades = DragonRuleEngine(config=StrategyConfig()).run(indicator_df)
  18. disabled_events, disabled_trades = DragonRuleEngine(
  19. config=StrategyConfig(disabled_rules=frozenset({"predictive_b1_break_exit"}))
  20. ).run(indicator_df)
  21. predictive = baseline_trades[baseline_trades["sell_reason"] == "predictive_b1_break_exit"].copy()
  22. if predictive.empty:
  23. raise RuntimeError("No predictive_b1_break_exit trades found in baseline.")
  24. predictive["buy_dt"] = pd.to_datetime(predictive["buy_date"])
  25. predictive["sell_dt"] = pd.to_datetime(predictive["sell_date"])
  26. indicator_df["dt"] = indicator_df.index
  27. indicator_df = indicator_df.reset_index(drop=True)
  28. pos_lookup = {dt.date().isoformat(): idx for idx, dt in enumerate(pd.to_datetime(indicator_df["dt"]))}
  29. rows: list[dict[str, object]] = []
  30. for _, trade in predictive.iterrows():
  31. sell_idx = pos_lookup[trade["sell_date"]]
  32. entry_price = float(trade["buy_price"])
  33. exit_price = float(trade["sell_price"])
  34. future_3 = indicator_df.iloc[sell_idx + 1 : sell_idx + 4]
  35. future_5 = indicator_df.iloc[sell_idx + 1 : sell_idx + 6]
  36. future_10 = indicator_df.iloc[sell_idx + 1 : sell_idx + 11]
  37. alt_trade = disabled_trades[
  38. (disabled_trades["buy_date"] == trade["buy_date"]) & (disabled_trades["buy_reason"] == trade["buy_reason"])
  39. ].copy()
  40. alt_sell_date = alt_trade["sell_date"].iloc[0] if not alt_trade.empty else ""
  41. alt_sell_reason = alt_trade["sell_reason"].iloc[0] if not alt_trade.empty else ""
  42. alt_return = float(alt_trade["return_pct"].iloc[0]) if not alt_trade.empty else float("nan")
  43. resumed_trade = baseline_trades[baseline_trades["buy_date"] > trade["sell_date"]].head(1).copy()
  44. resumed_buy_date = resumed_trade["buy_date"].iloc[0] if not resumed_trade.empty else ""
  45. resumed_buy_reason = resumed_trade["buy_reason"].iloc[0] if not resumed_trade.empty else ""
  46. resumed_sell_date = resumed_trade["sell_date"].iloc[0] if not resumed_trade.empty else ""
  47. resumed_return = float(resumed_trade["return_pct"].iloc[0]) if not resumed_trade.empty else float("nan")
  48. combined_return = (1.0 + float(trade["return_pct"])) * (1.0 + resumed_return) - 1.0 if resumed_trade.shape[0] == 1 else float("nan")
  49. rows.append(
  50. {
  51. "buy_date": trade["buy_date"],
  52. "buy_reason": trade["buy_reason"],
  53. "predictive_sell_date": trade["sell_date"],
  54. "predictive_return_pct": float(trade["return_pct"]),
  55. "predictive_holding_days": int(trade["holding_days"]),
  56. "exit_a1": float(
  57. baseline_events[
  58. (baseline_events["date"] == trade["sell_date"])
  59. & (baseline_events["layer"] == "real_trade")
  60. & (baseline_events["side"] == "SELL")
  61. ]["a1"].iloc[0]
  62. ),
  63. "exit_b1": float(
  64. baseline_events[
  65. (baseline_events["date"] == trade["sell_date"])
  66. & (baseline_events["layer"] == "real_trade")
  67. & (baseline_events["side"] == "SELL")
  68. ]["b1"].iloc[0]
  69. ),
  70. "exit_c1": float(
  71. baseline_events[
  72. (baseline_events["date"] == trade["sell_date"])
  73. & (baseline_events["layer"] == "real_trade")
  74. & (baseline_events["side"] == "SELL")
  75. ]["c1"].iloc[0]
  76. ),
  77. "future_3d_low_pct": float(future_3["low"].min()) / exit_price - 1.0 if not future_3.empty else float("nan"),
  78. "future_3d_high_pct": float(future_3["high"].max()) / exit_price - 1.0 if not future_3.empty else float("nan"),
  79. "future_5d_low_pct": float(future_5["low"].min()) / exit_price - 1.0 if not future_5.empty else float("nan"),
  80. "future_5d_high_pct": float(future_5["high"].max()) / exit_price - 1.0 if not future_5.empty else float("nan"),
  81. "future_10d_low_pct": float(future_10["low"].min()) / exit_price - 1.0 if not future_10.empty else float("nan"),
  82. "future_10d_high_pct": float(future_10["high"].max()) / exit_price - 1.0 if not future_10.empty else float("nan"),
  83. "disabled_alt_sell_date": alt_sell_date,
  84. "disabled_alt_sell_reason": alt_sell_reason,
  85. "disabled_alt_return_pct": alt_return,
  86. "resumed_buy_date": resumed_buy_date,
  87. "resumed_buy_reason": resumed_buy_reason,
  88. "resumed_sell_date": resumed_sell_date,
  89. "resumed_return_pct": resumed_return,
  90. "combined_split_return_pct": combined_return,
  91. }
  92. )
  93. audit_df = pd.DataFrame(rows)
  94. audit_df.to_csv(base_dir / "dragon_predictive_break_audit.csv", index=False, encoding="utf-8-sig")
  95. lines = [
  96. "# Dragon Predictive Break Review",
  97. "",
  98. f"- predictive exit trades in baseline: `{len(audit_df)}`",
  99. f"- predictive exit avg return: `{audit_df['predictive_return_pct'].mean():.2%}`",
  100. f"- disabled-rule alt avg return: `{audit_df['disabled_alt_return_pct'].mean():.2%}`",
  101. f"- split-chain combined avg return: `{audit_df['combined_split_return_pct'].mean():.2%}`",
  102. "",
  103. "## Trade Cards",
  104. ]
  105. for _, row in audit_df.iterrows():
  106. lines.append(
  107. f"- `{row['buy_date']} -> {row['predictive_sell_date']}` `{row['buy_reason']}` | "
  108. f"exit a1 `{row['exit_a1']:.4f}` b1 `{row['exit_b1']:.4f}` c1 `{row['exit_c1']:.2f}` | "
  109. f"3d low `{row['future_3d_low_pct']:.2%}` / 5d high `{row['future_5d_high_pct']:.2%}`"
  110. )
  111. lines.append(
  112. f" disabled path -> `{row['disabled_alt_sell_date']}` `{row['disabled_alt_sell_reason']}` `{row['disabled_alt_return_pct']:.2%}`"
  113. )
  114. lines.append(
  115. f" split path reentry -> `{row['resumed_buy_date']}` `{row['resumed_buy_reason']}` to `{row['resumed_sell_date']}` `{row['resumed_return_pct']:.2%}`; combined `{row['combined_split_return_pct']:.2%}`"
  116. )
  117. lines.extend(
  118. [
  119. "",
  120. "## Quant Judgment",
  121. "- The remaining predictive break is a bridge-style exit, not a generic stop-loss bucket.",
  122. "- Disabling it improves the single uninterrupted trade return, but destroys the workbook-aligned split chain.",
  123. "- Under the current reconstruction objective, this rule should be frozen unless the user explicitly accepts lower workbook alignment.",
  124. ]
  125. )
  126. (base_dir / "dragon_predictive_break_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  127. if __name__ == "__main__":
  128. main()