validate_eth_relmom_lb84_regime_gate.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. from __future__ import annotations
  2. import argparse
  3. from dataclasses import dataclass
  4. from pathlib import Path
  5. import pandas as pd
  6. CACHE_DIR = Path("data/okx-candles")
  7. OUTPUT_DIR = Path("reports/eth-exploration")
  8. SOURCE_TOTALS = OUTPUT_DIR / "eth-relative-momentum-totals.csv"
  9. PREFIX = "eth-relmom-lb84-regime-gate"
  10. INITIAL_EQUITY = 10_000.0
  11. TAKER_FEE = 0.0004
  12. BAR = "4H"
  13. LOOKBACK = 84
  14. HORIZONS = (
  15. ("full", None),
  16. ("3y", pd.DateOffset(years=3)),
  17. ("1y", pd.DateOffset(years=1)),
  18. ("6m", pd.DateOffset(months=6)),
  19. ("3m", pd.DateOffset(months=3)),
  20. )
  21. @dataclass(frozen=True)
  22. class Params:
  23. trend: int
  24. rel_entry: float
  25. vol_quantile: float
  26. short_weight: float
  27. long_weight: float
  28. @property
  29. def base_name(self) -> str:
  30. return (
  31. f"eth_relmom-4H-lb84-tr{self.trend}"
  32. f"-re{self.rel_entry:.3f}-vq{self.vol_quantile:.1f}"
  33. f"-sw{self.short_weight:.2f}-lw{self.long_weight:.2f}"
  34. )
  35. @dataclass(frozen=True)
  36. class Gate:
  37. name: str
  38. description: str
  39. GATES = (
  40. Gate("no_gate", "baseline 4H-lb84 signal without extra regime gate"),
  41. Gate("eth_bull90", "ETH trailing 90-day return > 0"),
  42. Gate("eth_bear90", "ETH trailing 90-day return <= 0"),
  43. Gate("btc_bull90", "BTC trailing 90-day return > 0"),
  44. Gate("btc_bear90", "BTC trailing 90-day return <= 0"),
  45. Gate("eth_bull90_high_vol", "ETH trailing 90-day return > 0 and ETH 30-day realized vol above trailing 365-day median"),
  46. Gate("eth_bull90_low_vol", "ETH trailing 90-day return > 0 and ETH 30-day realized vol at or below trailing 365-day median"),
  47. Gate("eth_bear90_high_vol", "ETH trailing 90-day return <= 0 and ETH 30-day realized vol above trailing 365-day median"),
  48. Gate("eth_bear90_low_vol", "ETH trailing 90-day return <= 0 and ETH 30-day realized vol at or below trailing 365-day median"),
  49. Gate("btc_bull90_high_vol", "BTC trailing 90-day return > 0 and ETH 30-day realized vol above trailing 365-day median"),
  50. Gate("btc_bear90_high_vol", "BTC trailing 90-day return <= 0 and ETH 30-day realized vol above trailing 365-day median"),
  51. )
  52. def load_15m(symbol: str) -> pd.DataFrame:
  53. path = CACHE_DIR / symbol / "15m.csv"
  54. frame = pd.read_csv(path)
  55. frame["ts"] = pd.to_datetime(frame["ts"], unit="ms", utc=True)
  56. return frame.sort_values("ts").drop_duplicates("ts", keep="last").set_index("ts")
  57. def resample_4h(frame: pd.DataFrame) -> pd.DataFrame:
  58. out = frame.resample("4h", label="left", closed="left").agg(
  59. open=("open", "first"),
  60. high=("high", "max"),
  61. low=("low", "min"),
  62. close=("close", "last"),
  63. volume=("volume", "sum"),
  64. )
  65. return out.dropna()
  66. def load_closes() -> pd.DataFrame:
  67. return pd.DataFrame(
  68. {
  69. symbol: resample_4h(load_15m(symbol))["close"]
  70. for symbol in ("ETH-USDT-SWAP", "BTC-USDT-SWAP")
  71. }
  72. ).dropna()
  73. def params_from_source(source: Path) -> list[Params]:
  74. frame = pd.read_csv(source)
  75. scoped = frame[
  76. (frame["bar"] == BAR)
  77. & (frame["lookback"] == LOOKBACK)
  78. & (frame["return_1y"] > 0.0)
  79. & (frame["return_6m"] > 0.0)
  80. & (frame["return_3m"] > 0.0)
  81. ].sort_values(["return_1y", "return_6m"], ascending=[False, False])
  82. params: list[Params] = []
  83. seen: set[tuple[int, float, float, float, float]] = set()
  84. for row in scoped.itertuples(index=False):
  85. key = (
  86. int(row.trend),
  87. float(row.rel_entry),
  88. float(row.vol_quantile),
  89. float(row.short_weight),
  90. float(row.long_weight),
  91. )
  92. if key in seen:
  93. continue
  94. seen.add(key)
  95. params.append(Params(*key))
  96. return params
  97. def target_position(closes: pd.DataFrame, params: Params) -> pd.Series:
  98. eth = closes["ETH-USDT-SWAP"]
  99. btc = closes["BTC-USDT-SWAP"]
  100. eth_momentum = eth / eth.shift(LOOKBACK) - 1.0
  101. btc_momentum = btc / btc.shift(LOOKBACK) - 1.0
  102. relative = eth_momentum - btc_momentum
  103. eth_trend = eth.ewm(span=params.trend, adjust=False).mean()
  104. btc_trend = btc.ewm(span=params.trend, adjust=False).mean()
  105. eth_vol = eth.pct_change().rolling(LOOKBACK).std(ddof=1)
  106. vol_gate = eth_vol >= eth_vol.rolling(params.trend).quantile(params.vol_quantile)
  107. position = pd.Series(0.0, index=closes.index)
  108. short_signal = (relative <= -params.rel_entry) & (eth < eth_trend) & vol_gate
  109. long_signal = (relative >= params.rel_entry) & (eth > eth_trend) & (btc > btc_trend) & vol_gate
  110. position.loc[short_signal] = -params.short_weight
  111. position.loc[long_signal] = params.long_weight
  112. return position.fillna(0.0)
  113. def regime_frame(closes: pd.DataFrame) -> pd.DataFrame:
  114. bars_per_day = 6
  115. eth = closes["ETH-USDT-SWAP"]
  116. btc = closes["BTC-USDT-SWAP"]
  117. out = pd.DataFrame(index=closes.index)
  118. out["eth_ret_90d"] = eth / eth.shift(90 * bars_per_day) - 1.0
  119. out["btc_ret_90d"] = btc / btc.shift(90 * bars_per_day) - 1.0
  120. out["eth_rv_30d"] = eth.pct_change().rolling(30 * bars_per_day).std(ddof=1)
  121. out["eth_rv_365d_median"] = out["eth_rv_30d"].rolling(365 * bars_per_day).median()
  122. out["market_regime"] = "unclassified"
  123. out.loc[out["eth_ret_90d"] > 0.0, "market_regime"] = "bull_90d"
  124. out.loc[out["eth_ret_90d"] <= 0.0, "market_regime"] = "bear_90d"
  125. out["vol_regime"] = "unclassified"
  126. out.loc[out["eth_rv_30d"] > out["eth_rv_365d_median"], "vol_regime"] = "high_vol"
  127. out.loc[out["eth_rv_30d"] <= out["eth_rv_365d_median"], "vol_regime"] = "low_vol"
  128. return out
  129. def gate_mask(regimes: pd.DataFrame, gate: Gate) -> pd.Series:
  130. high_vol = regimes["eth_rv_30d"] > regimes["eth_rv_365d_median"]
  131. low_vol = regimes["eth_rv_30d"] <= regimes["eth_rv_365d_median"]
  132. if gate.name == "no_gate":
  133. return pd.Series(True, index=regimes.index)
  134. if gate.name == "eth_bull90":
  135. return regimes["eth_ret_90d"] > 0.0
  136. if gate.name == "eth_bear90":
  137. return regimes["eth_ret_90d"] <= 0.0
  138. if gate.name == "btc_bull90":
  139. return regimes["btc_ret_90d"] > 0.0
  140. if gate.name == "btc_bear90":
  141. return regimes["btc_ret_90d"] <= 0.0
  142. if gate.name == "eth_bull90_high_vol":
  143. return (regimes["eth_ret_90d"] > 0.0) & high_vol
  144. if gate.name == "eth_bull90_low_vol":
  145. return (regimes["eth_ret_90d"] > 0.0) & low_vol
  146. if gate.name == "eth_bear90_high_vol":
  147. return (regimes["eth_ret_90d"] <= 0.0) & high_vol
  148. if gate.name == "eth_bear90_low_vol":
  149. return (regimes["eth_ret_90d"] <= 0.0) & low_vol
  150. if gate.name == "btc_bull90_high_vol":
  151. return (regimes["btc_ret_90d"] > 0.0) & high_vol
  152. if gate.name == "btc_bear90_high_vol":
  153. return (regimes["btc_ret_90d"] <= 0.0) & high_vol
  154. raise ValueError(f"unknown gate: {gate.name}")
  155. def net_returns(closes: pd.DataFrame, position: pd.Series) -> pd.Series:
  156. eth_returns = closes["ETH-USDT-SWAP"].pct_change().fillna(0.0)
  157. executed = position.shift(1).fillna(0.0)
  158. turnover = executed.diff().abs().fillna(executed.abs())
  159. return executed * eth_returns - turnover * TAKER_FEE
  160. def equity_from_returns(returns: pd.Series) -> pd.Series:
  161. equity = INITIAL_EQUITY * (1.0 + returns.fillna(0.0)).cumprod()
  162. equity.name = "equity"
  163. return equity
  164. def trades_from_returns(position: pd.Series, returns: pd.Series) -> list[dict[str, object]]:
  165. executed = position.shift(1).fillna(0.0)
  166. active = executed != 0.0
  167. groups = (active.ne(active.shift(1)) | executed.ne(executed.shift(1))).cumsum()
  168. trades: list[dict[str, object]] = []
  169. for _, mask in active.groupby(groups):
  170. if not bool(mask.iloc[0]):
  171. continue
  172. index = mask.index
  173. trade_returns = returns.loc[index]
  174. trades.append(
  175. {
  176. "side": "short" if float(executed.loc[index[0]]) < 0.0 else "long",
  177. "entry_time": index[0],
  178. "exit_time": index[-1],
  179. "return": float((1.0 + trade_returns).prod() - 1.0),
  180. }
  181. )
  182. return trades
  183. def series_metrics(series: pd.Series) -> dict[str, float]:
  184. if len(series) < 2:
  185. return {"total_return": 0.0, "annualized_return": 0.0, "max_drawdown": 0.0}
  186. years = (series.index[-1] - series.index[0]).total_seconds() / 86_400 / 365
  187. total = float(series.iloc[-1] / series.iloc[0] - 1.0)
  188. annualized = (1.0 + total) ** (1.0 / years) - 1.0 if total > -1.0 and years > 0.0 else 0.0
  189. drawdown = float((series.cummax() - series).div(series.cummax()).max())
  190. return {"total_return": total, "annualized_return": annualized, "max_drawdown": drawdown}
  191. def trade_metrics(trades: list[dict[str, object]], start: pd.Timestamp, end: pd.Timestamp) -> dict[str, float | int]:
  192. scoped = [float(trade["return"]) for trade in trades if start <= pd.Timestamp(trade["exit_time"]) <= end]
  193. wins = [value for value in scoped if value > 0.0]
  194. losses = [value for value in scoped if value < 0.0]
  195. gross_profit = sum(wins)
  196. gross_loss = abs(sum(losses))
  197. return {
  198. "win_rate": len(wins) / len(scoped) if scoped else 0.0,
  199. "profit_factor": gross_profit / gross_loss if gross_loss else 0.0,
  200. "trades": len(scoped),
  201. }
  202. def trade_metrics_for_mask(trades: list[dict[str, object]], mask: pd.Series) -> dict[str, float | int]:
  203. scoped = [float(trade["return"]) for trade in trades if bool(mask.reindex([pd.Timestamp(trade["exit_time"])]).fillna(False).iloc[0])]
  204. wins = [value for value in scoped if value > 0.0]
  205. losses = [value for value in scoped if value < 0.0]
  206. gross_profit = sum(wins)
  207. gross_loss = abs(sum(losses))
  208. return {
  209. "win_rate": len(wins) / len(scoped) if scoped else 0.0,
  210. "profit_factor": gross_profit / gross_loss if gross_loss else 0.0,
  211. "trades": len(scoped),
  212. }
  213. def metric_row(
  214. name: str,
  215. params: Params,
  216. gate: Gate,
  217. segment_type: str,
  218. segment: str,
  219. equity: pd.Series,
  220. trades: list[dict[str, object]],
  221. mask: pd.Series | None = None,
  222. ) -> dict[str, object]:
  223. scoped = equity if mask is None else equity[mask.reindex(equity.index).fillna(False)]
  224. if len(scoped) < 2:
  225. start = equity.index[0]
  226. end = equity.index[-1]
  227. metrics = {"total_return": 0.0, "annualized_return": 0.0, "max_drawdown": 0.0}
  228. trade_stats = {"win_rate": 0.0, "profit_factor": 0.0, "trades": 0}
  229. else:
  230. start = scoped.index[0]
  231. end = scoped.index[-1]
  232. metrics = series_metrics(scoped)
  233. trade_stats = trade_metrics(trades, start, end)
  234. return {
  235. "name": name,
  236. "base_name": params.base_name,
  237. "gate": gate.name,
  238. "gate_description": gate.description,
  239. "segment_type": segment_type,
  240. "segment": segment,
  241. "start": start.strftime("%Y-%m-%d"),
  242. "end": end.strftime("%Y-%m-%d"),
  243. "bar": BAR,
  244. "lookback": LOOKBACK,
  245. "trend": params.trend,
  246. "rel_entry": params.rel_entry,
  247. "vol_quantile": params.vol_quantile,
  248. "short_weight": params.short_weight,
  249. "long_weight": params.long_weight,
  250. **metrics,
  251. **trade_stats,
  252. }
  253. def segment_metric_row(
  254. name: str,
  255. params: Params,
  256. gate: Gate,
  257. segment_type: str,
  258. segment: str,
  259. returns: pd.Series,
  260. trades: list[dict[str, object]],
  261. mask: pd.Series,
  262. ) -> dict[str, object]:
  263. aligned_mask = mask.reindex(returns.index).fillna(False)
  264. scoped_returns = returns[aligned_mask].copy()
  265. if len(scoped_returns) < 2:
  266. start = returns.index[0]
  267. end = returns.index[-1]
  268. metrics = {"total_return": 0.0, "annualized_return": 0.0, "max_drawdown": 0.0}
  269. trade_stats = {"win_rate": 0.0, "profit_factor": 0.0, "trades": 0}
  270. else:
  271. scoped_returns.iloc[0] = 0.0
  272. scoped_equity = equity_from_returns(scoped_returns)
  273. start = scoped_equity.index[0]
  274. end = scoped_equity.index[-1]
  275. metrics = series_metrics(scoped_equity)
  276. trade_stats = trade_metrics_for_mask(trades, aligned_mask)
  277. return {
  278. "name": name,
  279. "base_name": params.base_name,
  280. "gate": gate.name,
  281. "gate_description": gate.description,
  282. "segment_type": segment_type,
  283. "segment": segment,
  284. "start": start.strftime("%Y-%m-%d"),
  285. "end": end.strftime("%Y-%m-%d"),
  286. "bar": BAR,
  287. "lookback": LOOKBACK,
  288. "trend": params.trend,
  289. "rel_entry": params.rel_entry,
  290. "vol_quantile": params.vol_quantile,
  291. "short_weight": params.short_weight,
  292. "long_weight": params.long_weight,
  293. **metrics,
  294. **trade_stats,
  295. }
  296. def horizon_rows(name: str, params: Params, gate: Gate, equity: pd.Series, trades: list[dict[str, object]]) -> list[dict[str, object]]:
  297. rows = []
  298. end = equity.index[-1]
  299. for horizon, offset in HORIZONS:
  300. mask = None if offset is None else pd.Series(equity.index >= end - offset, index=equity.index)
  301. rows.append(metric_row(name, params, gate, "horizon", horizon, equity, trades, mask))
  302. return rows
  303. def period_rows(
  304. name: str,
  305. params: Params,
  306. gate: Gate,
  307. returns: pd.Series,
  308. trades: list[dict[str, object]],
  309. regimes: pd.DataFrame,
  310. ) -> list[dict[str, object]]:
  311. rows: list[dict[str, object]] = []
  312. for year in sorted(set(returns.index.year)):
  313. rows.append(segment_metric_row(name, params, gate, "year", str(year), returns, trades, pd.Series(returns.index.year == year, index=returns.index)))
  314. for segment in ("bull_90d", "bear_90d"):
  315. rows.append(segment_metric_row(name, params, gate, "market_regime", segment, returns, trades, regimes["market_regime"] == segment))
  316. for segment in ("high_vol", "low_vol"):
  317. rows.append(segment_metric_row(name, params, gate, "vol_regime", segment, returns, trades, regimes["vol_regime"] == segment))
  318. return rows
  319. def markdown_table(frame: pd.DataFrame) -> str:
  320. values = [list(frame.columns), ["---" for _ in frame.columns]]
  321. values.extend(frame.astype(object).where(pd.notna(frame), "").values.tolist())
  322. lines = []
  323. for row in values:
  324. cells = []
  325. for value in row:
  326. cells.append(f"{value:.6g}" if isinstance(value, float) else str(value).replace("|", "\\|"))
  327. lines.append("| " + " | ".join(cells) + " |")
  328. return "\n".join(lines)
  329. def write_report(command: str, paths: list[Path], selected: pd.DataFrame, horizons: pd.DataFrame, periods: pd.DataFrame, qualified: pd.DataFrame) -> str:
  330. conclusion = (
  331. "ACCEPT: at least one explicit non-forward-looking regime gate passed the full sample, all required recent horizons, yearly rows, and bull/bear/volatility segment checks."
  332. if len(qualified)
  333. else "REJECT: no 4H-lb84 candidate produced a full-sample logically closed usable strategy after explicit regime gating."
  334. )
  335. display_cols = [
  336. "name",
  337. "total_return",
  338. "annualized_return",
  339. "max_drawdown",
  340. "win_rate",
  341. "profit_factor",
  342. "trades",
  343. "return_3y",
  344. "return_1y",
  345. "return_6m",
  346. "return_3m",
  347. "min_year_return",
  348. "min_market_return",
  349. "min_vol_return",
  350. "usable",
  351. ]
  352. selected_display = selected[display_cols].head(20)
  353. selected_names = set(selected["name"].head(5))
  354. horizon_display = horizons[horizons["name"].isin(selected_names)]
  355. period_display = periods[periods["name"].isin(selected_names)]
  356. return "\n".join(
  357. [
  358. "# ETH Relative Momentum 4H-lb84 Regime Gate Validation",
  359. "",
  360. f"Run command: `{command}`",
  361. "",
  362. "Output files:",
  363. *[f"- `{path}`" for path in paths],
  364. "",
  365. "Scope: offline validation only, using cached OKX ETH-USDT-SWAP and BTC-USDT-SWAP 15m candles resampled to 4H. No live API path or order path is used.",
  366. "No-future rule: all gates use trailing values available at the 4H close; positions are shifted one bar, so execution starts on the next 4H bar.",
  367. "Gate set: no gate, ETH/BTC trailing 90-day bull/bear, and those states crossed with ETH 30-day realized volatility above/below its trailing 365-day rolling median.",
  368. "Usable filter: positive full/3y/1y/6m/3m returns, full max drawdown <= 35%, PF > 1, at least 20 trades, no negative calendar year, no negative bull/bear segment, and no negative high/low-vol segment.",
  369. "",
  370. f"Conclusion: {conclusion}",
  371. "",
  372. "## Top Rows",
  373. "",
  374. markdown_table(selected_display),
  375. "",
  376. "## Required Horizons For Top 5",
  377. "",
  378. markdown_table(horizon_display),
  379. "",
  380. "## Year And Regime Segments For Top 5",
  381. "",
  382. markdown_table(period_display),
  383. "",
  384. ]
  385. )
  386. def main() -> int:
  387. parser = argparse.ArgumentParser()
  388. parser.add_argument("--output-dir", type=Path, default=OUTPUT_DIR)
  389. parser.add_argument("--source-totals", type=Path, default=SOURCE_TOTALS)
  390. args = parser.parse_args()
  391. args.output_dir.mkdir(parents=True, exist_ok=True)
  392. params_list = params_from_source(args.source_totals)
  393. if not params_list:
  394. raise RuntimeError("no recent-positive 4H-lb84 candidates found in source totals")
  395. closes = load_closes()
  396. regimes = regime_frame(closes)
  397. total_rows: list[dict[str, object]] = []
  398. horizon_data: list[dict[str, object]] = []
  399. period_data: list[dict[str, object]] = []
  400. for params in params_list:
  401. base_position = target_position(closes, params)
  402. for gate in GATES:
  403. position = base_position.where(gate_mask(regimes, gate).reindex(base_position.index).fillna(False), 0.0)
  404. returns = net_returns(closes, position)
  405. equity = equity_from_returns(returns)
  406. trades = trades_from_returns(position, returns)
  407. name = f"{params.base_name}-gate-{gate.name}"
  408. horizons = horizon_rows(name, params, gate, equity, trades)
  409. periods = period_rows(name, params, gate, returns, trades, regimes)
  410. by_horizon = {row["segment"]: row for row in horizons}
  411. full = by_horizon["full"]
  412. year_returns = [float(row["total_return"]) for row in periods if row["segment_type"] == "year"]
  413. market_returns = [float(row["total_return"]) for row in periods if row["segment_type"] == "market_regime"]
  414. vol_returns = [float(row["total_return"]) for row in periods if row["segment_type"] == "vol_regime"]
  415. row = {
  416. **full,
  417. "return_3y": float(by_horizon["3y"]["total_return"]),
  418. "return_1y": float(by_horizon["1y"]["total_return"]),
  419. "return_6m": float(by_horizon["6m"]["total_return"]),
  420. "return_3m": float(by_horizon["3m"]["total_return"]),
  421. "min_year_return": min(year_returns),
  422. "min_market_return": min(market_returns),
  423. "min_vol_return": min(vol_returns),
  424. }
  425. row["usable"] = (
  426. row["total_return"] > 0.0
  427. and row["return_3y"] > 0.0
  428. and row["return_1y"] > 0.0
  429. and row["return_6m"] > 0.0
  430. and row["return_3m"] > 0.0
  431. and row["max_drawdown"] <= 0.35
  432. and row["profit_factor"] > 1.0
  433. and row["trades"] >= 20
  434. and row["min_year_return"] >= 0.0
  435. and row["min_market_return"] >= 0.0
  436. and row["min_vol_return"] >= 0.0
  437. )
  438. row["score"] = (
  439. float(row["annualized_return"])
  440. - float(row["max_drawdown"])
  441. + float(row["return_1y"])
  442. + 0.5 * float(row["return_6m"])
  443. + 0.25 * float(row["return_3m"])
  444. + 0.5 * float(row["min_year_return"])
  445. + 0.5 * float(row["min_market_return"])
  446. + 0.5 * float(row["min_vol_return"])
  447. )
  448. total_rows.append(row)
  449. horizon_data.extend(horizons)
  450. period_data.extend(periods)
  451. totals = pd.DataFrame(total_rows).sort_values(["usable", "score"], ascending=[False, False])
  452. horizons = pd.DataFrame(horizon_data)
  453. periods = pd.DataFrame(period_data)
  454. qualified = totals[totals["usable"]]
  455. selected = qualified if len(qualified) else totals.head(25)
  456. totals_path = args.output_dir / f"{PREFIX}-totals.csv"
  457. selected_path = args.output_dir / f"{PREFIX}-selected.csv"
  458. horizon_path = args.output_dir / f"{PREFIX}-horizons.csv"
  459. period_path = args.output_dir / f"{PREFIX}-periods.csv"
  460. report_path = args.output_dir / f"{PREFIX}-report.md"
  461. totals.to_csv(totals_path, index=False)
  462. selected.to_csv(selected_path, index=False)
  463. horizons[horizons["name"].isin(set(selected["name"]))].to_csv(horizon_path, index=False)
  464. periods[periods["name"].isin(set(selected["name"]))].to_csv(period_path, index=False)
  465. report_path.write_text(
  466. write_report(
  467. "rtk .venv/bin/python scripts/validate_eth_relmom_lb84_regime_gate.py",
  468. [totals_path, selected_path, horizon_path, period_path, report_path],
  469. selected,
  470. horizons[horizons["name"].isin(set(selected["name"]))],
  471. periods[periods["name"].isin(set(selected["name"]))],
  472. qualified,
  473. ),
  474. encoding="utf-8",
  475. )
  476. print(report_path)
  477. print(selected.head(10).to_string(index=False))
  478. return 0
  479. if __name__ == "__main__":
  480. raise SystemExit(main())