test_bb_squeeze_3m_execution.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. from __future__ import annotations
  2. import argparse
  3. from dataclasses import dataclass
  4. from pathlib import Path
  5. import pandas as pd
  6. from okx_codex_trader.candles import align_candles_by_ts, load_candles_csv
  7. from okx_codex_trader.models import Candle
  8. from okx_codex_trader.research_metrics import (
  9. DEFAULT_COSTS,
  10. DEFAULT_INITIAL_EQUITY,
  11. DEFAULT_PRIMARY_COST,
  12. cost_equity_frame,
  13. equity_metrics,
  14. format_utc_ts,
  15. trade_stats,
  16. )
  17. from okx_codex_trader.sampled_report import SegmentResult, mark_to_market, trade_equity
  18. from okx_codex_trader.time_rules import entry_allowed, is_us_open_window
  19. ETH_SYMBOL = "ETH-USDT-SWAP"
  20. BTC_SYMBOL = "BTC-USDT-SWAP"
  21. DATA_DIR = Path("data/okx-candles")
  22. OUTPUT_DIR = Path("reports/eth-exploration")
  23. INITIAL_EQUITY = DEFAULT_INITIAL_EQUITY
  24. PRIMARY_COST = DEFAULT_PRIMARY_COST
  25. COSTS = DEFAULT_COSTS
  26. LEVERAGE = 3
  27. HORIZONS = (
  28. ("full", None),
  29. ("3y", pd.DateOffset(years=3)),
  30. ("1y", pd.DateOffset(years=1)),
  31. ("6m", pd.DateOffset(months=6)),
  32. ("3m", pd.DateOffset(months=3)),
  33. ("30d", pd.DateOffset(days=30)),
  34. ("21d", pd.DateOffset(days=21)),
  35. )
  36. @dataclass(frozen=True)
  37. class Position:
  38. side: str
  39. entry_ts: int
  40. entry_price: float
  41. margin_used: float
  42. stop_price: float
  43. take_price: float
  44. mfe_pct: float
  45. def fmt(ts: int) -> str:
  46. return format_utc_ts(ts)
  47. def close_trade(trades: list[dict[str, object]], position: Position, candle: Candle, exit_price: float, reason: str) -> tuple[float, bool]:
  48. exit_equity = trade_equity(
  49. side=position.side,
  50. margin_used=position.margin_used,
  51. entry_price=position.entry_price,
  52. exit_price=exit_price,
  53. leverage=LEVERAGE,
  54. )
  55. pnl = exit_equity - position.margin_used
  56. trades.append(
  57. {
  58. "side": "Long" if position.side == "long" else "Short",
  59. "entry_time": fmt(position.entry_ts),
  60. "exit_time": fmt(candle.ts),
  61. "exit_ts": candle.ts,
  62. "entry_price": round(position.entry_price, 4),
  63. "exit_price": round(exit_price, 4),
  64. "pnl": round(pnl, 4),
  65. "return_pct": round(pnl / position.margin_used * 100.0, 4),
  66. "cost_weight": 1.0,
  67. "exit_reason": reason,
  68. "mfe_pct": round(position.mfe_pct * 100.0, 4),
  69. }
  70. )
  71. return exit_equity, pnl > 0.0
  72. def favorable_move(side: str, entry_price: float, candle: Candle) -> float:
  73. if side == "long":
  74. return candle.high / entry_price - 1.0
  75. return entry_price / candle.low - 1.0
  76. def risk_exit(position: Position, candle: Candle, use_breakeven: bool) -> tuple[float, str] | None:
  77. stop = position.stop_price
  78. reason = "stop"
  79. if use_breakeven and position.mfe_pct >= 0.008:
  80. breakeven = position.entry_price * (1.001 if position.side == "long" else 0.999)
  81. stop = max(stop, breakeven) if position.side == "long" else min(stop, breakeven)
  82. reason = "breakeven"
  83. if position.side == "long":
  84. if candle.open <= stop:
  85. return candle.open, f"{reason}_gap" if reason != "stop" else "stop_gap"
  86. if candle.open >= position.take_price:
  87. return candle.open, "take_gap"
  88. if candle.low <= stop:
  89. return stop, reason
  90. if candle.high >= position.take_price:
  91. return position.take_price, "take_profit"
  92. else:
  93. if candle.open >= stop:
  94. return candle.open, f"{reason}_gap" if reason != "stop" else "stop_gap"
  95. if candle.open <= position.take_price:
  96. return candle.open, "take_gap"
  97. if candle.high >= stop:
  98. return stop, reason
  99. if candle.low <= position.take_price:
  100. return position.take_price, "take_profit"
  101. return None
  102. def build_15m_signals(eth: list[Candle], btc: list[Candle]) -> pd.DataFrame:
  103. eth_close = pd.Series([c.close for c in eth], dtype=float)
  104. btc_close = pd.Series([c.close for c in btc], dtype=float)
  105. middle = eth_close.rolling(96).mean()
  106. stdev = eth_close.rolling(96).std(ddof=0)
  107. upper = middle + 2.0 * stdev
  108. lower = middle - 2.0 * stdev
  109. bandwidth = (upper - lower) / middle
  110. threshold = bandwidth.rolling(960).quantile(0.25)
  111. btc_sma = btc_close.rolling(480).mean()
  112. eth_vol = eth_close.pct_change().rolling(96).std(ddof=0)
  113. return pd.DataFrame(
  114. {
  115. "ts": [c.ts for c in eth],
  116. "middle": middle,
  117. "upper": upper,
  118. "lower": lower,
  119. "bandwidth": bandwidth,
  120. "threshold": threshold,
  121. "btc_close": btc_close,
  122. "btc_sma": btc_sma,
  123. "eth_vol": eth_vol,
  124. }
  125. )
  126. def run_15m_baseline(eth: list[Candle], btc: list[Candle]) -> SegmentResult:
  127. indicators = build_15m_signals(eth, btc)
  128. equity = INITIAL_EQUITY
  129. peak = equity
  130. max_dd = 0.0
  131. wins = 0
  132. trades: list[dict[str, object]] = []
  133. equity_curve: list[dict[str, float | int]] = []
  134. position: Position | None = None
  135. pending_side: str | None = None
  136. pending_middle_exit = False
  137. cooldown_until = -1
  138. warmup = 960
  139. for index in range(warmup, len(eth)):
  140. candle = eth[index]
  141. if pending_middle_exit and position is not None:
  142. equity, won = close_trade(trades, position, candle, candle.open, "signal_middle")
  143. wins += int(won)
  144. position = None
  145. pending_middle_exit = False
  146. cooldown_until = index + 24
  147. if pending_side is not None and position is None:
  148. entry = candle.open
  149. position = Position(
  150. side=pending_side,
  151. entry_ts=candle.ts,
  152. entry_price=entry,
  153. margin_used=equity,
  154. stop_price=entry * (0.99 if pending_side == "long" else 1.01),
  155. take_price=entry * (1.03 if pending_side == "long" else 0.97),
  156. mfe_pct=0.0,
  157. )
  158. pending_side = None
  159. current = equity
  160. if position is not None:
  161. out = risk_exit(position, candle, True)
  162. if out is not None:
  163. price, reason = out
  164. equity, won = close_trade(trades, position, candle, price, reason)
  165. wins += int(won)
  166. current = equity
  167. position = None
  168. cooldown_until = index + 24
  169. if position is not None:
  170. position = Position(**{**position.__dict__, "mfe_pct": max(position.mfe_pct, favorable_move(position.side, position.entry_price, candle))})
  171. current = mark_to_market(
  172. side=position.side,
  173. margin_used=position.margin_used,
  174. entry_price=position.entry_price,
  175. mark_price=candle.close,
  176. leverage=LEVERAGE,
  177. )
  178. peak = max(peak, current)
  179. max_dd = max(max_dd, (peak - current) / peak)
  180. equity_curve.append({"ts": candle.ts, "equity": current, "close": candle.close})
  181. if index == len(eth) - 1:
  182. continue
  183. row = indicators.iloc[index]
  184. if any(value != value for value in (row.middle, row.upper, row.lower, row.bandwidth, row.threshold, row.btc_sma, row.eth_vol)):
  185. continue
  186. if position is not None:
  187. middle_exit = (position.side == "long" and candle.close < row.middle * 0.999) or (position.side == "short" and candle.close > row.middle * 1.001)
  188. if middle_exit and not is_us_open_window(candle.ts):
  189. pending_middle_exit = True
  190. continue
  191. if index < cooldown_until or row.eth_vol > 0.006 or not entry_allowed(candle.ts, "weekday") or not row.btc_close > row.btc_sma:
  192. continue
  193. if row.bandwidth <= row.threshold:
  194. if candle.close > row.upper:
  195. pending_side = "long"
  196. elif candle.close < row.lower:
  197. pending_side = "short"
  198. return SegmentResult(len(trades), (equity_curve[-1]["equity"] - INITIAL_EQUITY) / INITIAL_EQUITY, wins / len(trades) if trades else 0.0, max_dd, trades, position.__dict__ if position else None, eth[warmup:], equity_curve, [], [])
  199. def run_3m_execution(eth15: list[Candle], btc15: list[Candle], eth3: list[Candle]) -> SegmentResult:
  200. indicators = build_15m_signals(eth15, btc15)
  201. three_by_window: dict[int, list[Candle]] = {}
  202. for candle in eth3:
  203. parent_ts = candle.ts - (candle.ts % 900_000)
  204. three_by_window.setdefault(parent_ts, []).append(candle)
  205. equity = INITIAL_EQUITY
  206. peak = equity
  207. max_dd = 0.0
  208. wins = 0
  209. trades: list[dict[str, object]] = []
  210. equity_curve: list[dict[str, float | int]] = []
  211. position: Position | None = None
  212. pending_side: str | None = None
  213. pending_middle_exit = False
  214. cooldown_until = -1
  215. warmup = 960
  216. for index in range(warmup, len(eth15)):
  217. candle15 = eth15[index]
  218. subcandles = [c for c in three_by_window.get(candle15.ts, []) if candle15.ts <= c.ts < candle15.ts + 900_000]
  219. if not subcandles:
  220. subcandles = [candle15]
  221. if pending_middle_exit and position is not None:
  222. equity, won = close_trade(trades, position, subcandles[0], subcandles[0].open, "signal_middle")
  223. wins += int(won)
  224. position = None
  225. pending_middle_exit = False
  226. cooldown_until = index + 24
  227. if pending_side is not None and position is None:
  228. entry = subcandles[0].open
  229. position = Position(
  230. side=pending_side,
  231. entry_ts=subcandles[0].ts,
  232. entry_price=entry,
  233. margin_used=equity,
  234. stop_price=entry * (0.99 if pending_side == "long" else 1.01),
  235. take_price=entry * (1.03 if pending_side == "long" else 0.97),
  236. mfe_pct=0.0,
  237. )
  238. pending_side = None
  239. current = equity
  240. if position is not None:
  241. for sub in subcandles:
  242. out = risk_exit(position, sub, True)
  243. if out is not None:
  244. price, reason = out
  245. equity, won = close_trade(trades, position, sub, price, f"3m_{reason}")
  246. wins += int(won)
  247. current = equity
  248. position = None
  249. cooldown_until = index + 24
  250. break
  251. position = Position(**{**position.__dict__, "mfe_pct": max(position.mfe_pct, favorable_move(position.side, position.entry_price, sub))})
  252. if position is not None:
  253. current = mark_to_market(
  254. side=position.side,
  255. margin_used=position.margin_used,
  256. entry_price=position.entry_price,
  257. mark_price=candle15.close,
  258. leverage=LEVERAGE,
  259. )
  260. peak = max(peak, current)
  261. max_dd = max(max_dd, (peak - current) / peak)
  262. equity_curve.append({"ts": candle15.ts, "equity": current, "close": candle15.close})
  263. if index == len(eth15) - 1:
  264. continue
  265. row = indicators.iloc[index]
  266. if any(value != value for value in (row.middle, row.upper, row.lower, row.bandwidth, row.threshold, row.btc_sma, row.eth_vol)):
  267. continue
  268. if position is not None:
  269. middle_exit = (position.side == "long" and candle15.close < row.middle * 0.999) or (position.side == "short" and candle15.close > row.middle * 1.001)
  270. if middle_exit and not is_us_open_window(candle15.ts):
  271. pending_middle_exit = True
  272. continue
  273. if index < cooldown_until or row.eth_vol > 0.006 or not entry_allowed(candle15.ts, "weekday") or not row.btc_close > row.btc_sma:
  274. continue
  275. if row.bandwidth <= row.threshold:
  276. if candle15.close > row.upper:
  277. pending_side = "long"
  278. elif candle15.close < row.lower:
  279. pending_side = "short"
  280. return SegmentResult(len(trades), (equity_curve[-1]["equity"] - INITIAL_EQUITY) / INITIAL_EQUITY, wins / len(trades) if trades else 0.0, max_dd, trades, position.__dict__ if position else None, eth15[warmup:], equity_curve, [], [])
  281. def refined_entry(side: str, subcandles: list[Candle], mode: str) -> tuple[int, float] | None:
  282. if mode == "confirm_1":
  283. if len(subcandles) < 2:
  284. return None
  285. first = subcandles[0]
  286. if side == "long" and first.close > first.open:
  287. return subcandles[1].ts, subcandles[1].open
  288. if side == "short" and first.close < first.open:
  289. return subcandles[1].ts, subcandles[1].open
  290. return None
  291. pullback_rates = {
  292. "pullback_0005": 0.0005,
  293. "pullback_001": 0.001,
  294. "pullback_002": 0.002,
  295. "pullback_003": 0.003,
  296. }
  297. if mode in pullback_rates:
  298. anchor = subcandles[0].open
  299. rate = pullback_rates[mode]
  300. target = anchor * (1.0 - rate if side == "long" else 1.0 + rate)
  301. for offset, candle in enumerate(subcandles[:-1]):
  302. if side == "long" and candle.low <= target:
  303. return subcandles[offset + 1].ts, subcandles[offset + 1].open
  304. if side == "short" and candle.high >= target:
  305. return subcandles[offset + 1].ts, subcandles[offset + 1].open
  306. return None
  307. twap_slices = {
  308. "twap_2x3m": 2,
  309. "twap_3x3m": 3,
  310. "twap_4x3m": 4,
  311. "twap_5x3m": 5,
  312. }
  313. if mode in twap_slices:
  314. if not subcandles:
  315. return None
  316. slices = subcandles[: twap_slices[mode]]
  317. return slices[-1].ts, sum(candle.open for candle in slices) / len(slices)
  318. raise ValueError("entry mode is invalid")
  319. def run_3m_entry_execution(eth15: list[Candle], btc15: list[Candle], eth3: list[Candle], entry_mode: str) -> SegmentResult:
  320. indicators = build_15m_signals(eth15, btc15)
  321. three_by_window: dict[int, list[Candle]] = {}
  322. for candle in eth3:
  323. parent_ts = candle.ts - (candle.ts % 900_000)
  324. three_by_window.setdefault(parent_ts, []).append(candle)
  325. equity = INITIAL_EQUITY
  326. peak = equity
  327. max_dd = 0.0
  328. wins = 0
  329. trades: list[dict[str, object]] = []
  330. equity_curve: list[dict[str, float | int]] = []
  331. position: Position | None = None
  332. pending_side: str | None = None
  333. pending_middle_exit = False
  334. cooldown_until = -1
  335. warmup = 960
  336. for index in range(warmup, len(eth15)):
  337. candle15 = eth15[index]
  338. subcandles = [c for c in three_by_window.get(candle15.ts, []) if candle15.ts <= c.ts < candle15.ts + 900_000]
  339. if not subcandles:
  340. subcandles = [candle15]
  341. if pending_middle_exit and position is not None:
  342. equity, won = close_trade(trades, position, subcandles[0], subcandles[0].open, "signal_middle")
  343. wins += int(won)
  344. position = None
  345. pending_middle_exit = False
  346. cooldown_until = index + 24
  347. if pending_side is not None and position is None:
  348. entry = refined_entry(pending_side, subcandles, entry_mode)
  349. if entry is not None:
  350. entry_ts, entry_price = entry
  351. position = Position(
  352. side=pending_side,
  353. entry_ts=entry_ts,
  354. entry_price=entry_price,
  355. margin_used=equity,
  356. stop_price=entry_price * (0.99 if pending_side == "long" else 1.01),
  357. take_price=entry_price * (1.03 if pending_side == "long" else 0.97),
  358. mfe_pct=0.0,
  359. )
  360. pending_side = None
  361. current = equity
  362. if position is not None:
  363. for sub in subcandles:
  364. if sub.ts < position.entry_ts:
  365. continue
  366. out = risk_exit(position, sub, True)
  367. if out is not None:
  368. price, reason = out
  369. equity, won = close_trade(trades, position, sub, price, f"3m_{reason}")
  370. wins += int(won)
  371. current = equity
  372. position = None
  373. cooldown_until = index + 24
  374. break
  375. position = Position(**{**position.__dict__, "mfe_pct": max(position.mfe_pct, favorable_move(position.side, position.entry_price, sub))})
  376. if position is not None:
  377. current = mark_to_market(
  378. side=position.side,
  379. margin_used=position.margin_used,
  380. entry_price=position.entry_price,
  381. mark_price=candle15.close,
  382. leverage=LEVERAGE,
  383. )
  384. peak = max(peak, current)
  385. max_dd = max(max_dd, (peak - current) / peak)
  386. equity_curve.append({"ts": candle15.ts, "equity": current, "close": candle15.close})
  387. if index == len(eth15) - 1:
  388. continue
  389. row = indicators.iloc[index]
  390. if any(value != value for value in (row.middle, row.upper, row.lower, row.bandwidth, row.threshold, row.btc_sma, row.eth_vol)):
  391. continue
  392. if position is not None:
  393. middle_exit = (position.side == "long" and candle15.close < row.middle * 0.999) or (position.side == "short" and candle15.close > row.middle * 1.001)
  394. if middle_exit and not is_us_open_window(candle15.ts):
  395. pending_middle_exit = True
  396. continue
  397. if index < cooldown_until or row.eth_vol > 0.006 or not entry_allowed(candle15.ts, "weekday") or not row.btc_close > row.btc_sma:
  398. continue
  399. if row.bandwidth <= row.threshold:
  400. if candle15.close > row.upper:
  401. pending_side = "long"
  402. elif candle15.close < row.lower:
  403. pending_side = "short"
  404. return SegmentResult(len(trades), (equity_curve[-1]["equity"] - INITIAL_EQUITY) / INITIAL_EQUITY, wins / len(trades) if trades else 0.0, max_dd, trades, position.__dict__ if position else None, eth15[warmup:], equity_curve, [], [])
  405. def scoped_trades(trades: list[dict[str, object]], start: pd.Timestamp, end_ts: int) -> list[dict[str, object]]:
  406. return [trade for trade in trades if start < pd.to_datetime(str(trade["exit_time"]), utc=True) <= pd.to_datetime(end_ts, unit="ms", utc=True)]
  407. def write_outputs(results: dict[str, SegmentResult], last_ts: int, output_dir: Path) -> None:
  408. rows: list[dict[str, object]] = []
  409. reason_rows: list[dict[str, object]] = []
  410. trade_rows: list[dict[str, object]] = []
  411. for name, result in results.items():
  412. for trade in result.trades:
  413. trade_rows.append({"name": name, **trade})
  414. for cost_name, cost in COSTS:
  415. frame = cost_equity_frame(result, cost)
  416. for label, offset in HORIZONS:
  417. if offset is None:
  418. scoped = frame[["ts", "equity"]].copy()
  419. start = pd.Timestamp(scoped["ts"].iloc[0])
  420. else:
  421. end = pd.to_datetime(last_ts, unit="ms", utc=True)
  422. start = end - offset
  423. before = frame[frame["ts"] <= start]
  424. start_equity = float(before["equity"].iloc[-1]) if len(before) else float(frame["equity"].iloc[0])
  425. scoped = pd.concat([pd.DataFrame([{"ts": start, "equity": start_equity}]), frame[frame["ts"] > start][["ts", "equity"]]], ignore_index=True)
  426. trades = scoped_trades(result.trades, start, last_ts)
  427. rows.append(
  428. {
  429. "name": name,
  430. "cost": cost_name,
  431. "horizon": label,
  432. "trades": len(trades),
  433. "win_rate": sum(1 for trade in trades if float(trade["return_pct"]) > 0.0) / len(trades) if trades else 0.0,
  434. **trade_stats(trades),
  435. **equity_metrics(scoped, int(start.timestamp() * 1000), last_ts),
  436. }
  437. )
  438. for reason, group in pd.DataFrame(result.trades).groupby("exit_reason") if result.trades else []:
  439. reason_rows.append({"name": name, "exit_reason": reason, "trades": len(group), **trade_stats(group.to_dict("records"))})
  440. output_dir.mkdir(parents=True, exist_ok=True)
  441. pd.DataFrame(rows).to_csv(output_dir / "bb-squeeze-15m-signal-3m-execution-horizons.csv", index=False)
  442. pd.DataFrame(reason_rows).to_csv(output_dir / "bb-squeeze-15m-signal-3m-execution-exits.csv", index=False)
  443. pd.DataFrame(trade_rows).to_csv(output_dir / "bb-squeeze-15m-signal-3m-execution-trades.csv", index=False)
  444. primary = pd.DataFrame(rows)
  445. report = "# BB squeeze 15m signal plus 3m execution test\n\n"
  446. report += markdown_table(primary[(primary["cost"] == PRIMARY_COST) & (primary["horizon"].isin(["full", "1y", "6m", "3m", "30d", "21d"]))])
  447. report += "\n"
  448. (output_dir / "bb-squeeze-15m-signal-3m-execution-report.md").write_text(report, encoding="utf-8")
  449. def markdown_table(frame: pd.DataFrame) -> str:
  450. columns = list(frame.columns)
  451. lines = [
  452. "| " + " | ".join(columns) + " |",
  453. "| " + " | ".join(["---"] * len(columns)) + " |",
  454. ]
  455. for row in frame.itertuples(index=False):
  456. lines.append("| " + " | ".join(str(value) for value in row) + " |")
  457. return "\n".join(lines)
  458. def main() -> int:
  459. parser = argparse.ArgumentParser()
  460. parser.add_argument("--years", type=float, default=10.0)
  461. parser.add_argument("--output-dir", type=Path, default=OUTPUT_DIR)
  462. args = parser.parse_args()
  463. eth15 = load_candles_csv(DATA_DIR, ETH_SYMBOL, "15m")
  464. btc15 = load_candles_csv(DATA_DIR, BTC_SYMBOL, "15m")
  465. eth15, btc15 = align_candles_by_ts(eth15, btc15)
  466. eth3 = load_candles_csv(DATA_DIR, ETH_SYMBOL, "3m")
  467. requested = min(int(args.years * 365 * 24 * 60 / 15), len(eth15))
  468. first_ts = eth15[-requested].ts
  469. eth15 = [c for c in eth15 if c.ts >= first_ts]
  470. btc15 = [c for c in btc15 if c.ts >= first_ts]
  471. eth3 = [c for c in eth3 if first_ts <= c.ts <= eth15[-1].ts + 900_000]
  472. results = {
  473. "15m_baseline": run_15m_baseline(eth15, btc15),
  474. "15m_signal_3m_risk": run_3m_execution(eth15, btc15, eth3),
  475. "15m_signal_3m_confirm_1": run_3m_entry_execution(eth15, btc15, eth3, "confirm_1"),
  476. "15m_signal_3m_pullback_0005": run_3m_entry_execution(eth15, btc15, eth3, "pullback_0005"),
  477. "15m_signal_3m_pullback_001": run_3m_entry_execution(eth15, btc15, eth3, "pullback_001"),
  478. "15m_signal_3m_pullback_002": run_3m_entry_execution(eth15, btc15, eth3, "pullback_002"),
  479. "15m_signal_3m_pullback_003": run_3m_entry_execution(eth15, btc15, eth3, "pullback_003"),
  480. "15m_signal_3m_twap_2x3m": run_3m_entry_execution(eth15, btc15, eth3, "twap_2x3m"),
  481. "15m_signal_3m_twap_3x3m": run_3m_entry_execution(eth15, btc15, eth3, "twap_3x3m"),
  482. "15m_signal_3m_twap_4x3m": run_3m_entry_execution(eth15, btc15, eth3, "twap_4x3m"),
  483. "15m_signal_3m_twap_5x3m": run_3m_entry_execution(eth15, btc15, eth3, "twap_5x3m"),
  484. }
  485. write_outputs(results, eth15[-1].ts, args.output_dir)
  486. print((args.output_dir / "bb-squeeze-15m-signal-3m-execution-report.md").as_posix())
  487. return 0
  488. if __name__ == "__main__":
  489. raise SystemExit(main())