evaluate_live_bb_squeeze_recent_risk_exit.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. from __future__ import annotations
  2. import argparse
  3. import sys
  4. from dataclasses import dataclass
  5. from pathlib import Path
  6. import pandas as pd
  7. sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
  8. from okx_codex_trader.models import Candle
  9. from okx_codex_trader.sampled_report import SegmentResult, mark_to_market, trade_equity
  10. from scripts.search_live_bb_squeeze_exit_variants import (
  11. COSTS,
  12. DATA_DIR,
  13. INITIAL_EQUITY,
  14. LEVERAGE,
  15. OUTPUT_DIR,
  16. PRIMARY_COST,
  17. _format_ts,
  18. _load_candles,
  19. cost_equity_frame,
  20. equity_metrics,
  21. worst_month,
  22. )
  23. ETH_SYMBOL = "ETH-USDT-SWAP"
  24. BTC_SYMBOL = "BTC-USDT-SWAP"
  25. BAR = "15m"
  26. MIN_TRADES_PER_YEAR = 60.0
  27. RECENT_WINDOWS = ("1y", "6m", "3m")
  28. WINDOWS = (
  29. ("full", None),
  30. ("1y", pd.DateOffset(years=1)),
  31. ("6m", pd.DateOffset(months=6)),
  32. ("3m", pd.DateOffset(months=3)),
  33. )
  34. @dataclass(frozen=True)
  35. class Variant:
  36. middle_exit_buffer_pct: float
  37. middle_exit_confirm_bars: int
  38. stop_loss_pct: float
  39. take_profit_pct: float | None
  40. eth_vol_cap: float
  41. risk_filter: str
  42. max_hold_bars: int | None
  43. @property
  44. def name(self) -> str:
  45. take_profit = "none" if self.take_profit_pct is None else f"{self.take_profit_pct:g}"
  46. max_hold = "none" if self.max_hold_bars is None else str(self.max_hold_bars)
  47. return (
  48. "live-bb-squeeze-risk-exit"
  49. f"-mxbuf{self.middle_exit_buffer_pct:g}-mxc{self.middle_exit_confirm_bars}"
  50. f"-sl{self.stop_loss_pct:g}-tp{take_profit}"
  51. f"-vc{self.eth_vol_cap:g}-{self.risk_filter}-mh{max_hold}"
  52. )
  53. def align_pair(left: list[Candle], right: list[Candle]) -> tuple[list[Candle], list[Candle]]:
  54. right_by_ts = {candle.ts: candle for candle in right}
  55. left_out: list[Candle] = []
  56. right_out: list[Candle] = []
  57. for candle in left:
  58. other = right_by_ts.get(candle.ts)
  59. if other is not None:
  60. left_out.append(candle)
  61. right_out.append(other)
  62. return left_out, right_out
  63. def close_position(
  64. trades: list[dict[str, object]],
  65. exits: list[dict[str, object]],
  66. position: dict[str, object],
  67. candle: Candle,
  68. exit_price: float,
  69. reason: str,
  70. ) -> tuple[float, bool]:
  71. margin_used = float(position["margin_used"])
  72. exit_equity = trade_equity(
  73. side=str(position["side"]),
  74. margin_used=margin_used,
  75. entry_price=float(position["entry_price"]),
  76. exit_price=exit_price,
  77. leverage=LEVERAGE,
  78. )
  79. pnl = exit_equity - margin_used
  80. trades.append(
  81. {
  82. "side": "Long" if position["side"] == "long" else "Short",
  83. "entry_time": _format_ts(int(position["entry_time"])),
  84. "exit_time": _format_ts(candle.ts),
  85. "exit_ts": candle.ts,
  86. "entry_price": round(float(position["entry_price"]), 4),
  87. "exit_price": round(exit_price, 4),
  88. "pnl": round(pnl, 4),
  89. "return_pct": round(pnl / margin_used * 100.0, 4),
  90. "cost_weight": 1.0,
  91. "exit_reason": reason,
  92. }
  93. )
  94. exits.append({"ts": candle.ts, "price": exit_price, "side": position["side"]})
  95. return exit_equity, pnl > 0.0
  96. def risk_filter_passes(
  97. *,
  98. variant: Variant,
  99. side: str,
  100. index: int,
  101. eth_close: pd.Series,
  102. btc_close: pd.Series,
  103. eth_24h_return: list[float],
  104. btc_24h_return: list[float],
  105. btc_sma: list[float],
  106. ) -> bool:
  107. if variant.risk_filter == "none":
  108. return True
  109. if variant.risk_filter == "eth_24h_not_crash":
  110. return float(eth_24h_return[index]) >= -0.03
  111. if variant.risk_filter == "btc_24h_not_crash":
  112. return float(btc_24h_return[index]) >= -0.02
  113. if variant.risk_filter == "trend_aligned":
  114. if side == "long":
  115. return float(eth_close.iloc[index]) >= float(eth_close.rolling(192).mean().iloc[index])
  116. return float(eth_close.iloc[index]) <= float(eth_close.rolling(192).mean().iloc[index])
  117. if variant.risk_filter == "btc_trend_aligned":
  118. if side == "long":
  119. return float(btc_close.iloc[index]) >= float(btc_sma[index])
  120. return float(btc_close.iloc[index]) <= float(btc_sma[index])
  121. raise ValueError(f"unknown risk filter: {variant.risk_filter}")
  122. def run_variant(eth: list[Candle], btc: list[Candle], variant: Variant) -> SegmentResult:
  123. eth_close = pd.Series([candle.close for candle in eth], dtype=float)
  124. btc_close = pd.Series([candle.close for candle in btc], dtype=float)
  125. middle_series = eth_close.rolling(48).mean()
  126. stdev = eth_close.rolling(48).std(ddof=0)
  127. upper = (middle_series + 2.0 * stdev).tolist()
  128. lower = (middle_series - 2.0 * stdev).tolist()
  129. middle = middle_series.tolist()
  130. bandwidth = ((pd.Series(upper) - pd.Series(lower)) / middle_series).tolist()
  131. threshold = pd.Series(bandwidth, dtype=float).rolling(960).quantile(0.25).tolist()
  132. eth_vol = eth_close.pct_change().rolling(96).std(ddof=0).tolist()
  133. eth_24h_return = (eth_close / eth_close.shift(96) - 1.0).tolist()
  134. btc_24h_return = (btc_close / btc_close.shift(96) - 1.0).tolist()
  135. btc_sma = btc_close.rolling(480).mean().tolist()
  136. equity = INITIAL_EQUITY
  137. ending_equity = equity
  138. peak_equity = equity
  139. max_drawdown_mark = 0.0
  140. wins = 0
  141. trades: list[dict[str, object]] = []
  142. entries: list[dict[str, object]] = []
  143. exits: list[dict[str, object]] = []
  144. equity_curve: list[dict[str, float | int]] = []
  145. position: dict[str, object] | None = None
  146. pending_entry_side: str | None = None
  147. pending_exit_reason: str | None = None
  148. middle_exit_streak = 0
  149. cooldown_until = -1
  150. for index in range(960, len(eth)):
  151. candle = eth[index]
  152. if pending_exit_reason is not None and position is not None:
  153. equity, won = close_position(trades, exits, position, candle, candle.open, pending_exit_reason)
  154. wins += int(won)
  155. position = None
  156. pending_exit_reason = None
  157. middle_exit_streak = 0
  158. cooldown_until = index + 24
  159. if pending_entry_side is not None and position is None and equity > 0.0:
  160. position = {
  161. "side": pending_entry_side,
  162. "entry_time": candle.ts,
  163. "entry_index": index,
  164. "entry_price": candle.open,
  165. "margin_used": equity,
  166. "stop_price": candle.open * (1.0 - variant.stop_loss_pct if pending_entry_side == "long" else 1.0 + variant.stop_loss_pct),
  167. "take_price": None
  168. if variant.take_profit_pct is None
  169. else candle.open * (1.0 + variant.take_profit_pct if pending_entry_side == "long" else 1.0 - variant.take_profit_pct),
  170. }
  171. entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
  172. pending_entry_side = None
  173. current_equity = equity
  174. if position is not None:
  175. side = str(position["side"])
  176. stop_hit = (side == "long" and candle.low <= float(position["stop_price"])) or (
  177. side == "short" and candle.high >= float(position["stop_price"])
  178. )
  179. take_price = position["take_price"]
  180. take_hit = take_price is not None and (
  181. (side == "long" and candle.high >= float(take_price)) or (side == "short" and candle.low <= float(take_price))
  182. )
  183. if stop_hit or take_hit:
  184. exit_price = float(position["stop_price"] if stop_hit else take_price)
  185. equity, won = close_position(trades, exits, position, candle, exit_price, "stop" if stop_hit else "take_profit")
  186. wins += int(won)
  187. current_equity = equity
  188. position = None
  189. middle_exit_streak = 0
  190. cooldown_until = index + 24
  191. if position is not None:
  192. current_equity = mark_to_market(
  193. side=str(position["side"]),
  194. margin_used=float(position["margin_used"]),
  195. entry_price=float(position["entry_price"]),
  196. mark_price=candle.close,
  197. leverage=LEVERAGE,
  198. )
  199. peak_equity = max(peak_equity, current_equity)
  200. max_drawdown_mark = max(max_drawdown_mark, (peak_equity - current_equity) / peak_equity)
  201. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  202. ending_equity = current_equity
  203. if index == len(eth) - 1 or equity <= 0.0:
  204. continue
  205. values = (middle[index], upper[index], lower[index], bandwidth[index], threshold[index], eth_vol[index])
  206. if any(value != value for value in values):
  207. continue
  208. if position is not None:
  209. middle_exit = (
  210. position["side"] == "long" and candle.close < float(middle[index]) * (1.0 - variant.middle_exit_buffer_pct)
  211. ) or (
  212. position["side"] == "short" and candle.close > float(middle[index]) * (1.0 + variant.middle_exit_buffer_pct)
  213. )
  214. middle_exit_streak = middle_exit_streak + 1 if middle_exit else 0
  215. max_hold_exit = variant.max_hold_bars is not None and index - int(position["entry_index"]) >= variant.max_hold_bars
  216. if middle_exit_streak >= variant.middle_exit_confirm_bars:
  217. pending_exit_reason = "middle_exit"
  218. elif max_hold_exit:
  219. pending_exit_reason = "max_hold"
  220. continue
  221. if index < cooldown_until:
  222. continue
  223. if float(eth_vol[index]) > variant.eth_vol_cap:
  224. continue
  225. if bandwidth[index] <= threshold[index]:
  226. if candle.close > float(upper[index]) and risk_filter_passes(
  227. variant=variant,
  228. side="long",
  229. index=index,
  230. eth_close=eth_close,
  231. btc_close=btc_close,
  232. eth_24h_return=eth_24h_return,
  233. btc_24h_return=btc_24h_return,
  234. btc_sma=btc_sma,
  235. ):
  236. pending_entry_side = "long"
  237. elif candle.close < float(lower[index]) and risk_filter_passes(
  238. variant=variant,
  239. side="short",
  240. index=index,
  241. eth_close=eth_close,
  242. btc_close=btc_close,
  243. eth_24h_return=eth_24h_return,
  244. btc_24h_return=btc_24h_return,
  245. btc_sma=btc_sma,
  246. ):
  247. pending_entry_side = "short"
  248. trade_count = len(trades)
  249. return SegmentResult(
  250. trade_count=trade_count,
  251. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  252. win_rate=wins / trade_count if trade_count else 0.0,
  253. max_drawdown=max_drawdown_mark,
  254. trades=trades,
  255. open_position=position,
  256. candles=eth[960:],
  257. equity_curve=equity_curve,
  258. entries=entries,
  259. exits=exits,
  260. )
  261. def build_variants() -> list[Variant]:
  262. variants: list[Variant] = []
  263. for buffer in (0.0, 0.0005, 0.001, 0.0015):
  264. for confirm in (1, 2, 3):
  265. variants.append(Variant(buffer, confirm, 0.01, None, 0.006, "none", None))
  266. for buffer, confirm in ((0.0005, 1), (0.001, 1), (0.001, 3), (0.0015, 2)):
  267. for stop_loss in (0.008, 0.01, 0.012):
  268. for take_profit in (None, 0.02, 0.03):
  269. variants.append(Variant(buffer, confirm, stop_loss, take_profit, 0.006, "none", None))
  270. for buffer, confirm in ((0.0005, 1), (0.001, 1), (0.001, 3), (0.0015, 2)):
  271. for risk_filter in ("eth_24h_not_crash", "btc_24h_not_crash", "trend_aligned", "btc_trend_aligned"):
  272. variants.append(Variant(buffer, confirm, 0.01, None, 0.006, risk_filter, None))
  273. variants.append(Variant(buffer, confirm, 0.01, None, 0.005, risk_filter, None))
  274. for buffer, confirm in ((0.0005, 1), (0.001, 1), (0.0015, 2)):
  275. for max_hold in (96, 192):
  276. variants.append(Variant(buffer, confirm, 0.01, None, 0.006, "none", max_hold))
  277. return sorted(set(variants), key=lambda variant: variant.name)
  278. def window_frame(frame: pd.DataFrame, label: str, offset: pd.DateOffset | None, last_ts: int) -> tuple[pd.DataFrame, int]:
  279. if offset is None:
  280. return frame[["ts", "equity"]].copy(), int(pd.Timestamp(frame["ts"].iloc[0]).timestamp() * 1000)
  281. end_time = pd.to_datetime(last_ts, unit="ms", utc=True)
  282. cutoff = end_time - offset
  283. before = frame[frame["ts"] <= cutoff]
  284. if len(before):
  285. start_equity = float(before["equity"].iloc[-1])
  286. after = frame[frame["ts"] > cutoff]
  287. scoped = pd.concat([pd.DataFrame([{"ts": cutoff, "equity": start_equity}]), after[["ts", "equity"]]], ignore_index=True)
  288. else:
  289. scoped = frame[["ts", "equity"]].copy()
  290. return scoped, int(pd.Timestamp(scoped["ts"].iloc[0]).timestamp() * 1000)
  291. def trade_count_in_window(trades: list[dict[str, object]], start_ts: int, last_ts: int) -> int:
  292. return sum(1 for trade in trades if start_ts < int(trade["exit_ts"]) <= last_ts)
  293. def format_cell(value: object) -> str:
  294. if isinstance(value, float):
  295. return f"{value:.6g}"
  296. return str(value).replace("|", "\\|")
  297. def markdown_table(frame: pd.DataFrame) -> str:
  298. rows = [list(frame.columns), ["---" for _ in frame.columns]]
  299. rows.extend(frame.astype(object).where(pd.notna(frame), "").values.tolist())
  300. return "\n".join("| " + " | ".join(format_cell(value) for value in row) + " |" for row in rows)
  301. def write_report(
  302. *,
  303. summary: pd.DataFrame,
  304. windows: pd.DataFrame,
  305. output_files: list[Path],
  306. command: str,
  307. first_ts: int,
  308. last_ts: int,
  309. ) -> str:
  310. baseline_name = "live-bb-squeeze-risk-exit-mxbuf0.0005-mxc1-sl0.01-tpnone-vc0.006-none-mhnone"
  311. baseline = windows[windows["name"] == baseline_name].copy()
  312. eligible = summary[summary["eligible_frequency"]].copy()
  313. recent_rank = eligible.sort_values(
  314. ["recent_min_calmar", "recent_min_return", "full_net_max_drawdown", "full_trades_per_year"],
  315. ascending=[False, False, True, False],
  316. ).head(10)
  317. full_rank = eligible.sort_values(
  318. ["full_net_calmar", "recent_min_calmar", "full_net_max_drawdown"],
  319. ascending=[False, False, True],
  320. ).head(10)
  321. baseline_pivot = baseline[
  322. ["window", "window_trades", "trades_per_year", "net_total_return", "net_max_drawdown", "net_calmar"]
  323. ]
  324. candidate = recent_rank.iloc[0] if len(recent_rank) else None
  325. replace = "No"
  326. reason = "No frequency-eligible variant was found."
  327. if candidate is not None:
  328. candidate_windows = windows[windows["name"] == candidate["name"]].set_index("window")
  329. baseline_windows = baseline.set_index("window")
  330. recent_better = all(
  331. float(candidate_windows.loc[window, "net_calmar"]) > float(baseline_windows.loc[window, "net_calmar"])
  332. and float(candidate_windows.loc[window, "net_total_return"]) >= float(baseline_windows.loc[window, "net_total_return"])
  333. for window in RECENT_WINDOWS
  334. )
  335. full_not_worse = float(candidate["full_net_max_drawdown"]) <= float(summary.loc[summary["name"] == baseline_name, "full_net_max_drawdown"].iloc[0])
  336. if recent_better and full_not_worse:
  337. replace = "Yes, for paper observation only"
  338. reason = "Best eligible candidate improved all recent windows without increasing full-window closed-equity drawdown."
  339. else:
  340. reason = "Best eligible candidate did not clear every recent window versus the current live parameter set."
  341. return (
  342. "# Live BB squeeze recent risk/exit exploration\n\n"
  343. "Scope: offline local-candle research only. Live executor, deploy files, and order paths were not changed.\n\n"
  344. f"Run command: `{command}`\n"
  345. f"Local aligned history: `{_format_ts(first_ts)}` to `{_format_ts(last_ts)}`.\n"
  346. f"Primary cost model: `{PRIMARY_COST}` from existing BB squeeze evaluation helpers.\n"
  347. f"Frequency rule: eligible variants require at least {MIN_TRADES_PER_YEAR:g} closed trades/year in full, 1y, 6m, and 3m windows.\n\n"
  348. "Output files:\n"
  349. + "\n".join(f"- `{path}`" for path in output_files)
  350. + "\n\n"
  351. "## Current live baseline\n\n"
  352. + markdown_table(baseline_pivot)
  353. + "\n\n"
  354. "## Top recent-ranked eligible variants\n\n"
  355. + markdown_table(
  356. recent_rank[
  357. [
  358. "name",
  359. "full_trades_per_year",
  360. "recent_min_return",
  361. "recent_min_calmar",
  362. "full_net_total_return",
  363. "full_net_max_drawdown",
  364. "full_net_calmar",
  365. ]
  366. ]
  367. )
  368. + "\n\n"
  369. "## Top full-window eligible variants\n\n"
  370. + markdown_table(
  371. full_rank[
  372. [
  373. "name",
  374. "full_trades_per_year",
  375. "full_net_total_return",
  376. "full_net_max_drawdown",
  377. "full_net_calmar",
  378. "recent_min_return",
  379. "recent_min_calmar",
  380. ]
  381. ]
  382. )
  383. + "\n\n"
  384. "## Replacement verdict\n\n"
  385. f"- Recommend replacing live strategy now: {replace}.\n"
  386. f"- Reason: {reason}\n"
  387. )
  388. def main() -> int:
  389. parser = argparse.ArgumentParser()
  390. parser.add_argument("--bar", default=BAR)
  391. parser.add_argument("--output-dir", type=Path, default=OUTPUT_DIR)
  392. args = parser.parse_args()
  393. eth, btc = align_pair(_load_candles(ETH_SYMBOL, args.bar), _load_candles(BTC_SYMBOL, args.bar))
  394. cost = dict(COSTS)[PRIMARY_COST]
  395. summary_rows: list[dict[str, object]] = []
  396. window_rows: list[dict[str, object]] = []
  397. variants = build_variants()
  398. for index, variant in enumerate(variants, start=1):
  399. result = run_variant(eth, btc, variant)
  400. frame = cost_equity_frame(result, cost)
  401. month, month_return = worst_month(frame)
  402. per_window: dict[str, dict[str, float | int | str]] = {}
  403. for label, offset in WINDOWS:
  404. scoped_frame, start_ts = window_frame(frame, label, offset, eth[-1].ts)
  405. metrics = equity_metrics(scoped_frame, start_ts, eth[-1].ts)
  406. trades = trade_count_in_window(result.trades, start_ts, eth[-1].ts)
  407. years = (eth[-1].ts - start_ts) / 86_400_000 / 365
  408. row = {
  409. "cost": PRIMARY_COST,
  410. "symbol": ETH_SYMBOL,
  411. "bar": args.bar,
  412. "name": variant.name,
  413. "window": label,
  414. "window_start": pd.to_datetime(start_ts, unit="ms", utc=True).strftime("%Y-%m-%d %H:%M"),
  415. "window_end": _format_ts(eth[-1].ts),
  416. "window_trades": trades,
  417. "trades_per_year": trades / years if years > 0.0 else 0.0,
  418. **metrics,
  419. }
  420. window_rows.append(row)
  421. per_window[label] = row
  422. eligible_frequency = all(float(per_window[label]["trades_per_year"]) >= MIN_TRADES_PER_YEAR for label, _ in WINDOWS)
  423. recent_returns = [float(per_window[label]["net_total_return"]) for label in RECENT_WINDOWS]
  424. recent_calmars = [float(per_window[label]["net_calmar"]) for label in RECENT_WINDOWS]
  425. summary_rows.append(
  426. {
  427. "cost": PRIMARY_COST,
  428. "symbol": ETH_SYMBOL,
  429. "bar": args.bar,
  430. "name": variant.name,
  431. "middle_exit_buffer_pct": variant.middle_exit_buffer_pct,
  432. "middle_exit_confirm_bars": variant.middle_exit_confirm_bars,
  433. "stop_loss_pct": variant.stop_loss_pct,
  434. "take_profit_pct": variant.take_profit_pct,
  435. "eth_vol_cap": variant.eth_vol_cap,
  436. "risk_filter": variant.risk_filter,
  437. "max_hold_bars": variant.max_hold_bars,
  438. "first_candle": _format_ts(eth[0].ts),
  439. "last_candle": _format_ts(eth[-1].ts),
  440. "trades": result.trade_count,
  441. "full_trades_per_year": float(per_window["full"]["trades_per_year"]),
  442. "eligible_frequency": eligible_frequency,
  443. "gross_total_return": result.total_return,
  444. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  445. "worst_month": month,
  446. "worst_month_return": month_return,
  447. "recent_min_return": min(recent_returns),
  448. "recent_min_calmar": min(recent_calmars),
  449. "recent_avg_return": sum(recent_returns) / len(recent_returns),
  450. "full_net_total_return": per_window["full"]["net_total_return"],
  451. "full_net_annualized_return": per_window["full"]["net_annualized_return"],
  452. "full_net_max_drawdown": per_window["full"]["net_max_drawdown"],
  453. "full_net_calmar": per_window["full"]["net_calmar"],
  454. }
  455. )
  456. print(f"done {index}/{len(variants)} {variant.name}")
  457. summary = pd.DataFrame(summary_rows).sort_values(
  458. ["eligible_frequency", "recent_min_calmar", "recent_min_return", "full_net_calmar"],
  459. ascending=[False, False, False, False],
  460. )
  461. windows = pd.DataFrame(window_rows)
  462. windows["window"] = pd.Categorical(windows["window"], categories=[label for label, _ in WINDOWS], ordered=True)
  463. windows = windows.sort_values(["window", "net_calmar", "net_total_return"], ascending=[True, False, False])
  464. args.output_dir.mkdir(parents=True, exist_ok=True)
  465. prefix = "live-bb-squeeze-recent-risk-exit"
  466. summary_path = args.output_dir / f"{prefix}-summary.csv"
  467. window_path = args.output_dir / f"{prefix}-windows.csv"
  468. report_path = args.output_dir / f"{prefix}-report.md"
  469. output_files = [summary_path, window_path, report_path]
  470. summary.to_csv(summary_path, index=False)
  471. windows.to_csv(window_path, index=False)
  472. command = f"rtk .venv/bin/python {Path(__file__).as_posix()} --bar {args.bar} --output-dir {args.output_dir.as_posix()}"
  473. report_path.write_text(
  474. write_report(summary=summary, windows=windows, output_files=output_files, command=command, first_ts=eth[0].ts, last_ts=eth[-1].ts),
  475. encoding="utf-8",
  476. )
  477. print(summary.head(10).to_string(index=False))
  478. return 0
  479. if __name__ == "__main__":
  480. raise SystemExit(main())