dragon_aux_signal_audit.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. def _load_csv(base_dir: Path, name: str) -> pd.DataFrame:
  5. return pd.read_csv(base_dir / name, encoding="utf-8-sig")
  6. def _annotate_buy_cluster(row: pd.Series) -> str:
  7. a1 = float(row["a1"])
  8. b1 = float(row["b1"])
  9. c1 = float(row["c1"])
  10. ql_buy = bool(row["ql_buy"])
  11. if ql_buy and a1 > 0.03 and b1 > 0.15:
  12. return "strong_dual_gold_reconfirm"
  13. if b1 > 0.24 and c1 > 45:
  14. return "early_strength_reconfirm"
  15. if a1 > 0.07 and c1 > 90:
  16. return "super_hot_trend_reconfirm"
  17. return "other_hold_reconfirm"
  18. def _annotate_sell_cluster(row: pd.Series) -> str:
  19. reason = str(row["reason"])
  20. a1 = float(row["a1"])
  21. b1 = float(row["b1"])
  22. c1 = float(row["c1"])
  23. days_from_prev_real_sell = row["days_from_prev_real_sell"]
  24. ql_sell = bool(row["ql_sell"])
  25. kdj_sell = bool(row["kdj_sell"])
  26. if reason.endswith("state_crash_followthrough"):
  27. return "crash_followthrough"
  28. if pd.notna(days_from_prev_real_sell) and 0 < float(days_from_prev_real_sell) <= 10:
  29. return "post_exit_confirmation"
  30. if c1 > 80 and (ql_sell or kdj_sell):
  31. return "high_zone_warning"
  32. if b1 < -0.12 and (ql_sell or kdj_sell):
  33. return "strong_break_warning"
  34. if ql_sell and c1 < 20 and a1 < -0.02:
  35. return "deep_oversold_followthrough"
  36. return "other_bearish_followthrough"
  37. def _build_real_trade_lookup(strategy_events: pd.DataFrame, side: str) -> pd.DataFrame:
  38. real = strategy_events[(strategy_events["layer"] == "real_trade") & (strategy_events["side"] == side)].copy()
  39. real["dt"] = pd.to_datetime(real["date"])
  40. real = real.sort_values("dt")
  41. return real[["dt", "date", "reason", "c1"]]
  42. def _attach_nearest_real_context(aux: pd.DataFrame, real_buy: pd.DataFrame, real_sell: pd.DataFrame) -> pd.DataFrame:
  43. aux = aux.copy()
  44. aux["dt"] = pd.to_datetime(aux["date"])
  45. prev_buy_date: list[str | None] = []
  46. days_from_prev_real_buy: list[int | None] = []
  47. prev_sell_date: list[str | None] = []
  48. days_from_prev_real_sell: list[int | None] = []
  49. buy_dates = real_buy["dt"].tolist()
  50. sell_dates = real_sell["dt"].tolist()
  51. for dt in aux["dt"]:
  52. earlier_buys = [x for x in buy_dates if x < dt]
  53. earlier_sells = [x for x in sell_dates if x < dt]
  54. if earlier_buys:
  55. prev_buy = earlier_buys[-1]
  56. prev_buy_date.append(prev_buy.date().isoformat())
  57. days_from_prev_real_buy.append((dt - prev_buy).days)
  58. else:
  59. prev_buy_date.append(None)
  60. days_from_prev_real_buy.append(None)
  61. if earlier_sells:
  62. prev_sell = earlier_sells[-1]
  63. prev_sell_date.append(prev_sell.date().isoformat())
  64. days_from_prev_real_sell.append((dt - prev_sell).days)
  65. else:
  66. prev_sell_date.append(None)
  67. days_from_prev_real_sell.append(None)
  68. aux["prev_real_buy_date"] = prev_buy_date
  69. aux["days_from_prev_real_buy"] = days_from_prev_real_buy
  70. aux["prev_real_sell_date"] = prev_sell_date
  71. aux["days_from_prev_real_sell"] = days_from_prev_real_sell
  72. return aux
  73. def main() -> None:
  74. base_dir = Path(__file__).resolve().parent
  75. strategy_events = _load_csv(base_dir, "dragon_strategy_events.csv")
  76. workbook_layers = _load_csv(base_dir, "dragon_workbook_layers.csv")
  77. aux = strategy_events[strategy_events["layer"] == "aux_signal"].copy()
  78. aux = aux.sort_values(["side", "date"]).reset_index(drop=True)
  79. workbook_aux = workbook_layers[workbook_layers["layer"] == "aux_signal"][
  80. ["date", "side", "signal_reason", "note"]
  81. ].copy()
  82. workbook_aux = workbook_aux.rename(columns={"signal_reason": "workbook_signal_reason", "note": "workbook_note"})
  83. real_buy = _build_real_trade_lookup(strategy_events, "BUY")
  84. real_sell = _build_real_trade_lookup(strategy_events, "SELL")
  85. aux = _attach_nearest_real_context(aux, real_buy, real_sell)
  86. aux = aux.merge(workbook_aux, on=["date", "side"], how="left")
  87. aux["matched_workbook_aux"] = aux["workbook_signal_reason"].notna()
  88. buy_mask = aux["side"] == "BUY"
  89. sell_mask = aux["side"] == "SELL"
  90. aux.loc[buy_mask, "cluster"] = aux.loc[buy_mask].apply(_annotate_buy_cluster, axis=1)
  91. aux.loc[sell_mask, "cluster"] = aux.loc[sell_mask].apply(_annotate_sell_cluster, axis=1)
  92. audit_cols = [
  93. "date",
  94. "side",
  95. "reason",
  96. "cluster",
  97. "matched_workbook_aux",
  98. "prev_real_buy_date",
  99. "days_from_prev_real_buy",
  100. "prev_real_sell_date",
  101. "days_from_prev_real_sell",
  102. "a1",
  103. "b1",
  104. "c1",
  105. "kdj_buy",
  106. "kdj_sell",
  107. "ql_buy",
  108. "ql_sell",
  109. "workbook_signal_reason",
  110. "workbook_note",
  111. ]
  112. aux[audit_cols].to_csv(base_dir / "dragon_aux_signal_audit.csv", index=False, encoding="utf-8-sig")
  113. lines = [
  114. "# Dragon Aux Signal Audit",
  115. "",
  116. f"- Strategy aux signals: `{len(aux)}`",
  117. f"- Aux BUY: `{int(buy_mask.sum())}`",
  118. f"- Aux SELL: `{int(sell_mask.sum())}`",
  119. f"- Workbook overlap: `{int(aux['matched_workbook_aux'].sum())}`",
  120. "",
  121. "## Cluster Summary",
  122. ]
  123. summary = (
  124. aux.groupby(["side", "cluster", "matched_workbook_aux"])
  125. .size()
  126. .reset_index(name="count")
  127. .sort_values(["side", "cluster", "matched_workbook_aux"])
  128. )
  129. for _, row in summary.iterrows():
  130. lines.append(
  131. f"- {row['side']} / {row['cluster']} / matched `{bool(row['matched_workbook_aux'])}`: `{int(row['count'])}`"
  132. )
  133. lines.extend(["", "## Workbook-Matched Aux Rows"])
  134. matched_rows = aux[aux["matched_workbook_aux"]].copy()
  135. for _, row in matched_rows.iterrows():
  136. lines.append(
  137. f"- {row['date']} {row['side']} `{row['reason']}` -> `{row['cluster']}` | a1 `{row['a1']:.4f}` b1 `{row['b1']:.4f}` c1 `{row['c1']:.2f}`"
  138. )
  139. lines.extend(["", "## Top Unmatched Buckets"])
  140. unmatched_summary = (
  141. aux[~aux["matched_workbook_aux"]]
  142. .groupby(["side", "cluster"])
  143. .size()
  144. .reset_index(name="count")
  145. .sort_values("count", ascending=False)
  146. .head(12)
  147. )
  148. for _, row in unmatched_summary.iterrows():
  149. lines.append(f"- {row['side']} / {row['cluster']}: `{int(row['count'])}`")
  150. (base_dir / "dragon_aux_signal_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  151. if __name__ == "__main__":
  152. main()