compare_eth_twap_evaluation.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. from __future__ import annotations
  2. import importlib.util
  3. import sys
  4. from pathlib import Path
  5. import pandas as pd
  6. ROOT = Path(__file__).resolve().parents[1]
  7. sys.path.insert(0, str(ROOT))
  8. from okx_codex_trader.models import Candle
  9. from scripts import search_eth_price_twap_variants as search
  10. from scripts.explore_ultrashort import (
  11. CANDLE_CACHE_DIR,
  12. INITIAL_EQUITY,
  13. LEVERAGE,
  14. build_rsi2_long_guarded_price_twap_candidate,
  15. cost_adjusted_trade_equity_frame,
  16. history_bars_for_years,
  17. load_cached_candles,
  18. recent_horizon_metrics_from_equity,
  19. )
  20. SYMBOL = "ETH-USDT-SWAP"
  21. BAR = "15m"
  22. MAKER_TAKER_COST = 0.0021
  23. OUTPUT_DIR = Path("reports/eth-exploration")
  24. CSV_PATH = OUTPUT_DIR / "eth-twap-evaluation-diff.csv"
  25. MD_PATH = OUTPUT_DIR / "eth-twap-evaluation-diff.md"
  26. HORIZONS = (
  27. ("3y", pd.DateOffset(years=3)),
  28. ("1y", pd.DateOffset(years=1)),
  29. ("6m", pd.DateOffset(months=6)),
  30. ("3m", pd.DateOffset(months=3)),
  31. )
  32. DEEP_SPEC = {
  33. "trend_sma": 50,
  34. "rsi_threshold": 3.0,
  35. "exit_rsi": 55.0,
  36. "stop_loss_pct": 0.008,
  37. "max_hold_bars": 48,
  38. "entry_offsets": (0.002, 0.005, 0.008),
  39. "entry_valid_bars": 3,
  40. "fill_buffer": 0.0,
  41. }
  42. def load_report_module():
  43. path = Path(__file__).resolve().with_name("generate_ultrashort_report.py")
  44. spec = importlib.util.spec_from_file_location("generate_ultrashort_report", path)
  45. if spec is None or spec.loader is None:
  46. raise RuntimeError("cannot load generate_ultrashort_report.py")
  47. module = importlib.util.module_from_spec(spec)
  48. sys.modules[spec.name] = module
  49. spec.loader.exec_module(module)
  50. return module
  51. def cached_history(years: float) -> list[Candle]:
  52. candles, _ = load_cached_candles(CANDLE_CACHE_DIR, SYMBOL, BAR)
  53. if not candles:
  54. raise RuntimeError(f"missing cached candles: {SYMBOL} {BAR}")
  55. requested_bars = history_bars_for_years(BAR, years)
  56. return candles[-requested_bars:] if len(candles) > requested_bars else candles
  57. def equity_frame_from_search_result(result) -> pd.DataFrame:
  58. return search.equity_frame(result)
  59. def metric_rows(label: str, candles: list[Candle], trade_count: int, gross_equity: pd.DataFrame, net_equity: pd.DataFrame) -> list[dict[str, object]]:
  60. gross_return = float(gross_equity["equity"].iloc[-1] / gross_equity["equity"].iloc[0] - 1.0)
  61. net_return = float(net_equity["equity"].iloc[-1] / net_equity["equity"].iloc[0] - 1.0)
  62. rows: list[dict[str, object]] = [
  63. {
  64. "evaluation": label,
  65. "window": "total",
  66. "first_candle": search._format_ts(candles[0].ts),
  67. "last_candle": search._format_ts(candles[-1].ts),
  68. "bars": len(candles),
  69. "trades": trade_count,
  70. "gross_return": gross_return,
  71. "maker_taker_net_return": net_return,
  72. }
  73. ]
  74. horizons = recent_horizon_metrics_from_equity(net_equity, candles[-1].ts, HORIZONS)
  75. for row in horizons.to_dict("records"):
  76. rows.append(
  77. {
  78. "evaluation": label,
  79. "window": row["horizon"],
  80. "first_candle": row["horizon_start"],
  81. "last_candle": row["horizon_end"],
  82. "bars": "",
  83. "trades": trade_count,
  84. "gross_return": "",
  85. "maker_taker_net_return": float(row["net_total_return"]),
  86. }
  87. )
  88. return rows
  89. def pct(value: object) -> str:
  90. if value == "":
  91. return ""
  92. return f"{float(value) * 100:.2f}%"
  93. def markdown_table(frame: pd.DataFrame) -> str:
  94. display = frame.copy()
  95. display["gross_return"] = display["gross_return"].map(pct)
  96. display["maker_taker_net_return"] = display["maker_taker_net_return"].map(pct)
  97. columns = [
  98. "evaluation",
  99. "window",
  100. "first_candle",
  101. "last_candle",
  102. "bars",
  103. "trades",
  104. "gross_return",
  105. "maker_taker_net_return",
  106. ]
  107. rows = [["" if pd.isna(value) else str(value) for value in row] for row in display[columns].itertuples(index=False, name=None)]
  108. return "\n".join(
  109. [
  110. "| " + " | ".join(columns) + " |",
  111. "| " + " | ".join("---" for _ in columns) + " |",
  112. *["| " + " | ".join(row) + " |" for row in rows],
  113. ]
  114. )
  115. def main() -> int:
  116. report = load_report_module()
  117. report_candles = report.load_cached_history(report.load_explore_module(), SYMBOL, BAR, history_bars_for_years(BAR, 10.0))
  118. report_candidate = build_rsi2_long_guarded_price_twap_candidate(
  119. int(DEEP_SPEC["trend_sma"]),
  120. float(DEEP_SPEC["rsi_threshold"]),
  121. float(DEEP_SPEC["exit_rsi"]),
  122. float(DEEP_SPEC["stop_loss_pct"]),
  123. int(DEEP_SPEC["max_hold_bars"]),
  124. tuple(DEEP_SPEC["entry_offsets"]),
  125. int(DEEP_SPEC["entry_valid_bars"]),
  126. float(DEEP_SPEC["fill_buffer"]),
  127. )
  128. report_gross = report_candidate.run(
  129. candles=report_candles,
  130. leverage=LEVERAGE,
  131. warmup_bars=report_candidate.warmup_bars,
  132. )
  133. report_gross_equity = cost_adjusted_trade_equity_frame(report_gross, 0.0)
  134. report_net = cost_adjusted_trade_equity_frame(report_gross, MAKER_TAKER_COST)
  135. search_candles = cached_history(search.YEARS)
  136. search_gross = search.run_price_twap_segment(
  137. candles=search_candles,
  138. spec=DEEP_SPEC,
  139. roundtrip_cost_on_margin=0.0,
  140. )
  141. search_gross_equity = equity_frame_from_search_result(search_gross)
  142. search_net_result = search.run_price_twap_segment(
  143. candles=search_candles,
  144. spec=DEEP_SPEC,
  145. roundtrip_cost_on_margin=MAKER_TAKER_COST,
  146. )
  147. search_net = equity_frame_from_search_result(search_net_result)
  148. rows = [
  149. *metric_rows("main_report_10y_eval", report_candles, report_gross.trade_count, report_gross_equity, report_net),
  150. *metric_rows("search_script_3y_eval", search_candles, search_gross.trade_count, search_gross_equity, search_net),
  151. ]
  152. frame = pd.DataFrame(rows)
  153. OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
  154. frame.to_csv(CSV_PATH, index=False)
  155. MD_PATH.write_text(
  156. "\n".join(
  157. [
  158. "# ETH RSI2 Price TWAP deep evaluation diff",
  159. "",
  160. "Parameter under test: `trend_sma=50, rsi_threshold=3.0, exit_rsi=55.0, stop_loss_pct=0.008, max_hold_bars=48, entry_offsets=(0.002, 0.005, 0.008), entry_valid_bars=3, fill_buffer=0.0`.",
  161. "",
  162. markdown_table(frame),
  163. "",
  164. "## Readout",
  165. "",
  166. "- `main_report_10y_eval` matches `scripts/generate_ultrashort_report.py`: load up to 10 years, run the shared `explore_ultrashort` candidate gross, then apply maker_taker cost with `cost_weight` per closed trade.",
  167. "- `search_script_3y_eval` matches `scripts/search_eth_price_twap_variants.py`: load 3 years, run its local price-TWAP segment, and deduct maker_taker cost inside each close.",
  168. "- The prior search grid did not include the main-report ETH deep parameter. It searched `trend_sma in {80,160}`, `rsi_threshold in {5,8,10}`, `entry_valid_bars in {2,4}`, and offset sets `(0.001,0.003,0.005)` / `(0.003,0.006,0.009)`, not `trend_sma=50`, `rsi_threshold=3`, deep offsets `(0.002,0.005,0.008)`, `entry_valid_bars=3`.",
  169. "- Therefore the sign mismatch is primarily parameter universe plus total-sample scope: the main report's cost table is 10-year total, while the search table was a different 3-year grid sorted by maker_taker `net_calmar` then `net_annualized_return`.",
  170. "",
  171. "Adopt the main-report evaluation口径 for reporting selected strategies: shared `explore_ultrashort` runner, 10-year cached history for total cost sensitivity, and explicit recent horizons for 3y/1y/6m/3m.",
  172. "",
  173. ]
  174. ),
  175. encoding="utf-8",
  176. )
  177. print(CSV_PATH)
  178. print(MD_PATH)
  179. print(frame.to_string(index=False))
  180. return 0
  181. if __name__ == "__main__":
  182. raise SystemExit(main())