search_eth_nextgen_micro_portfolio.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. import sys
  5. from dataclasses import dataclass
  6. from pathlib import Path
  7. import pandas as pd
  8. sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
  9. from scripts import explore_ultrashort as explore
  10. from scripts import search_eth_btc_nextgen_variants as nextgen
  11. from scripts import search_eth_microstructure_variants as micro
  12. OUTPUT_DIR = Path("reports/eth-exploration")
  13. PREFIX = "eth-nextgen-micro-portfolio"
  14. PRIMARY_COST = "maker_taker"
  15. ROUNDTRIP_COST = 0.0021
  16. NEXTGEN_BASELINE = "equal-2-c0003"
  17. NEXTGEN_LEGS = (
  18. "btc_trend_eth_rsi:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0",
  19. "btc_shock_guard_eth_rsi:15m:eth-btc-shock-filter-et50-l3.0-x55.0-bt480-bm240-br0.01-sw96-sv0.01-sd0.05",
  20. )
  21. MICRO_NAMES = (
  22. "atr-compress-expand-r48-q0.15-sl0.008-tp0.016-mf0.25-us",
  23. "atr-compress-expand-r48-q0.15-sl0.008-tp0.016-mf0.4-us",
  24. "atr-compress-expand-r96-q0.15-sl0.008-tp0.016-mf0.25-us",
  25. "atr-compress-expand-r96-q0.15-sl0.008-tp0.016-mf0.4-us",
  26. )
  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. )
  34. @dataclass(frozen=True)
  35. class LegReturn:
  36. source: str
  37. exit_date: pd.Timestamp
  38. value: float
  39. def daily_equity_from_frame(frame: pd.DataFrame, index: pd.DatetimeIndex) -> pd.Series:
  40. series = frame.set_index("ts")["equity"].sort_index()
  41. daily = series.reindex(index.union(series.index)).sort_index().ffill().reindex(index).ffill()
  42. daily.iloc[0] = explore.INITIAL_EQUITY
  43. return daily
  44. def metrics_from_daily(series: pd.Series) -> dict[str, float]:
  45. years = (series.index[-1] - series.index[0]).total_seconds() / 86_400 / 365
  46. total = float(series.iloc[-1] / series.iloc[0] - 1.0)
  47. annualized = (1.0 + total) ** (1.0 / years) - 1.0 if total > -1.0 and years > 0.0 else 0.0
  48. drawdown = explore.max_drawdown_from_equity([float(value) for value in series])
  49. returns = series.pct_change().dropna()
  50. daily_std = float(returns.std(ddof=1)) if len(returns) > 1 else 0.0
  51. risk_reward = float(returns.mean()) / daily_std * (365**0.5) if daily_std else 0.0
  52. return {
  53. "net_total_return": total,
  54. "net_annualized_return": annualized,
  55. "net_max_drawdown": drawdown,
  56. "net_calmar": annualized / drawdown if drawdown else 0.0,
  57. "risk_reward_ratio": risk_reward,
  58. }
  59. def monthly_rows(name: str, series: pd.Series) -> pd.DataFrame:
  60. monthly = series.resample("ME").last()
  61. frame = pd.DataFrame(
  62. {
  63. "name": name,
  64. "month": monthly.index.strftime("%Y-%m"),
  65. "start_equity": monthly.shift(1).fillna(series.iloc[0]).to_numpy(),
  66. "end_equity": monthly.to_numpy(),
  67. }
  68. )
  69. frame["return"] = frame["end_equity"] / frame["start_equity"] - 1.0
  70. return frame
  71. def horizon_rows(name: str, series: pd.Series, trades: list[LegReturn], monthly: pd.DataFrame) -> list[dict[str, object]]:
  72. end = series.index[-1]
  73. rows: list[dict[str, object]] = []
  74. for label, offset in HORIZONS:
  75. horizon = series if offset is None else series[series.index >= end - offset]
  76. if len(horizon) < 2:
  77. horizon = series
  78. start = horizon.index[0]
  79. horizon_trades = [trade for trade in trades if trade.exit_date >= start]
  80. horizon_monthly = monthly[monthly["month"] >= start.strftime("%Y-%m")]
  81. worst = horizon_monthly.sort_values("return").iloc[0] if len(horizon_monthly) else None
  82. rows.append(
  83. {
  84. "name": name,
  85. "horizon": label,
  86. "horizon_start": horizon.index[0].strftime("%Y-%m-%d"),
  87. "horizon_end": horizon.index[-1].strftime("%Y-%m-%d"),
  88. "worst_month": "" if worst is None else str(worst["month"]),
  89. "worst_month_return": 0.0 if worst is None else float(worst["return"]),
  90. **trade_return_stats(horizon_trades),
  91. **metrics_from_daily(horizon),
  92. }
  93. )
  94. return rows
  95. def returns_to_equity(name: str, returns: pd.Series) -> pd.Series:
  96. equity = explore.INITIAL_EQUITY * (1.0 + returns.fillna(0.0)).cumprod()
  97. equity.iloc[0] = explore.INITIAL_EQUITY
  98. equity.name = name
  99. return equity
  100. def trade_return_stats(returns: list[LegReturn]) -> dict[str, float]:
  101. values = [row.value for row in returns]
  102. wins = [value for value in values if value > 0.0]
  103. losses = [value for value in values if value < 0.0]
  104. avg_win = sum(wins) / len(wins) if wins else 0.0
  105. avg_loss_abs = abs(sum(losses) / len(losses)) if losses else 0.0
  106. gross_profit = sum(wins)
  107. gross_loss_abs = abs(sum(losses))
  108. return {
  109. "trades": len(values),
  110. "win_rate": len(wins) / len(values) if values else 0.0,
  111. "payoff_ratio": avg_win / avg_loss_abs if avg_loss_abs else 0.0,
  112. "profit_factor": gross_profit / gross_loss_abs if gross_loss_abs else 0.0,
  113. }
  114. def nextgen_trade_returns(result: object, weight: float) -> list[LegReturn]:
  115. rows: list[LegReturn] = []
  116. for trade in result.trades:
  117. value = (float(trade["return_pct"]) / 100.0 - ROUNDTRIP_COST * float(trade.get("cost_weight", 1.0))) * weight
  118. rows.append(LegReturn("nextgen", pd.to_datetime(str(trade["exit_time"]), utc=True).normalize(), value))
  119. return rows
  120. def micro_trade_returns(result: micro.SegmentResult, weight: float) -> list[LegReturn]:
  121. rows: list[LegReturn] = []
  122. for trade in result.trades:
  123. value = (float(trade["return_pct"]) / 100.0 - ROUNDTRIP_COST * float(trade["cost_weight"])) * weight
  124. rows.append(LegReturn("micro", pd.to_datetime(str(trade["exit_time"]), utc=True).normalize(), value))
  125. return rows
  126. def combine_trade_returns(
  127. *,
  128. nextgen_returns: list[LegReturn],
  129. micro_returns: list[LegReturn],
  130. nextgen_weight: float,
  131. micro_weight: float,
  132. micro_mask: pd.Series | None = None,
  133. ) -> list[LegReturn]:
  134. values = [LegReturn(row.source, row.exit_date, row.value * nextgen_weight) for row in nextgen_returns]
  135. for row in micro_returns:
  136. if micro_mask is None or bool(micro_mask.reindex([row.exit_date]).fillna(False).iloc[0]):
  137. values.append(LegReturn(row.source, row.exit_date, row.value * micro_weight))
  138. return values
  139. def load_nextgen(index: pd.DatetimeIndex) -> tuple[pd.Series, list[LegReturn]]:
  140. strategies = {
  141. f"{strategy.family}:{strategy.bar}:{strategy.candidate.name}": strategy
  142. for strategy in nextgen.build_strategies()
  143. }
  144. missing = [key for key in NEXTGEN_LEGS if key not in strategies]
  145. if missing:
  146. raise KeyError(f"missing nextgen legs: {missing}")
  147. data = {
  148. (symbol, "15m"): nextgen.load_candles(symbol, "15m", 10.0)
  149. for symbol in ("ETH-USDT-SWAP", "BTC-USDT-SWAP")
  150. }
  151. leg_series: list[pd.Series] = []
  152. trade_returns: list[LegReturn] = []
  153. for key in NEXTGEN_LEGS:
  154. result = nextgen.run_strategy(strategies[key], data)
  155. frame = explore.cost_adjusted_trade_equity_frame(result, ROUNDTRIP_COST)
  156. leg_series.append(daily_equity_from_frame(frame, index))
  157. trade_returns.extend(nextgen_trade_returns(result, 0.5))
  158. returns = pd.DataFrame({key: series.pct_change().fillna(0.0) for key, series in zip(NEXTGEN_LEGS, leg_series)}).mean(axis=1)
  159. return returns_to_equity(NEXTGEN_BASELINE, returns), trade_returns
  160. def load_micro_candidates(index: pd.DatetimeIndex) -> dict[str, tuple[pd.Series, list[LegReturn]]]:
  161. candles = micro._load_candles(micro.SYMBOL, micro.BAR)
  162. requested = int(10.0 * 365 * 24 * 60 / 15)
  163. candles = candles[-requested:]
  164. variants = {variant.name: variant for variant in micro.build_variants()}
  165. missing = [name for name in MICRO_NAMES if name not in variants]
  166. if missing:
  167. raise KeyError(f"missing micro variants: {missing}")
  168. output: dict[str, tuple[pd.Series, list[LegReturn]]] = {}
  169. for name in MICRO_NAMES:
  170. result = variants[name].run(candles)
  171. frame = micro.cost_equity_frame(result, ROUNDTRIP_COST)
  172. output[name] = (daily_equity_from_frame(frame, index), micro_trade_returns(result, 1.0))
  173. return output
  174. def evaluate_portfolio(
  175. *,
  176. name: str,
  177. kind: str,
  178. series: pd.Series,
  179. nextgen_weight: float,
  180. micro_weight: float,
  181. micro_name: str,
  182. trade_returns: list[LegReturn],
  183. ) -> tuple[dict[str, object], list[dict[str, object]], pd.DataFrame]:
  184. monthly = monthly_rows(name, series)
  185. worst = monthly.sort_values("return").iloc[0]
  186. horizons = horizon_rows(name, series, trade_returns, monthly)
  187. min_recent = min(float(row["net_total_return"]) for row in horizons if row["horizon"] != "full")
  188. row = {
  189. "name": name,
  190. "kind": kind,
  191. "micro_name": micro_name,
  192. "nextgen_weight": nextgen_weight,
  193. "micro_weight": micro_weight,
  194. "worst_month": str(worst["month"]),
  195. "worst_month_return": float(worst["return"]),
  196. "min_recent_total_return": min_recent,
  197. **trade_return_stats(trade_returns),
  198. **metrics_from_daily(series),
  199. }
  200. return row, horizons, monthly
  201. def build_portfolios(
  202. *,
  203. nextgen_series: pd.Series,
  204. nextgen_trade_returns: list[LegReturn],
  205. micro_candidates: dict[str, tuple[pd.Series, list[LegReturn]]],
  206. ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
  207. rows: list[dict[str, object]] = []
  208. horizon_output: list[dict[str, object]] = []
  209. monthly_frames: list[pd.DataFrame] = []
  210. equity_frames: list[pd.DataFrame] = []
  211. nextgen_returns = nextgen_series.pct_change().fillna(0.0)
  212. baseline_row, baseline_horizons, baseline_monthly = evaluate_portfolio(
  213. name=NEXTGEN_BASELINE,
  214. kind="baseline",
  215. series=nextgen_series,
  216. nextgen_weight=1.0,
  217. micro_weight=0.0,
  218. micro_name="",
  219. trade_returns=nextgen_trade_returns,
  220. )
  221. rows.append(baseline_row)
  222. horizon_output.extend(baseline_horizons)
  223. monthly_frames.append(baseline_monthly)
  224. equity_frames.append(equity_frame(NEXTGEN_BASELINE, nextgen_series))
  225. for micro_name, (micro_series, micro_trade_rows) in micro_candidates.items():
  226. micro_returns = micro_series.pct_change().fillna(0.0)
  227. for micro_weight in (0.10, 0.15, 0.20, 0.25, 0.30):
  228. nextgen_weight = 1.0 - micro_weight
  229. returns = nextgen_returns * nextgen_weight + micro_returns * micro_weight
  230. name = f"blend-ng{nextgen_weight:.2f}-{short_micro_name(micro_name)}"
  231. series = returns_to_equity(name, returns)
  232. trades = combine_trade_returns(
  233. nextgen_returns=nextgen_trade_returns,
  234. micro_returns=micro_trade_rows,
  235. nextgen_weight=nextgen_weight,
  236. micro_weight=micro_weight,
  237. )
  238. add_result(rows, horizon_output, monthly_frames, equity_frames, name, "equity_blend", micro_name, nextgen_weight, micro_weight, series, trades)
  239. flat_mask = nextgen_returns.abs() < 1e-12
  240. for micro_weight in (0.25, 0.40):
  241. returns = nextgen_returns + micro_returns.where(flat_mask, 0.0) * micro_weight
  242. name = f"nonoverlap-m{micro_weight:.2f}-{short_micro_name(micro_name)}"
  243. series = returns_to_equity(name, returns)
  244. trades = combine_trade_returns(
  245. nextgen_returns=nextgen_trade_returns,
  246. micro_returns=micro_trade_rows,
  247. nextgen_weight=1.0,
  248. micro_weight=micro_weight,
  249. micro_mask=flat_mask,
  250. )
  251. add_result(rows, horizon_output, monthly_frames, equity_frames, name, "signal_non_overlap", micro_name, 1.0, micro_weight, series, trades)
  252. for lookback in (30, 60, 90, 120):
  253. nextgen_regime = nextgen_series / nextgen_series.shift(lookback) - 1.0
  254. micro_regime = micro_series / micro_series.shift(lookback) - 1.0
  255. active = ((nextgen_regime < 0.0) & (micro_regime > 0.0)).shift(1).fillna(False).astype(bool)
  256. switch_returns = nextgen_returns.where(~active, micro_returns)
  257. switch_name = f"switch-l{lookback}-{short_micro_name(micro_name)}"
  258. switch_series = returns_to_equity(switch_name, switch_returns)
  259. switch_trades = combine_trade_returns(
  260. nextgen_returns=[row for row in nextgen_trade_returns if not bool(active.reindex([row.exit_date]).fillna(False).iloc[0])],
  261. micro_returns=micro_trade_rows,
  262. nextgen_weight=1.0,
  263. micro_weight=1.0,
  264. micro_mask=active,
  265. )
  266. add_result(rows, horizon_output, monthly_frames, equity_frames, switch_name, "recent_regime_switch", micro_name, 1.0, 1.0, switch_series, switch_trades)
  267. for micro_weight in (0.25, 0.40):
  268. overlay_returns = nextgen_returns + micro_returns.where(active, 0.0) * micro_weight
  269. overlay_name = f"riskoff-overlay-l{lookback}-m{micro_weight:.2f}-{short_micro_name(micro_name)}"
  270. overlay_series = returns_to_equity(overlay_name, overlay_returns)
  271. overlay_trades = combine_trade_returns(
  272. nextgen_returns=nextgen_trade_returns,
  273. micro_returns=micro_trade_rows,
  274. nextgen_weight=1.0,
  275. micro_weight=micro_weight,
  276. micro_mask=active,
  277. )
  278. add_result(
  279. rows,
  280. horizon_output,
  281. monthly_frames,
  282. equity_frames,
  283. overlay_name,
  284. "riskoff_overlay",
  285. micro_name,
  286. 1.0,
  287. micro_weight,
  288. overlay_series,
  289. overlay_trades,
  290. )
  291. summary = pd.DataFrame(rows).sort_values(
  292. ["net_calmar", "net_annualized_return", "min_recent_total_return", "worst_month_return"],
  293. ascending=[False, False, False, False],
  294. )
  295. horizons = pd.DataFrame(horizon_output)
  296. horizons["horizon"] = pd.Categorical(horizons["horizon"], categories=[label for label, _ in HORIZONS], ordered=True)
  297. horizons = horizons.sort_values(["name", "horizon"])
  298. monthly = pd.concat(monthly_frames, ignore_index=True)
  299. equity = pd.concat(equity_frames, ignore_index=True)
  300. top_names = set(summary.head(25)["name"]) | {NEXTGEN_BASELINE}
  301. return summary, horizons[horizons["name"].isin(top_names)], monthly[monthly["name"].isin(top_names)], equity[equity["name"].isin(top_names)]
  302. def add_result(
  303. rows: list[dict[str, object]],
  304. horizon_output: list[dict[str, object]],
  305. monthly_frames: list[pd.DataFrame],
  306. equity_frames: list[pd.DataFrame],
  307. name: str,
  308. kind: str,
  309. micro_name: str,
  310. nextgen_weight: float,
  311. micro_weight: float,
  312. series: pd.Series,
  313. trades: list[LegReturn],
  314. ) -> None:
  315. row, horizons, monthly = evaluate_portfolio(
  316. name=name,
  317. kind=kind,
  318. series=series,
  319. nextgen_weight=nextgen_weight,
  320. micro_weight=micro_weight,
  321. micro_name=micro_name,
  322. trade_returns=trades,
  323. )
  324. rows.append(row)
  325. horizon_output.extend(horizons)
  326. monthly_frames.append(monthly)
  327. equity_frames.append(equity_frame(name, series))
  328. def equity_frame(name: str, series: pd.Series) -> pd.DataFrame:
  329. return pd.DataFrame({"name": name, "date": series.index.strftime("%Y-%m-%d"), "equity": series.to_numpy()})
  330. def short_micro_name(name: str) -> str:
  331. return name.replace("atr-compress-expand-", "").replace("sl0.008-tp0.016-", "").replace("-", "_")
  332. def markdown_table(frame: pd.DataFrame) -> str:
  333. rows = [list(frame.columns), ["---" for _ in frame.columns]]
  334. rows.extend(frame.astype(object).where(pd.notna(frame), "").values.tolist())
  335. return "\n".join("| " + " | ".join(format_cell(value) for value in row) + " |" for row in rows)
  336. def format_cell(value: object) -> str:
  337. if isinstance(value, float):
  338. return f"{value:.6g}"
  339. return str(value).replace("|", "\\|")
  340. def write_report(
  341. *,
  342. command: str,
  343. output_files: list[Path],
  344. summary: pd.DataFrame,
  345. horizons: pd.DataFrame,
  346. monthly: pd.DataFrame,
  347. ) -> str:
  348. baseline = summary[summary["name"] == NEXTGEN_BASELINE].iloc[0]
  349. best = summary.iloc[0]
  350. best_horizons = horizons[horizons["name"] == best["name"]]
  351. baseline_horizons = horizons[horizons["name"] == NEXTGEN_BASELINE]
  352. top = summary.head(12)
  353. best_months = monthly[monthly["name"] == best["name"]].sort_values("return").head(12)
  354. baseline_months = monthly[monthly["name"] == NEXTGEN_BASELINE].sort_values("return").head(12)
  355. dilution = (
  356. "The best combination improves risk-adjusted return versus nextgen equal-2-c0003."
  357. if float(best["net_calmar"]) > float(baseline["net_calmar"]) and float(best["net_total_return"]) >= float(baseline["net_total_return"]) * 0.98
  358. else "The best combination is mainly return dilution unless its lower drawdown or better worst month is preferred."
  359. )
  360. lines = [
  361. "# ETH nextgen + microstructure portfolio exploration",
  362. "",
  363. f"Run command: `{command}`",
  364. "",
  365. "No order placement or exchange API path is used; this script reads local candle/report data only.",
  366. "",
  367. "Output files:",
  368. *[f"- `{path}`" for path in output_files],
  369. "",
  370. "Base nextgen: `equal-2-c0003` with the two documented maker_taker legs.",
  371. "Micro candidates: ATR compression/expansion US-session robust candidates only.",
  372. "",
  373. "## Conclusion",
  374. "",
  375. dilution,
  376. "",
  377. "## Top combinations",
  378. "",
  379. markdown_table(
  380. top[
  381. [
  382. "name",
  383. "kind",
  384. "micro_name",
  385. "net_total_return",
  386. "net_annualized_return",
  387. "net_max_drawdown",
  388. "net_calmar",
  389. "risk_reward_ratio",
  390. "worst_month",
  391. "worst_month_return",
  392. "min_recent_total_return",
  393. "trades",
  394. "win_rate",
  395. "payoff_ratio",
  396. "profit_factor",
  397. ]
  398. ]
  399. ),
  400. "",
  401. "## Best horizon metrics",
  402. "",
  403. markdown_table(
  404. best_horizons[
  405. [
  406. "horizon",
  407. "horizon_start",
  408. "horizon_end",
  409. "net_total_return",
  410. "net_annualized_return",
  411. "net_max_drawdown",
  412. "net_calmar",
  413. "trades",
  414. "win_rate",
  415. "payoff_ratio",
  416. "profit_factor",
  417. "risk_reward_ratio",
  418. "worst_month",
  419. "worst_month_return",
  420. ]
  421. ]
  422. ),
  423. "",
  424. "## Baseline horizon metrics",
  425. "",
  426. markdown_table(
  427. baseline_horizons[
  428. [
  429. "horizon",
  430. "horizon_start",
  431. "horizon_end",
  432. "net_total_return",
  433. "net_annualized_return",
  434. "net_max_drawdown",
  435. "net_calmar",
  436. "trades",
  437. "win_rate",
  438. "payoff_ratio",
  439. "profit_factor",
  440. "risk_reward_ratio",
  441. "worst_month",
  442. "worst_month_return",
  443. ]
  444. ]
  445. ),
  446. "",
  447. "## Worst months: best combination",
  448. "",
  449. markdown_table(best_months[["month", "return"]]),
  450. "",
  451. "## Worst months: nextgen baseline",
  452. "",
  453. markdown_table(baseline_months[["month", "return"]]),
  454. ]
  455. return "\n".join(lines) + "\n"
  456. def main() -> int:
  457. parser = argparse.ArgumentParser()
  458. parser.add_argument("--output-dir", type=Path, default=OUTPUT_DIR)
  459. args = parser.parse_args()
  460. existing_equity = pd.read_csv(args.output_dir / "eth-btc-nextgen-equity.csv")
  461. base = existing_equity[(existing_equity["cost_model"] == PRIMARY_COST) & (existing_equity["name"] == NEXTGEN_BASELINE)].copy()
  462. if base.empty:
  463. raise KeyError(f"missing existing nextgen equity for {NEXTGEN_BASELINE}")
  464. index = pd.DatetimeIndex(pd.to_datetime(base["date"], utc=True))
  465. nextgen_series, nextgen_returns = load_nextgen(index)
  466. micro_candidates = load_micro_candidates(index)
  467. summary, horizons, monthly, equity = build_portfolios(
  468. nextgen_series=nextgen_series,
  469. nextgen_trade_returns=nextgen_returns,
  470. micro_candidates=micro_candidates,
  471. )
  472. args.output_dir.mkdir(parents=True, exist_ok=True)
  473. summary_path = args.output_dir / f"{PREFIX}-summary.csv"
  474. horizon_path = args.output_dir / f"{PREFIX}-horizons.csv"
  475. monthly_path = args.output_dir / f"{PREFIX}-monthly.csv"
  476. equity_path = args.output_dir / f"{PREFIX}-equity.csv"
  477. json_path = args.output_dir / f"{PREFIX}-top.json"
  478. report_path = args.output_dir / f"{PREFIX}-report.md"
  479. output_files = [summary_path, horizon_path, monthly_path, equity_path, json_path, report_path]
  480. summary.to_csv(summary_path, index=False)
  481. horizons.to_csv(horizon_path, index=False)
  482. monthly.to_csv(monthly_path, index=False)
  483. equity.to_csv(equity_path, index=False)
  484. json_path.write_text(json.dumps(summary.head(20).to_dict("records"), indent=2), encoding="utf-8")
  485. command = f"rtk .venv/bin/python {Path(__file__).as_posix()}"
  486. report_path.write_text(
  487. write_report(command=command, output_files=output_files, summary=summary, horizons=horizons, monthly=monthly),
  488. encoding="utf-8",
  489. )
  490. print(summary.head(12).to_string(index=False))
  491. return 0
  492. if __name__ == "__main__":
  493. raise SystemExit(main())