summarize_eth_recent_bearish_candidates.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. import pandas as pd
  5. REPORT_DIR = Path("reports/eth-exploration")
  6. DATA_META = Path("data/okx-candles/ETH-USDT-SWAP/15m.meta.json")
  7. SUMMARY_CSV = REPORT_DIR / "eth-recent-bearish-candidates-summary.csv"
  8. REPORT_MD = REPORT_DIR / "eth-recent-bearish-candidates-report.md"
  9. HORIZONS = ("full", "3y", "1y", "6m", "3m")
  10. CANDIDATES = (
  11. {
  12. "candidate": "crash_follow_short",
  13. "source": REPORT_DIR / "eth-bearish-price-proxy-totals.csv",
  14. "name": "crash_follow-1H-f20-s120-lb8-th0.035-sl0.02-tp0.06-h96-btc_riskoff",
  15. "direction": "short",
  16. "logic": "BTC risk-off gate + ETH downside continuation; enter short after sharp ETH drop below EMA regime.",
  17. "readonly_observe": "yes",
  18. "reason": "All requested windows are positive, but full-window MDD is high and PF is thin.",
  19. },
  20. {
  21. "candidate": "trend_exhaustion_short",
  22. "source": REPORT_DIR / "eth-bearish-failure-confirmation-totals.csv",
  23. "name": "trend_exhaustion-1H-f50-s240-lb8-th0.012-sl0.03-tp0.045-h72-none",
  24. "direction": "short",
  25. "logic": "Short failed relief rallies inside a falling EMA regime.",
  26. "readonly_observe": "yes",
  27. "reason": "All requested windows are positive, but 3m PF is near flat and historical drawdown remains material.",
  28. },
  29. {
  30. "candidate": "calendar_short_anomaly",
  31. "source": REPORT_DIR / "eth-btc-calendar-carry-totals.csv",
  32. "name": "eth-1h-short-h22-weekend-hold4-volcalm",
  33. "direction": "short",
  34. "logic": "Fixed-hold ETH weekend short bucket under calm volatility.",
  35. "readonly_observe": "no",
  36. "reason": "Positive windows exist, but full return is small relative to MDD and edge is calendar-fragile.",
  37. },
  38. )
  39. def pct(value: float) -> str:
  40. return f"{value * 100:.2f}%"
  41. def metric(row: pd.Series, horizon: str, key: str) -> object:
  42. return row[f"{horizon}_{key}"]
  43. def load_selected(candidate: dict[str, object]) -> pd.Series:
  44. frame = pd.read_csv(Path(candidate["source"]))
  45. selected = frame[frame["name"] == candidate["name"]]
  46. if len(selected) != 1:
  47. raise ValueError(f"missing candidate {candidate['name']} in {candidate['source']}")
  48. return selected.iloc[0]
  49. def data_window() -> str:
  50. payload = json.loads(DATA_META.read_text(encoding="utf-8"))
  51. first = pd.to_datetime(int(payload["first_ts"]), unit="ms", utc=True).strftime("%Y-%m-%d %H:%M UTC")
  52. last = pd.to_datetime(int(payload["last_ts"]), unit="ms", utc=True).strftime("%Y-%m-%d %H:%M UTC")
  53. return f"{first} to {last}; rows={payload['rows']}"
  54. def markdown_table(frame: pd.DataFrame) -> str:
  55. rows = [list(frame.columns), ["---" for _ in frame.columns]]
  56. rows.extend(frame.astype(object).where(pd.notna(frame), "").values.tolist())
  57. return "\n".join("| " + " | ".join(str(value).replace("|", "\\|") for value in row) + " |" for row in rows)
  58. def main() -> int:
  59. summary_rows: list[dict[str, object]] = []
  60. decision_rows: list[dict[str, object]] = []
  61. for candidate in CANDIDATES:
  62. row = load_selected(candidate)
  63. for horizon in HORIZONS:
  64. summary_rows.append(
  65. {
  66. "candidate": candidate["candidate"],
  67. "strategy_name": candidate["name"],
  68. "direction": candidate["direction"],
  69. "horizon": "available_full" if horizon == "full" else horizon,
  70. "total_return": metric(row, horizon, "total_return"),
  71. "max_drawdown": metric(row, horizon, "max_drawdown"),
  72. "trades": int(metric(row, horizon, "trades")),
  73. "profit_factor": metric(row, horizon, "profit_factor"),
  74. "win_rate": metric(row, horizon, "win_rate"),
  75. "readonly_observe": candidate["readonly_observe"],
  76. }
  77. )
  78. decision_rows.append(
  79. {
  80. "candidate": candidate["candidate"],
  81. "direction": candidate["direction"],
  82. "logic": candidate["logic"],
  83. "readonly_observe": candidate["readonly_observe"],
  84. "reason": candidate["reason"],
  85. }
  86. )
  87. summary = pd.DataFrame(summary_rows)
  88. SUMMARY_CSV.parent.mkdir(parents=True, exist_ok=True)
  89. summary.to_csv(SUMMARY_CSV, index=False)
  90. display = summary.copy()
  91. display["total_return"] = display["total_return"].map(pct)
  92. display["max_drawdown"] = display["max_drawdown"].map(pct)
  93. display["profit_factor"] = display["profit_factor"].map(lambda value: f"{value:.3f}")
  94. display["win_rate"] = display["win_rate"].map(pct)
  95. report = (
  96. "# ETH Recent Bearish Candidate Review\n\n"
  97. f"Data window: {data_window()}.\n\n"
  98. "Scope: non-BB-squeeze candidates only; local candle/search outputs only; no live executor, deploy, or order path touched. "
  99. "The local ETH history is shorter than 10y, so `available_full` is the maximum available window.\n\n"
  100. "## Decision\n\n"
  101. f"{markdown_table(pd.DataFrame(decision_rows))}\n\n"
  102. "## Metrics\n\n"
  103. f"{markdown_table(display[['candidate', 'direction', 'horizon', 'total_return', 'max_drawdown', 'trades', 'profit_factor', 'win_rate', 'readonly_observe']])}\n\n"
  104. "## Source Files\n\n"
  105. "- `reports/eth-exploration/eth-bearish-price-proxy-totals.csv`\n"
  106. "- `reports/eth-exploration/eth-bearish-failure-confirmation-totals.csv`\n"
  107. "- `reports/eth-exploration/eth-btc-calendar-carry-totals.csv`\n"
  108. )
  109. REPORT_MD.write_text(report, encoding="utf-8")
  110. print(f"wrote {SUMMARY_CSV} and {REPORT_MD}")
  111. return 0
  112. if __name__ == "__main__":
  113. raise SystemExit(main())