dragon_cost_stress_test.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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, profit_factor
  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 _holding_bucket(days: int) -> str:
  16. if days <= 5:
  17. return "00-05d"
  18. if days <= 10:
  19. return "06-10d"
  20. if days <= 20:
  21. return "11-20d"
  22. if days <= 40:
  23. return "21-40d"
  24. return "41d+"
  25. def _run_branch(indicator_df: pd.DataFrame, config) -> pd.DataFrame:
  26. engine = DragonRuleEngine(config=config)
  27. _, trades = engine.run(indicator_df)
  28. trades = trades[
  29. (trades["buy_date"] >= START_DATE)
  30. & (trades["buy_date"] <= END_DATE)
  31. & (trades["sell_date"] >= START_DATE)
  32. & (trades["sell_date"] <= END_DATE)
  33. ].copy()
  34. trades["holding_bucket"] = trades["holding_days"].astype(int).map(_holding_bucket)
  35. return trades
  36. def _apply_costs(trades: pd.DataFrame, per_side_bps: float) -> pd.DataFrame:
  37. out = trades.copy()
  38. cost = per_side_bps / 10000.0
  39. out["net_return_pct"] = (
  40. (out["sell_price"].astype(float) * (1.0 - cost)) / (out["buy_price"].astype(float) * (1.0 + cost))
  41. ) - 1.0
  42. return out
  43. def _summarize(branch: str, per_side_bps: float, trades: pd.DataFrame) -> dict[str, object]:
  44. returns = trades["net_return_pct"].astype(float)
  45. compounded = float((1.0 + returns).prod() - 1.0) if not trades.empty else float("nan")
  46. years = evaluation_years(START_DATE, END_DATE)
  47. cagr = float((1.0 + compounded) ** (1.0 / years) - 1.0) if not trades.empty else float("nan")
  48. return {
  49. "branch": branch,
  50. "per_side_bps": per_side_bps,
  51. "trades": int(len(trades)),
  52. "win_rate": float((returns > 0).mean()) if not trades.empty else float("nan"),
  53. "avg_return": float(returns.mean()) if not trades.empty else float("nan"),
  54. "median_return": float(returns.median()) if not trades.empty else float("nan"),
  55. "profit_factor": profit_factor(returns) if not trades.empty else float("nan"),
  56. "compounded_return": compounded,
  57. "cagr": cagr,
  58. "short_00_05d_avg_return": float(trades[trades["holding_bucket"] == "00-05d"]["net_return_pct"].mean()),
  59. "short_06_10d_avg_return": float(trades[trades["holding_bucket"] == "06-10d"]["net_return_pct"].mean()),
  60. }
  61. def main() -> None:
  62. base_dir = Path(__file__).resolve().parent
  63. indicator_df = _load_indicator_snapshot(base_dir)
  64. branches = {
  65. "workbook_preserving": workbook_preserving_config(),
  66. "alpha_first_selective_veto": alpha_first_selective_veto_config(),
  67. "alpha_first_glued_refined_hot_cap": alpha_first_glued_refined_hot_cap_config(),
  68. }
  69. cost_levels = [0.0, 5.0, 10.0, 20.0]
  70. gross_trades = {name: _run_branch(indicator_df, cfg) for name, cfg in branches.items()}
  71. rows: list[dict[str, object]] = []
  72. for branch, trades in gross_trades.items():
  73. for bps in cost_levels:
  74. net_trades = _apply_costs(trades, bps)
  75. rows.append(_summarize(branch, bps, net_trades))
  76. result = pd.DataFrame(rows)
  77. result.to_csv(base_dir / "dragon_cost_stress_test.csv", index=False, encoding="utf-8-sig")
  78. lines = [
  79. "# Dragon Cost Stress Test",
  80. "",
  81. f"- Evaluation window: `{START_DATE}` to `{END_DATE}`.",
  82. "- Cost convention: symmetric per-side cost on entry and exit.",
  83. "",
  84. "## Summary",
  85. ]
  86. for branch in branches:
  87. subset = result[result["branch"] == branch].sort_values("per_side_bps")
  88. lines.append(f"### {branch}")
  89. for _, row in subset.iterrows():
  90. lines.append(
  91. f"- `{int(row['per_side_bps'])} bps/side`: CAGR `{row['cagr']:.2%}`, compounded `{row['compounded_return']:.2%}`, "
  92. f"avg_return `{row['avg_return']:.2%}`, PF `{row['profit_factor']:.2f}`, "
  93. f"`00-05d` `{row['short_00_05d_avg_return']:.2%}`, `06-10d` `{row['short_06_10d_avg_return']:.2%}`"
  94. )
  95. lines.append("")
  96. zero = result[result["per_side_bps"] == 0.0].set_index("branch")
  97. twenty = result[result["per_side_bps"] == 20.0].set_index("branch")
  98. lines.extend(
  99. [
  100. "## Quant Judgment",
  101. f"- At `20 bps/side`, current alpha branch CAGR = `{twenty.loc['alpha_first_selective_veto', 'cagr']:.2%}`.",
  102. f"- At `20 bps/side`, refined candidate CAGR = `{twenty.loc['alpha_first_glued_refined_hot_cap', 'cagr']:.2%}`.",
  103. f"- CAGR delta refined minus current alpha at `0 bps/side` = `{(zero.loc['alpha_first_glued_refined_hot_cap', 'cagr'] - zero.loc['alpha_first_selective_veto', 'cagr']):.2%}`.",
  104. f"- CAGR delta refined minus current alpha at `20 bps/side` = `{(twenty.loc['alpha_first_glued_refined_hot_cap', 'cagr'] - twenty.loc['alpha_first_selective_veto', 'cagr']):.2%}`.",
  105. "- If the refined branch remains ahead under cost pressure, its edge is not just a no-cost backtest artifact.",
  106. ]
  107. )
  108. (base_dir / "dragon_cost_stress_test.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  109. if __name__ == "__main__":
  110. main()