dragon_equity_curve_review.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. from dragon_branch_configs import (
  5. alpha_first_glued_refined_hot_cap_config,
  6. alpha_first_selective_veto_config,
  7. workbook_preserving_config,
  8. )
  9. from dragon_shared import END_DATE, START_DATE, evaluation_years
  10. from dragon_strategy import DragonRuleEngine
  11. def _load_indicator_snapshot(base_dir: Path) -> pd.DataFrame:
  12. df = pd.read_csv(base_dir / "dragon_indicator_snapshot.csv", encoding="utf-8-sig")
  13. df["date"] = pd.to_datetime(df["date"])
  14. return df.set_index("date", drop=False)
  15. def _max_drawdown(equity: pd.Series) -> float:
  16. running_max = equity.cummax()
  17. drawdown = equity / running_max - 1.0
  18. return float(drawdown.min())
  19. def _max_drawdown_duration(equity: pd.Series) -> int:
  20. running_max = equity.cummax()
  21. underwater = equity < running_max
  22. best = cur = 0
  23. for flag in underwater.tolist():
  24. cur = cur + 1 if flag else 0
  25. best = max(best, cur)
  26. return int(best)
  27. def _run_branch(indicator_df: pd.DataFrame, config) -> pd.DataFrame:
  28. engine = DragonRuleEngine(config=config)
  29. _, trades = engine.run(indicator_df)
  30. trades = trades[
  31. (trades["buy_date"] >= START_DATE)
  32. & (trades["buy_date"] <= END_DATE)
  33. & (trades["sell_date"] >= START_DATE)
  34. & (trades["sell_date"] <= END_DATE)
  35. ].copy()
  36. trades["sell_dt"] = pd.to_datetime(trades["sell_date"])
  37. trades = trades.sort_values("sell_dt").reset_index(drop=True)
  38. trades["equity"] = (1.0 + trades["return_pct"].astype(float)).cumprod()
  39. trades["drawdown"] = trades["equity"] / trades["equity"].cummax() - 1.0
  40. trades["sell_year"] = trades["sell_dt"].dt.year.astype(int)
  41. trades["sell_month"] = trades["sell_dt"].dt.to_period("M").astype(str)
  42. return trades
  43. def _summarize(branch: str, trades: pd.DataFrame) -> dict[str, object]:
  44. years = evaluation_years(START_DATE, END_DATE)
  45. compounded = float(trades["equity"].iloc[-1] - 1.0) if not trades.empty else float("nan")
  46. cagr = float((1.0 + compounded) ** (1.0 / years) - 1.0) if not trades.empty else float("nan")
  47. max_dd = _max_drawdown(trades["equity"]) if not trades.empty else float("nan")
  48. calmar = float(cagr / abs(max_dd)) if trades.shape[0] and max_dd < 0 else float("inf")
  49. return {
  50. "branch": branch,
  51. "trades": int(len(trades)),
  52. "compounded_return": compounded,
  53. "cagr": cagr,
  54. "max_drawdown": max_dd,
  55. "drawdown_duration_trades": _max_drawdown_duration(trades["equity"]) if not trades.empty else 0,
  56. "calmar": calmar,
  57. "best_trade": float(trades["return_pct"].max()) if not trades.empty else float("nan"),
  58. "worst_trade": float(trades["return_pct"].min()) if not trades.empty else float("nan"),
  59. }
  60. def main() -> None:
  61. base_dir = Path(__file__).resolve().parent
  62. indicator_df = _load_indicator_snapshot(base_dir)
  63. branches = {
  64. "workbook_preserving": workbook_preserving_config(),
  65. "alpha_first_selective_veto": alpha_first_selective_veto_config(),
  66. "alpha_first_glued_refined_hot_cap": alpha_first_glued_refined_hot_cap_config(),
  67. }
  68. equity_frames: list[pd.DataFrame] = []
  69. summary_rows: list[dict[str, object]] = []
  70. monthly_rows: list[dict[str, object]] = []
  71. yearly_rows: list[dict[str, object]] = []
  72. for name, cfg in branches.items():
  73. trades = _run_branch(indicator_df, cfg)
  74. trades["branch"] = name
  75. equity_frames.append(trades[["branch", "buy_date", "sell_date", "return_pct", "equity", "drawdown", "sell_year", "sell_month"]].copy())
  76. summary_rows.append(_summarize(name, trades))
  77. month = trades.groupby("sell_month", dropna=False)["return_pct"].apply(lambda s: float((1.0 + s).prod() - 1.0)).reset_index()
  78. month["branch"] = name
  79. monthly_rows.append(month)
  80. year = trades.groupby("sell_year", dropna=False)["return_pct"].apply(lambda s: float((1.0 + s).prod() - 1.0)).reset_index()
  81. year["branch"] = name
  82. yearly_rows.append(year)
  83. equity = pd.concat(equity_frames, ignore_index=True)
  84. summary = pd.DataFrame(summary_rows)
  85. monthly = pd.concat(monthly_rows, ignore_index=True)
  86. yearly = pd.concat(yearly_rows, ignore_index=True)
  87. equity.to_csv(base_dir / "dragon_equity_curve_review.csv", index=False, encoding="utf-8-sig")
  88. summary.to_csv(base_dir / "dragon_drawdown_review.csv", index=False, encoding="utf-8-sig")
  89. monthly.to_csv(base_dir / "dragon_monthly_return_review.csv", index=False, encoding="utf-8-sig")
  90. yearly.to_csv(base_dir / "dragon_yearly_return_review.csv", index=False, encoding="utf-8-sig")
  91. lines = [
  92. "# Dragon Equity Curve Review",
  93. "",
  94. f"- Evaluation window: `{START_DATE}` to `{END_DATE}`.",
  95. "- Equity is compounded in trade sequence order.",
  96. "",
  97. "## Summary",
  98. ]
  99. for _, row in summary.iterrows():
  100. lines.append(
  101. f"- `{row['branch']}`: compounded `{row['compounded_return']:.2%}`, CAGR `{row['cagr']:.2%}`, "
  102. f"max_drawdown `{row['max_drawdown']:.2%}`, drawdown_duration `{int(row['drawdown_duration_trades'])}` trades, "
  103. f"Calmar `{row['calmar']:.2f}`"
  104. )
  105. lines.extend(["", "## Quant Judgment"])
  106. best = summary.sort_values(["cagr", "calmar"], ascending=[False, False]).iloc[0]
  107. lines.append(
  108. f"- Best growth profile in this pack: `{best['branch']}` with CAGR `{best['cagr']:.2%}`, max_drawdown `{best['max_drawdown']:.2%}`, Calmar `{best['calmar']:.2f}`."
  109. )
  110. lines.append("- Use this review to judge whether higher alpha is being purchased with unacceptable drawdown concentration.")
  111. (base_dir / "dragon_equity_curve_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  112. if __name__ == "__main__":
  113. main()