dragon_trade_path_trace.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. BUY_REASON_STATE = {
  5. "deep_oversold_rebound_buy": "low_oversold_regime",
  6. "oversold_recovery_buy": "low_oversold_regime",
  7. "oversold_reversal_after_ql_buy": "rebound_after_sell_regime",
  8. "post_sell_rebound_buy": "rebound_after_sell_regime",
  9. "post_washout_kdj_reentry_buy": "rebound_after_sell_regime",
  10. "predictive_error_reentry_buy": "rebound_after_sell_regime",
  11. "hot_exit_reentry_buy": "rebound_after_sell_regime",
  12. "early_crash_probe_buy": "crash_probe_regime",
  13. "dual_gold_resonance_buy": "low_oversold_regime",
  14. "glued_buy": "mid_regime",
  15. "non_glued_positive_expansion_buy": "high_regime",
  16. }
  17. BUY_REASON_QUALIFICATION = {
  18. "deep_oversold_rebound_buy": "oversold_reversal_entry",
  19. "oversold_recovery_buy": "oversold_reversal_entry",
  20. "oversold_reversal_after_ql_buy": "oversold_reversal_entry",
  21. "post_sell_rebound_buy": "rebound_reentry",
  22. "post_washout_kdj_reentry_buy": "workbook_special_restart",
  23. "predictive_error_reentry_buy": "bridge_reentry",
  24. "hot_exit_reentry_buy": "bridge_reentry",
  25. "early_crash_probe_buy": "crash_probe_entry",
  26. "dual_gold_resonance_buy": "dual_gold_entry",
  27. "glued_buy": "glued_base_entry",
  28. "non_glued_positive_expansion_buy": "momentum_expansion_entry",
  29. }
  30. SELL_REASON_MANAGEMENT = {
  31. "crash_protection_exit": "predictive_risk_exit",
  32. "predictive_b1_break_exit": "predictive_risk_exit",
  33. "prewarning_reduction_exit": "prewarning_exit",
  34. "high_regime_momentum_break": "prewarning_exit",
  35. "high_regime_confirmed_exit:kdj_sell": "confirmed_trend_exit",
  36. "ql_high_zone_take_profit": "high_regime_take_profit",
  37. "ql_mid_zone_take_profit": "high_regime_take_profit",
  38. "medium_hot_take_profit": "high_regime_take_profit",
  39. "high_zone_post_ql_fade_exit": "ql_followthrough_exit",
  40. "post_ql_decay_exit": "ql_followthrough_exit",
  41. "post_dual_sell_decay_exit": "ql_followthrough_exit",
  42. "knife_take_profit_1": "first_take_profit",
  43. "knife_take_profit_2_glued": "first_take_profit",
  44. "knife_take_profit_2_wait_ql_s": "first_take_profit",
  45. "early_positive_take_profit": "first_take_profit",
  46. "oversold_rebound_take_profit": "first_take_profit",
  47. "glued_exit:kdj_sell": "confirmed_trend_exit",
  48. "small_positive_a1_declining:ql_sell": "confirmed_trend_exit",
  49. "negative_a1_no_b1_recovery:kdj_sell": "negative_a1_exit",
  50. "negative_a1_no_b1_recovery:ql_sell": "negative_a1_exit",
  51. "negative_a1_b1_not_strong:kdj_sell": "negative_a1_exit",
  52. "low_zone_dual_gold_exit:kdj_sell": "negative_a1_exit",
  53. "hard_exit:kdj_sell": "hard_risk_exit",
  54. "hard_exit:ql_sell": "hard_risk_exit",
  55. "early_failed_rebound_exit": "predictive_risk_exit",
  56. }
  57. def _infer_state_layer(buy_reason: str, buy_c1: float) -> str:
  58. normalized_reason = buy_reason.split(":", 1)[0]
  59. state = BUY_REASON_STATE.get(normalized_reason)
  60. if state == "mid_regime":
  61. if buy_c1 < 20:
  62. return "low_oversold_regime"
  63. if buy_c1 >= 70:
  64. return "high_regime"
  65. return state or "mid_regime"
  66. def _infer_entry_layer(buy_reason: str) -> str:
  67. normalized_reason = buy_reason.split(":", 1)[0]
  68. return BUY_REASON_QUALIFICATION.get(normalized_reason, "base_entry")
  69. def _infer_management_layer(sell_reason: str) -> str:
  70. return SELL_REASON_MANAGEMENT.get(sell_reason, "default_exit_management")
  71. def _infer_aux_context(hold_aux_buy: pd.DataFrame, post_exit_aux_sell: pd.DataFrame) -> tuple[str, str]:
  72. if hold_aux_buy.empty and post_exit_aux_sell.empty:
  73. return "no_aux_signal", ""
  74. if not hold_aux_buy.empty and not post_exit_aux_sell.empty:
  75. aux_layer = "aux_buy_and_aux_sell"
  76. elif not hold_aux_buy.empty:
  77. aux_layer = "aux_buy_only"
  78. else:
  79. aux_layer = "aux_sell_only"
  80. detail_frames = []
  81. if not hold_aux_buy.empty:
  82. detail_frames.append(hold_aux_buy)
  83. if not post_exit_aux_sell.empty:
  84. detail_frames.append(post_exit_aux_sell)
  85. aux_detail_df = pd.concat(detail_frames, ignore_index=True).sort_values("dt")
  86. detail = " | ".join(
  87. f"{row['date']}:{row['side']}:{row['reason']}" for _, row in aux_detail_df.iterrows()
  88. )
  89. return aux_layer, detail
  90. def _build_taxonomy_markdown(base_dir: Path, path_df: pd.DataFrame) -> None:
  91. lines = [
  92. "# Dragon Rule Taxonomy",
  93. "",
  94. "## Layer 1 Market State",
  95. "- `high_regime`: entries born in hot / high-C1 continuation or late-trend expansion states.",
  96. "- `mid_regime`: classic glued or middle-zone continuation entries.",
  97. "- `low_oversold_regime`: deep oversold, dual-gold reversal, low-C1 rebound, oversold recovery entries.",
  98. "- `rebound_after_sell_regime`: reentries after a prior sell, predictive error recovery, post-washout restart.",
  99. "- `crash_probe_regime`: early crash probe entries that intentionally test panic states.",
  100. "",
  101. "## Layer 2 Entry Qualification",
  102. ]
  103. qualification_map = (
  104. path_df[["buy_reason", "entry_qualification_layer"]]
  105. .drop_duplicates()
  106. .sort_values(["entry_qualification_layer", "buy_reason"])
  107. )
  108. for _, row in qualification_map.iterrows():
  109. lines.append(f"- `{row['buy_reason']}` -> `{row['entry_qualification_layer']}`")
  110. lines.extend(["", "## Layer 3 Position Management"])
  111. management_map = (
  112. path_df[["sell_reason", "position_management_layer"]]
  113. .drop_duplicates()
  114. .sort_values(["position_management_layer", "sell_reason"])
  115. )
  116. for _, row in management_map.iterrows():
  117. lines.append(f"- `{row['sell_reason']}` -> `{row['position_management_layer']}`")
  118. lines.extend(
  119. [
  120. "",
  121. "## Layer 4 Auxiliary Signal Context",
  122. "- `no_aux_signal`: no auxiliary confirmation inside the holding window.",
  123. "- `aux_buy_only`: only holding-period bullish confirmation appeared.",
  124. "- `aux_sell_only`: only post-exit or in-trade bearish confirmation appeared.",
  125. "- `aux_buy_and_aux_sell`: both auxiliary bullish and bearish signals appeared within the trade path.",
  126. "",
  127. "## Path Summary",
  128. ]
  129. )
  130. path_summary = (
  131. path_df.groupby(
  132. [
  133. "market_state_layer",
  134. "entry_qualification_layer",
  135. "position_management_layer",
  136. "aux_context_layer",
  137. ]
  138. )
  139. .size()
  140. .reset_index(name="count")
  141. .sort_values("count", ascending=False)
  142. )
  143. for _, row in path_summary.head(30).iterrows():
  144. lines.append(
  145. f"- `{row['market_state_layer']}` -> `{row['entry_qualification_layer']}` -> "
  146. f"`{row['position_management_layer']}` -> `{row['aux_context_layer']}`: `{int(row['count'])}`"
  147. )
  148. (base_dir / "dragon_rule_taxonomy.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  149. def main() -> None:
  150. base_dir = Path(__file__).resolve().parent
  151. trades = pd.read_csv(base_dir / "dragon_strategy_trades.csv", encoding="utf-8-sig")
  152. events = pd.read_csv(base_dir / "dragon_strategy_events.csv", encoding="utf-8-sig")
  153. workbook = pd.read_csv(base_dir / "dragon_workbook_layers.csv", encoding="utf-8-sig")
  154. trades["buy_dt"] = pd.to_datetime(trades["buy_date"])
  155. trades["sell_dt"] = pd.to_datetime(trades["sell_date"])
  156. events["dt"] = pd.to_datetime(events["date"])
  157. trades["next_buy_dt"] = trades["buy_dt"].shift(-1)
  158. trades["next_buy_date"] = trades["buy_date"].shift(-1)
  159. buy_events = (
  160. events[(events["layer"] == "real_trade") & (events["side"] == "BUY")]
  161. .rename(columns={"date": "buy_date", "close": "buy_close", "a1": "buy_a1", "b1": "buy_b1", "c1": "buy_c1"})
  162. [["buy_date", "buy_close", "buy_a1", "buy_b1", "buy_c1", "kdj_buy", "ql_buy"]]
  163. .copy()
  164. )
  165. sell_events = (
  166. events[(events["layer"] == "real_trade") & (events["side"] == "SELL")]
  167. .rename(columns={"date": "sell_date", "close": "sell_close", "a1": "sell_a1", "b1": "sell_b1", "c1": "sell_c1"})
  168. [["sell_date", "sell_close", "sell_a1", "sell_b1", "sell_c1", "kdj_sell", "ql_sell"]]
  169. .copy()
  170. )
  171. path_df = trades.merge(buy_events, on="buy_date", how="left").merge(sell_events, on="sell_date", how="left")
  172. path_df["market_state_layer"] = path_df.apply(lambda row: _infer_state_layer(str(row["buy_reason"]), float(row["buy_c1"])), axis=1)
  173. path_df["entry_qualification_layer"] = path_df["buy_reason"].map(_infer_entry_layer)
  174. path_df["position_management_layer"] = path_df["sell_reason"].map(_infer_management_layer)
  175. workbook_real_buy = set(workbook[(workbook["layer"] == "real_trade") & (workbook["side"] == "BUY")]["date"])
  176. workbook_real_sell = set(workbook[(workbook["layer"] == "real_trade") & (workbook["side"] == "SELL")]["date"])
  177. path_df["buy_aligned_with_workbook"] = path_df["buy_date"].isin(workbook_real_buy)
  178. path_df["sell_aligned_with_workbook"] = path_df["sell_date"].isin(workbook_real_sell)
  179. aux_layers: list[str] = []
  180. aux_details: list[str] = []
  181. aux_counts: list[int] = []
  182. hold_aux_buy_counts: list[int] = []
  183. hold_aux_buy_details: list[str] = []
  184. post_exit_aux_sell_counts: list[int] = []
  185. post_exit_aux_sell_details: list[str] = []
  186. for _, trade in path_df.iterrows():
  187. hold_aux_buy = events[
  188. (events["layer"] == "aux_signal")
  189. & (events["side"] == "BUY")
  190. & (events["dt"] >= trade["buy_dt"])
  191. & (events["dt"] <= trade["sell_dt"])
  192. ].sort_values("dt")
  193. post_exit_mask = (
  194. (events["layer"] == "aux_signal")
  195. & (events["side"] == "SELL")
  196. & (events["dt"] > trade["sell_dt"])
  197. )
  198. if pd.notna(trade["next_buy_dt"]):
  199. post_exit_mask = post_exit_mask & (events["dt"] < trade["next_buy_dt"])
  200. post_exit_aux_sell = events[post_exit_mask].sort_values("dt")
  201. aux_layer, aux_detail = _infer_aux_context(hold_aux_buy, post_exit_aux_sell)
  202. aux_layers.append(aux_layer)
  203. aux_details.append(aux_detail)
  204. aux_counts.append(len(hold_aux_buy) + len(post_exit_aux_sell))
  205. hold_aux_buy_counts.append(len(hold_aux_buy))
  206. hold_aux_buy_details.append(
  207. " | ".join(f"{row['date']}:BUY:{row['reason']}" for _, row in hold_aux_buy.iterrows())
  208. )
  209. post_exit_aux_sell_counts.append(len(post_exit_aux_sell))
  210. post_exit_aux_sell_details.append(
  211. " | ".join(f"{row['date']}:SELL:{row['reason']}" for _, row in post_exit_aux_sell.iterrows())
  212. )
  213. path_df["aux_context_layer"] = aux_layers
  214. path_df["aux_signal_count"] = aux_counts
  215. path_df["aux_signal_detail"] = aux_details
  216. path_df["hold_aux_buy_count"] = hold_aux_buy_counts
  217. path_df["hold_aux_buy_detail"] = hold_aux_buy_details
  218. path_df["post_exit_aux_sell_count"] = post_exit_aux_sell_counts
  219. path_df["post_exit_aux_sell_detail"] = post_exit_aux_sell_details
  220. path_df["layer_path"] = path_df.apply(
  221. lambda row: " > ".join(
  222. [
  223. row["market_state_layer"],
  224. row["entry_qualification_layer"],
  225. row["position_management_layer"],
  226. row["aux_context_layer"],
  227. ]
  228. ),
  229. axis=1,
  230. )
  231. output_cols = [
  232. "buy_date",
  233. "sell_date",
  234. "holding_days",
  235. "return_pct",
  236. "buy_reason",
  237. "sell_reason",
  238. "buy_a1",
  239. "buy_b1",
  240. "buy_c1",
  241. "sell_a1",
  242. "sell_b1",
  243. "sell_c1",
  244. "market_state_layer",
  245. "entry_qualification_layer",
  246. "position_management_layer",
  247. "aux_context_layer",
  248. "aux_signal_count",
  249. "hold_aux_buy_count",
  250. "hold_aux_buy_detail",
  251. "post_exit_aux_sell_count",
  252. "post_exit_aux_sell_detail",
  253. "aux_signal_detail",
  254. "buy_aligned_with_workbook",
  255. "sell_aligned_with_workbook",
  256. "next_buy_date",
  257. "layer_path",
  258. ]
  259. path_df[output_cols].to_csv(base_dir / "dragon_trade_path_trace.csv", index=False, encoding="utf-8-sig")
  260. _build_taxonomy_markdown(base_dir, path_df)
  261. if __name__ == "__main__":
  262. main()