dragon_deep_oversold_audit.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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 _classify_entry(row: pd.Series) -> str:
  7. a1 = float(row["buy_a1"])
  8. b1 = float(row["buy_b1"])
  9. c1 = float(row["buy_c1"])
  10. if b1 > 0:
  11. return "positive_b1_rebound"
  12. if c1 < 11 and a1 < -0.05 and b1 < -0.08:
  13. return "deep_capitulation"
  14. if c1 < 12 and b1 < -0.06:
  15. return "classic_oversold"
  16. if c1 >= 15 or a1 > -0.03 or b1 > -0.03:
  17. return "shallow_false_start"
  18. return "mixed_oversold"
  19. def _quality_bucket(row: pd.Series) -> str:
  20. ret = float(row["return_pct"])
  21. mfe = float(row["mfe_pct"])
  22. mae = float(row["mae_pct"])
  23. if ret > 0.02:
  24. return "strong_positive"
  25. if ret > 0 and mfe > 0.03:
  26. return "weak_positive"
  27. if ret <= -0.03 and mfe < 0.01:
  28. return "immediate_failure"
  29. if ret <= -0.03:
  30. return "rebound_then_fail"
  31. if mae < -0.04:
  32. return "high_volatility_noise"
  33. return "flat_noise"
  34. def main() -> None:
  35. base_dir = Path(__file__).resolve().parent
  36. quality = _load_csv(base_dir, "dragon_trade_quality.csv")
  37. path_trace = _load_csv(base_dir, "dragon_trade_path_trace.csv")
  38. rows = quality[quality["buy_reason"].astype(str).str.startswith("deep_oversold_rebound_buy")].copy()
  39. rows = rows.merge(
  40. path_trace[
  41. [
  42. "buy_date",
  43. "sell_date",
  44. "buy_a1",
  45. "buy_b1",
  46. "buy_c1",
  47. "buy_aligned_with_workbook",
  48. "sell_aligned_with_workbook",
  49. ]
  50. ],
  51. on=["buy_date", "sell_date"],
  52. how="left",
  53. )
  54. rows["entry_subtype"] = rows.apply(_classify_entry, axis=1)
  55. rows["quality_bucket"] = rows.apply(_quality_bucket, axis=1)
  56. rows["is_winner"] = rows["return_pct"] > 0
  57. rows["is_fast_failure"] = (rows["holding_days"] <= 6) & (rows["return_pct"] < 0)
  58. rows["is_shallow_like"] = (
  59. (rows["buy_c1"] >= 15)
  60. | (rows["buy_a1"] > -0.03)
  61. | (rows["buy_b1"] > -0.03)
  62. )
  63. audit_cols = [
  64. "buy_date",
  65. "sell_date",
  66. "buy_a1",
  67. "buy_b1",
  68. "buy_c1",
  69. "sell_reason",
  70. "holding_days",
  71. "return_pct",
  72. "mfe_pct",
  73. "mae_pct",
  74. "giveback_from_peak_pct",
  75. "entry_subtype",
  76. "quality_bucket",
  77. "is_winner",
  78. "is_fast_failure",
  79. "is_shallow_like",
  80. "buy_aligned_with_workbook",
  81. "sell_aligned_with_workbook",
  82. ]
  83. rows[audit_cols].to_csv(base_dir / "dragon_deep_oversold_audit.csv", index=False, encoding="utf-8-sig")
  84. subtype_summary = (
  85. rows.groupby("entry_subtype")
  86. .agg(
  87. trades=("buy_date", "count"),
  88. win_rate=("is_winner", "mean"),
  89. avg_return=("return_pct", "mean"),
  90. avg_mfe=("mfe_pct", "mean"),
  91. avg_mae=("mae_pct", "mean"),
  92. fast_failures=("is_fast_failure", "sum"),
  93. shallow_like=("is_shallow_like", "sum"),
  94. )
  95. .reset_index()
  96. .sort_values(["avg_return", "trades"], ascending=[True, False])
  97. )
  98. subtype_summary.to_csv(base_dir / "dragon_deep_oversold_subtype_summary.csv", index=False, encoding="utf-8-sig")
  99. lines = [
  100. "# Dragon Deep Oversold Audit",
  101. "",
  102. f"- audited trades: `{len(rows)}`",
  103. f"- all buy dates aligned with workbook: `{bool(rows['buy_aligned_with_workbook'].all())}`",
  104. f"- all sell dates aligned with workbook: `{bool(rows['sell_aligned_with_workbook'].all())}`",
  105. f"- winners: `{int(rows['is_winner'].sum())}` / `{len(rows)}`",
  106. f"- fast failures (holding <= 6d and negative return): `{int(rows['is_fast_failure'].sum())}`",
  107. "",
  108. "## Entry Subtype Summary",
  109. ]
  110. for _, row in subtype_summary.iterrows():
  111. lines.append(
  112. f"- `{row['entry_subtype']}`: trades `{int(row['trades'])}`, win_rate `{float(row['win_rate']):.2%}`, "
  113. f"avg_return `{float(row['avg_return']):.2%}`, avg_mfe `{float(row['avg_mfe']):.2%}`, "
  114. f"avg_mae `{float(row['avg_mae']):.2%}`, fast_failures `{int(row['fast_failures'])}`"
  115. )
  116. lines.extend(["", "## Candidate Pressure Points"])
  117. for _, row in rows.sort_values(["return_pct", "holding_days"]).head(8).iterrows():
  118. lines.append(
  119. f"- `{row['buy_date']} -> {row['sell_date']}`: `{row['entry_subtype']}` / `{row['quality_bucket']}` | "
  120. f"buy a1 `{float(row['buy_a1']):.4f}` b1 `{float(row['buy_b1']):.4f}` c1 `{float(row['buy_c1']):.2f}` | "
  121. f"return `{float(row['return_pct']):.2%}`"
  122. )
  123. lines.extend(
  124. [
  125. "",
  126. "## Quant Judgment",
  127. "- This rule family cannot be bluntly removed because every current deep-oversold trade is workbook-aligned.",
  128. "- The weakest local pattern is not the deepest capitulation bucket; it is the shallow or positive-B1 rebound subset.",
  129. "- Any redesign should therefore prefer subtype gating or delayed confirmation for shallow rebounds rather than tighter global oversold thresholds.",
  130. ]
  131. )
  132. (base_dir / "dragon_deep_oversold_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  133. if __name__ == "__main__":
  134. main()