dragon_short_holding_family_pressure.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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 _profit_factor(series: pd.Series) -> float:
  7. gross_profit = series[series > 0].sum()
  8. gross_loss = -series[series < 0].sum()
  9. if gross_loss == 0:
  10. return float("inf") if gross_profit > 0 else 0.0
  11. return float(gross_profit / gross_loss)
  12. def _summarize(df: pd.DataFrame, group_cols: list[str]) -> pd.DataFrame:
  13. rows: list[dict[str, object]] = []
  14. for key, group in df.groupby(group_cols, dropna=False):
  15. if not isinstance(key, tuple):
  16. key = (key,)
  17. row = {col: val for col, val in zip(group_cols, key)}
  18. row["trades"] = int(len(group))
  19. row["loss_trades"] = int((group["return_pct"] < 0).sum())
  20. row["win_rate"] = float((group["return_pct"] > 0).mean())
  21. row["avg_return"] = float(group["return_pct"].mean())
  22. row["avg_mfe"] = float(group["mfe_pct"].mean())
  23. row["avg_mae"] = float(group["mae_pct"].mean())
  24. row["avg_buy_plus_3d_return"] = float(group["buy_plus_3d_return"].mean())
  25. row["avg_sell_plus_3d_followthrough"] = float(group["sell_plus_3d_followthrough"].mean())
  26. row["profit_factor"] = _profit_factor(group["return_pct"])
  27. row["drag_score"] = float(len(group) * abs(min(float(group["return_pct"].mean()), 0.0)))
  28. rows.append(row)
  29. return pd.DataFrame(rows)
  30. def main() -> None:
  31. base_dir = Path(__file__).resolve().parent
  32. audit = _load_csv(base_dir, "dragon_short_holding_audit.csv")
  33. group_specs = [
  34. ("entry_family", ["holding_bucket", "entry_family"]),
  35. ("buy_reason", ["holding_bucket", "buy_reason"]),
  36. ("sell_reason", ["holding_bucket", "sell_reason"]),
  37. ("market_state", ["holding_bucket", "market_state_layer"]),
  38. ("entry_gate", ["holding_bucket", "entry_qualification_layer"]),
  39. ("path_combo", ["holding_bucket", "entry_family", "sell_reason"]),
  40. ]
  41. frames = []
  42. for group_type, cols in group_specs:
  43. frames.append(_summarize(audit, cols).assign(group_type=group_type))
  44. pressure = pd.concat(frames, ignore_index=True, sort=False)
  45. pressure = pressure.sort_values(["drag_score", "trades"], ascending=[False, False])
  46. pressure.to_csv(base_dir / "dragon_short_holding_family_pressure.csv", index=False, encoding="utf-8-sig")
  47. top_entry = pressure[pressure["group_type"] == "entry_family"].sort_values("drag_score", ascending=False).head(8)
  48. top_buy = pressure[pressure["group_type"] == "buy_reason"].sort_values("drag_score", ascending=False).head(8)
  49. top_path = pressure[pressure["group_type"] == "path_combo"].sort_values("drag_score", ascending=False).head(8)
  50. lines = [
  51. "# Dragon Short Holding Family Review",
  52. "",
  53. "- Scope: `alpha_first_selective_veto` short trades only.",
  54. "- Drag score definition: `trades * abs(min(avg_return, 0))`.",
  55. "",
  56. "## Top Entry-Family Drag",
  57. ]
  58. for _, row in top_entry.iterrows():
  59. lines.append(
  60. f"- `{row['holding_bucket']} / {row['entry_family']}`: trades `{int(row['trades'])}`, loss_trades `{int(row['loss_trades'])}`, "
  61. f"avg_return `{row['avg_return']:.2%}`, drag_score `{row['drag_score']:.4f}`"
  62. )
  63. lines.extend(["", "## Top Buy-Reason Drag"])
  64. for _, row in top_buy.iterrows():
  65. lines.append(
  66. f"- `{row['holding_bucket']} / {row['buy_reason']}`: trades `{int(row['trades'])}`, loss_trades `{int(row['loss_trades'])}`, "
  67. f"avg_return `{row['avg_return']:.2%}`, avg_buy_plus_3d `{row['avg_buy_plus_3d_return']:.2%}`, drag_score `{row['drag_score']:.4f}`"
  68. )
  69. lines.extend(["", "## Top Path Drag"])
  70. for _, row in top_path.iterrows():
  71. lines.append(
  72. f"- `{row['holding_bucket']} / {row['entry_family']} -> {row['sell_reason']}`: trades `{int(row['trades'])}`, "
  73. f"avg_return `{row['avg_return']:.2%}`, avg_sell_plus_3d `{row['avg_sell_plus_3d_followthrough']:.2%}`, drag_score `{row['drag_score']:.4f}`"
  74. )
  75. lead_entry = top_entry.iloc[0] if not top_entry.empty else None
  76. lead_path = top_path.iloc[0] if not top_path.empty else None
  77. lines.extend(["", "## Quant Judgment"])
  78. if lead_entry is not None:
  79. lines.append(
  80. f"- Lead short-holding drag family: `{lead_entry['holding_bucket']} / {lead_entry['entry_family']}` with drag_score `{lead_entry['drag_score']:.4f}`."
  81. )
  82. if lead_path is not None:
  83. lines.append(
  84. f"- Lead path-level drag: `{lead_path['holding_bucket']} / {lead_path['entry_family']} -> {lead_path['sell_reason']}`."
  85. )
  86. lines.append("- The next experiment pack should attack the highest drag family first and decide whether the issue is bad entry selection or premature exit handling.")
  87. (base_dir / "dragon_short_holding_family_review.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  88. if __name__ == "__main__":
  89. main()