dragon_strategy_overview.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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, format_num as _format_num, format_pct as _format_pct, 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 _event_overlap(events: pd.DataFrame, workbook: pd.DataFrame, side: str) -> tuple[int, int]:
  16. wb = set(workbook[(workbook["side"] == side) & (workbook["layer"] == "real_trade")]["date"])
  17. st = set(events[(events["side"] == side) & (events["layer"] == "real_trade")]["date"])
  18. return len(wb & st), len(st)
  19. def _run_branch(name: str, config, indicator_df: pd.DataFrame, workbook_events: pd.DataFrame) -> dict[str, object]:
  20. engine = DragonRuleEngine(config=config)
  21. events, trades = engine.run(indicator_df)
  22. events = events[(events["date"] >= START_DATE) & (events["date"] <= END_DATE)].copy()
  23. trades = trades[
  24. (trades["buy_date"] >= START_DATE)
  25. & (trades["buy_date"] <= END_DATE)
  26. & (trades["sell_date"] >= START_DATE)
  27. & (trades["sell_date"] <= END_DATE)
  28. ].copy()
  29. returns = trades["return_pct"].astype(float)
  30. compounded_return = float((1.0 + returns).prod() - 1.0) if not trades.empty else float("nan")
  31. years = evaluation_years(START_DATE, END_DATE)
  32. cagr = float((1.0 + compounded_return) ** (1.0 / years) - 1.0) if pd.notna(compounded_return) else float("nan")
  33. buy_overlap, buy_strategy_total = _event_overlap(events, workbook_events, "BUY")
  34. sell_overlap, sell_strategy_total = _event_overlap(events, workbook_events, "SELL")
  35. short_00_05d = float(trades[trades["holding_days"] <= 5]["return_pct"].mean())
  36. short_06_10d = float(trades[(trades["holding_days"] > 5) & (trades["holding_days"] <= 10)]["return_pct"].mean())
  37. return {
  38. "branch": name,
  39. "trades": int(len(trades)),
  40. "win_rate": float((returns > 0).mean()) if not trades.empty else float("nan"),
  41. "avg_return": float(returns.mean()) if not trades.empty else float("nan"),
  42. "median_return": float(returns.median()) if not trades.empty else float("nan"),
  43. "profit_factor": profit_factor(returns) if not trades.empty else float("nan"),
  44. "compounded_return": compounded_return,
  45. "cagr": cagr,
  46. "real_buy_overlap": int(buy_overlap),
  47. "real_sell_overlap": int(sell_overlap),
  48. "real_buy_strategy_total": int(buy_strategy_total),
  49. "real_sell_strategy_total": int(sell_strategy_total),
  50. "short_00_05d_avg_return": short_00_05d,
  51. "short_06_10d_avg_return": short_06_10d,
  52. }
  53. def main() -> None:
  54. base_dir = Path(__file__).resolve().parent
  55. indicator_df = _load_indicator_snapshot(base_dir)
  56. workbook_events = pd.read_csv(base_dir / "true_trade_events.csv", encoding="utf-8-sig")
  57. rows = [
  58. _run_branch("workbook_preserving", workbook_preserving_config(), indicator_df, workbook_events),
  59. _run_branch("alpha_first_selective_veto", alpha_first_selective_veto_config(), indicator_df, workbook_events),
  60. _run_branch("alpha_first_glued_refined_hot_cap", alpha_first_glued_refined_hot_cap_config(), indicator_df, workbook_events),
  61. ]
  62. df = pd.DataFrame(rows)
  63. df.to_csv(base_dir / "dragon_strategy_overview.csv", index=False, encoding="utf-8-sig")
  64. role_map = {
  65. "workbook_preserving": "official reconstruction baseline",
  66. "alpha_first_selective_veto": "current formal alpha branch",
  67. "alpha_first_glued_refined_hot_cap": "leading high-alpha candidate",
  68. }
  69. style_map = {
  70. "workbook_preserving": "most like workbook",
  71. "alpha_first_selective_veto": "balanced",
  72. "alpha_first_glued_refined_hot_cap": "most aggressive",
  73. }
  74. suit_map = {
  75. "workbook_preserving": "适合优先保留原表结构",
  76. "alpha_first_selective_veto": "适合兼顾原表和收益质量",
  77. "alpha_first_glued_refined_hot_cap": "适合更偏实战 alpha",
  78. }
  79. lines = [
  80. "# Dragon Strategy Overview",
  81. "",
  82. f"- Evaluation window: `{START_DATE}` to `{END_DATE}`.",
  83. "- Return metrics use compounded trade returns without extra slippage/fee adjustments.",
  84. "",
  85. "## Headline Table",
  86. ]
  87. for _, row in df.iterrows():
  88. branch = row["branch"]
  89. lines.extend(
  90. [
  91. f"### {branch}",
  92. f"- Role: `{role_map[branch]}`",
  93. f"- Style: `{style_map[branch]}`",
  94. f"- Trades: `{int(row['trades'])}`",
  95. f"- Win rate: `{_format_pct(float(row['win_rate']))}`",
  96. f"- Avg / Median trade: `{_format_pct(float(row['avg_return']))}` / `{_format_pct(float(row['median_return']))}`",
  97. f"- Profit factor: `{_format_num(float(row['profit_factor']))}`",
  98. f"- Compounded return: `{_format_pct(float(row['compounded_return']))}`",
  99. f"- CAGR: `{_format_pct(float(row['cagr']))}`",
  100. f"- Real BUY / SELL overlap: `{int(row['real_buy_overlap'])}/{int(row['real_sell_overlap'])}`",
  101. f"- Short `00-05d` / `06-10d`: `{_format_pct(float(row['short_00_05d_avg_return']))}` / `{_format_pct(float(row['short_06_10d_avg_return']))}`",
  102. f"- Suitable for: {suit_map[branch]}",
  103. "",
  104. ]
  105. )
  106. workbook = df[df["branch"] == "workbook_preserving"].iloc[0]
  107. alpha = df[df["branch"] == "alpha_first_selective_veto"].iloc[0]
  108. refined = df[df["branch"] == "alpha_first_glued_refined_hot_cap"].iloc[0]
  109. lines.extend(
  110. [
  111. "## Quick Read",
  112. f"- If you want the version most like the workbook, use `workbook_preserving`: CAGR `{_format_pct(float(workbook['cagr']))}`, overlap `{int(workbook['real_buy_overlap'])}/{int(workbook['real_sell_overlap'])}`.",
  113. f"- If you want the balanced version, use `alpha_first_selective_veto`: CAGR `{_format_pct(float(alpha['cagr']))}`, profit factor `{_format_num(float(alpha['profit_factor']))}`, overlap `{int(alpha['real_buy_overlap'])}/{int(alpha['real_sell_overlap'])}`.",
  114. f"- If you want the strongest alpha candidate, use `alpha_first_glued_refined_hot_cap`: CAGR `{_format_pct(float(refined['cagr']))}`, profit factor `{_format_num(float(refined['profit_factor']))}`, overlap `{int(refined['real_buy_overlap'])}/{int(refined['real_sell_overlap'])}`.",
  115. "",
  116. "## Quant Take",
  117. f"- `alpha_first_selective_veto` vs workbook: CAGR `+{(float(alpha['cagr']) - float(workbook['cagr'])):.2%}`, profit factor `+{(float(alpha['profit_factor']) - float(workbook['profit_factor'])):.2f}`, BUY/SELL overlap delta `{int(alpha['real_buy_overlap'] - workbook['real_buy_overlap'])}/{int(alpha['real_sell_overlap'] - workbook['real_sell_overlap'])}`.",
  118. f"- `alpha_first_glued_refined_hot_cap` vs current alpha: CAGR `+{(float(refined['cagr']) - float(alpha['cagr'])):.2%}`, profit factor `+{(float(refined['profit_factor']) - float(alpha['profit_factor'])):.2f}`, BUY/SELL overlap delta `{int(refined['real_buy_overlap'] - alpha['real_buy_overlap'])}/{int(refined['real_sell_overlap'] - alpha['real_sell_overlap'])}`.",
  119. "- Operationally, the refined branch is the best-performing candidate, but the current formal alpha branch remains the governance default because it gives up fewer workbook-aligned dates.",
  120. ]
  121. )
  122. (base_dir / "dragon_strategy_overview.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  123. if __name__ == "__main__":
  124. main()