"""TRACK E — CROSS-SECTIONAL BTC↔ETH relative-value + ENSEMBLE synthesis. Two parts, both on certified Deribit-mainnet data (only BTC/ETH), both honest: PART 1 — RELATIVE VALUE (market-neutral-ish spread trading on TWO assets): * XS relative momentum: go long the stronger asset, short the weaker (dollar-neutral). * ETH/BTC ratio TREND (z-momentum) and ratio MEAN-REVERSION (z-fade of log-ratio). * Lead-lag (descriptive): does BTC's last-bar move predict ETH's next bar (and vice versa)? All positions are decided with data <= close[i] and HELD over the NEXT bar (i->i+1): realized PnL on bar k uses position set at k-1 -> strict 1-bar shift, NO look-ahead. Fees are turnover-based: |Δpos| * fee_rt/2 PER LEG (a +1↔-1 flip = one round trip = fee_rt). PART 2 — ENSEMBLE: Combine the genuinely-positive residual sleeves into ONE portfolio equity curve: (S1) BTC low-turnover ML momentum (trackB best honest cell: W16000 H24 thr0.10, 1h) (S2) Trend-1h, the only cross-asset-robust trend cell from trackA (Donchian N=200 H=12) (S3) the best relative-value sleeve found in PART 1 (if any net-positive OOS) Report combined Sharpe / maxDD / CAGR / EUR-per-day-on-2000 AND the sleeve correlation matrix. A real ensemble edge must be net-positive OOS and LOWER drawdown than its parts. Run: uv run python scripts/research/trackE_xsec_ensemble.py uv run python scripts/research/trackE_xsec_ensemble.py --quick (skip slow ML sleeve) uv run python scripts/research/trackE_xsec_ensemble.py --no-cache (recompute ML proba) """ from __future__ import annotations import argparse import sys import time from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from src.backtest.harness import load, backtest_signals, oos_split # reuse trackB ML machinery (strict walk-forward, no leakage) and trackA donchian from scripts.research.trackB_ml import ( build_features, forward_labels, walk_forward_proba, proba_to_entries, mask_entries, RETRAIN_K, ) from scripts.research.trackA_trend import sig_donchian from sklearn.linear_model import LogisticRegression FEE = 0.001 # 0.10% round-trip baseline (per leg for the pair) BARS_PER_YEAR_1H = 24 * 365.25 # =========================================================================== # Generic honest stats on a per-bar RETURN series (returns realized bar (k-1)->k) # =========================================================================== def equity_from_returns(rets: np.ndarray) -> np.ndarray: eq = np.cumprod(1.0 + np.nan_to_num(rets)) return eq def sharpe(rets: np.ndarray, bpy: float = BARS_PER_YEAR_1H) -> float: r = rets[np.isfinite(rets)] if len(r) < 3 or np.std(r) == 0: return 0.0 return float(np.mean(r) / np.std(r) * np.sqrt(bpy)) def max_dd(equity: np.ndarray) -> float: peak = np.maximum.accumulate(equity) dd = (peak - equity) / peak return float(np.max(dd)) if len(dd) else 0.0 def cagr(equity: np.ndarray, ts: pd.Series) -> float: if len(equity) < 2: return 0.0 days = (ts.iloc[-1] - ts.iloc[0]).total_seconds() / 86400 years = days / 365.25 if days > 0 else 1.0 if years <= 0 or equity[-1] <= 0: return -1.0 return float(equity[-1] ** (1 / years) - 1) def daily_profit(equity: np.ndarray, ts: pd.Series, capital: float = 2000.0) -> float: if len(equity) < 2: return 0.0 days = (ts.iloc[-1] - ts.iloc[0]).total_seconds() / 86400 if days <= 0: return 0.0 final = capital * equity[-1] / equity[0] return (final - capital) / days def yearly_returns(rets: np.ndarray, ts: pd.Series) -> dict: eq = equity_from_returns(rets) s = pd.Series(eq, index=pd.DatetimeIndex(ts)) out = {} for y, g in s.groupby(s.index.year): if len(g) > 1 and g.iloc[0] > 0: out[int(y)] = float(g.iloc[-1] / g.iloc[0] - 1) return out def stat_block(rets: np.ndarray, ts: pd.Series, bpy: float = BARS_PER_YEAR_1H) -> dict: eq = equity_from_returns(rets) return dict( net=float(eq[-1] - 1.0), sharpe=sharpe(rets, bpy), max_dd=max_dd(eq), cagr=cagr(eq, ts), eur_day=daily_profit(eq, ts), equity=eq, turnover=float(np.mean(np.abs(np.diff(np.sign(rets) != 0)))), # placeholder, unused ) # =========================================================================== # RELATIVE-VALUE ENGINE — two legs, turnover-based fees, strict 1-bar shift. # pos arrays are decided at close[i] (data<=i). Realized return on bar k uses pos[k-1]. # =========================================================================== def pair_returns(cB: np.ndarray, cE: np.ndarray, posB: np.ndarray, posE: np.ndarray, fee_rt: float = FEE) -> np.ndarray: """Per-bar net return series for a two-leg book. rets[k] realized on bar (k-1)->k. Fee = (|ΔposB| + |ΔposE|) * fee_rt/2 charged when the position is (re)set.""" n = len(cB) aretB = np.zeros(n); aretE = np.zeros(n) aretB[1:] = cB[1:] / cB[:-1] - 1.0 aretE[1:] = cE[1:] / cE[:-1] - 1.0 rets = np.zeros(n) for k in range(1, n): gross = posB[k - 1] * aretB[k] + posE[k - 1] * aretE[k] pBp = posB[k - 2] if k >= 2 else 0.0 pEp = posE[k - 2] if k >= 2 else 0.0 turn = abs(posB[k - 1] - pBp) + abs(posE[k - 1] - pEp) rets[k] = gross - turn * fee_rt / 2.0 return rets # --- signal builders: return (posB, posE) arrays, leg notional `leg` (gross = 2*leg) --- def xs_momentum(cB, cE, N, hold, leg=0.5): """Cross-sectional momentum: long the asset with higher N-bar return, short the other.""" n = len(cB) posB = np.zeros(n); posE = np.zeros(n) curB = curE = 0.0 for i in range(n): if i >= N and (i % hold == 0): mB = cB[i] / cB[i - N] - 1.0 mE = cE[i] / cE[i - N] - 1.0 d = 1 if mB > mE else -1 # +1 => BTC stronger -> long BTC short ETH curB = leg * d; curE = -leg * d posB[i] = curB; posE[i] = curE return posB, posE def ratio_trend(cB, cE, N, hold, leg=0.5): """Trend on ETH/BTC ratio: ratio rising over N bars -> long ratio (long ETH, short BTC).""" ratio = cE / cB n = len(cB) posB = np.zeros(n); posE = np.zeros(n) curB = curE = 0.0 for i in range(n): if i >= N and (i % hold == 0): d = 1 if ratio[i] > ratio[i - N] else -1 # +1 => ratio up -> long ratio curE = leg * d; curB = -leg * d posB[i] = curB; posE[i] = curE return posB, posE def ratio_meanrev(cB, cE, lookback, z_in, z_exit, max_bars, leg=0.5): """Mean-reversion (z-fade) on log(ETH/BTC). z>+z_in -> short ratio; z<-z_in -> long ratio. Exit when |z|= z_in: state = -1; bars_in = 0 # ratio too high -> short ratio elif z[i] <= -z_in: state = 1; bars_in = 0 # ratio too low -> long ratio else: bars_in += 1 if abs(z[i]) <= z_exit or bars_in >= max_bars or (state == 1 and z[i] >= z_in) \ or (state == -1 and z[i] <= -z_in): state = 0 posE[i] = leg * state; posB[i] = -leg * state return posB, posE # =========================================================================== # OOS / fee-sweep helpers for the relative-value sleeves # =========================================================================== def rv_eval(cB, cE, ts, build_fn, params, fee_rt=FEE, frac=0.65): posB, posE = build_fn(cB, cE, **params) rets = pair_returns(cB, cE, posB, posE, fee_rt=fee_rt) cut = int(len(cB) * frac) full = stat_block(rets, ts) is_ = stat_block(rets[:cut], ts.iloc[:cut]) oos = stat_block(rets[cut:], ts.iloc[cut:]) # turnover: average per-bar leg turnover (both legs) turn = (np.abs(np.diff(posB, prepend=0)) + np.abs(np.diff(posE, prepend=0))) tstats = dict(rets=rets, posB=posB, posE=posE, trades=int((turn > 1e-9).sum()), avg_turn=float(turn.mean())) return full, is_, oos, tstats def fmt(s): return (f"net={s['net']*100:>+8.0f}% Sh={s['sharpe']:>+5.2f} DD={s['max_dd']*100:>4.0f}% " f"CAGR={s['cagr']*100:>+6.1f}% €/d={s['eur_day']:>+6.2f}") # =========================================================================== # PART 1 # =========================================================================== def part1_relative_value(quick=False): print("=" * 104) print("PART 1 — CROSS-SECTIONAL / RELATIVE-VALUE (BTC↔ETH, 1h, market-neutral spread)") print("=" * 104) b = load("BTC", "1h"); e = load("ETH", "1h") m = pd.merge(b[["timestamp", "close"]], e[["timestamp", "close"]], on="timestamp", suffixes=("_b", "_e")).reset_index(drop=True) ts = pd.to_datetime(m["timestamp"], unit="ms", utc=True) cB = m["close_b"].to_numpy(float); cE = m["close_e"].to_numpy(float) cut = int(len(m) * 0.65) print(f" common 1h bars: {len(m)} {ts.iloc[0].date()} → {ts.iloc[-1].date()} " f"(OOS starts {ts.iloc[cut].date()})") rb = np.log(cB[1:] / cB[:-1]); re = np.log(cE[1:] / cE[:-1]) print(f" contemporaneous corr(BTC,ETH 1h logret) = {np.corrcoef(rb, re)[0,1]:.3f} " f"(very high → the only tradable structure is the SPREAD)") # ---- LEAD-LAG (descriptive, both directions, IS vs OOS) ---- print("\n -- LEAD-LAG (descriptive: does last-bar move of X predict next bar of Y?) --") def ll(a_prev, b_next): a = a_prev[np.isfinite(a_prev) & np.isfinite(b_next)] bb = b_next[np.isfinite(a_prev) & np.isfinite(b_next)] return np.corrcoef(a, bb)[0, 1] if len(a) > 30 else np.nan print(f" corr(rB[i], rE[i+1]) = {ll(rb[:-1], re[1:]):+.4f} " f"corr(rE[i], rB[i+1]) = {ll(re[:-1], rb[1:]):+.4f}") print(f" corr(rB[i], rB[i+1]) = {ll(rb[:-1], rb[1:]):+.4f} " f"corr(rE[i], rE[i+1]) = {ll(re[:-1], re[1:]):+.4f}") print(" → |lead-lag| ~0.01-0.02: NO exploitable cross-predictive edge. Not pursued as a sleeve.") results = {} # ---- A) XS relative momentum grid ---- print("\n -- (A) XS RELATIVE MOMENTUM: long stronger / short weaker (dollar-neutral, gross=1) --") print(" param FULL | OOS") Ns = [24, 72, 168, 336] if not quick else [72, 168] holds = [6, 24, 72] if not quick else [24, 72] best_xs = None for N in Ns: for hold in holds: full, is_, oos, tstat = rv_eval(cB, cE, ts, xs_momentum, dict(N=N, hold=hold)) ok = oos["net"] > 0 and oos["sharpe"] > 0 print(f" N={N:>3} hold={hold:>2} | {fmt(full)} | OOS {fmt(oos)} " f"tr={tstat['trades']:>4} {'OK' if ok else ''}") if oos["net"] > 0 and (best_xs is None or oos["sharpe"] > best_xs[2]["sharpe"]): best_xs = (dict(N=N, hold=hold), full, oos, tstat, "xs_momentum") results["xs_momentum"] = best_xs # ---- B) ETH/BTC ratio TREND grid ---- print("\n -- (B) ETH/BTC RATIO TREND: long ratio when rising over N (long ETH/short BTC) --") print(" NOTE: with only TWO assets this is ALGEBRAICALLY IDENTICAL to (A) — 'long the") print(" stronger' ≡ 'trade the ratio trend'. Shown separately only to make that explicit.") best_rt = None for N in Ns: for hold in holds: full, is_, oos, tstat = rv_eval(cB, cE, ts, ratio_trend, dict(N=N, hold=hold)) ok = oos["net"] > 0 and oos["sharpe"] > 0 print(f" N={N:>3} hold={hold:>2} | {fmt(full)} | OOS {fmt(oos)} " f"tr={tstat['trades']:>4} {'OK' if ok else ''}") if oos["net"] > 0 and (best_rt is None or oos["sharpe"] > best_rt[2]["sharpe"]): best_rt = (dict(N=N, hold=hold), full, oos, tstat, "ratio_trend") results["ratio_trend"] = best_rt # ---- C) ETH/BTC ratio MEAN-REVERSION grid ---- print("\n -- (C) ETH/BTC RATIO MEAN-REVERSION: z-fade of log(ETH/BTC) --") best_mr = None LBs = [48, 168, 336] if not quick else [168] zins = [1.5, 2.0, 2.5] if not quick else [2.0] for lb in LBs: for zin in zins: full, is_, oos, tstat = rv_eval(cB, cE, ts, ratio_meanrev, dict(lookback=lb, z_in=zin, z_exit=0.5, max_bars=72)) ok = oos["net"] > 0 and oos["sharpe"] > 0 print(f" lb={lb:>3} zin={zin} | {fmt(full)} | OOS {fmt(oos)} " f"tr={tstat['trades']:>4} {'OK' if ok else ''}") if oos["net"] > 0 and (best_mr is None or oos["sharpe"] > best_mr[2]["sharpe"]): best_mr = (dict(lookback=lb, z_in=zin, z_exit=0.5, max_bars=72), full, oos, tstat, "ratio_meanrev") results["ratio_meanrev"] = best_mr # ---- choose the single best RV sleeve (positive OOS, highest OOS Sharpe) ---- cands = [v for v in results.values() if v is not None] cands.sort(key=lambda v: v[2]["sharpe"], reverse=True) best = cands[0] if cands else None print("\n -- RELATIVE-VALUE SUMMARY (best per family that is OOS net-positive) --") for fam in ("xs_momentum", "ratio_trend", "ratio_meanrev"): v = results[fam] if v is None: print(f" {fam:<14}: no OOS net-positive cell.") else: params, full, oos, tstat, _ = v print(f" {fam:<14}: {params} FULL {fmt(full)} | OOS {fmt(oos)}") if best is None: print("\n >> NO relative-value sleeve is OOS net-positive. No RV edge to add to the ensemble.") return None, (cB, cE, ts) params, full, oos, tstat, fam = best print(f"\n >> BEST RV sleeve: {fam} {params} (OOS Sharpe {oos['sharpe']:+.2f})") # ---- per-year + fee sweep + grid-neighbourhood robustness on the winner ---- build_fn = {"xs_momentum": xs_momentum, "ratio_trend": ratio_trend, "ratio_meanrev": ratio_meanrev}[fam] fullr, _, _, _ = rv_eval(cB, cE, ts, build_fn, params) print("\n per-year (full):") yr = yearly_returns(fullr["rets"] if False else pair_returns(cB, cE, *build_fn(cB, cE, **params)), ts) for y in sorted(yr): print(f" {y}: {yr[y]*100:>+7.1f}%") print("\n fee sweep (full-sample net, baseline 0.10% RT/leg):") for f in (0.0, 0.0005, 0.001, 0.0015, 0.002): fr, _, fo, _ = rv_eval(cB, cE, ts, build_fn, params, fee_rt=f) print(f" fee={f*1000:.1f}bp/leg → FULL net={fr['net']*100:>+7.0f}% " f"OOS net={fo['net']*100:>+7.0f}% (Sh {fo['sharpe']:+.2f})") return best, (cB, cE, ts) # =========================================================================== # PART 2 — ENSEMBLE # =========================================================================== def lr_factory(): return LogisticRegression(C=1.0, max_iter=300, class_weight="balanced") def ml_sleeve_btc(cache=True, no_cache=False): """BTC low-turnover ML momentum sleeve (trackB best honest cell W16000 H24 thr0.10).""" W, H, thr = 16000, 24, 0.10 df = load("BTC", "1h") cpath = Path(__file__).resolve().parent / ".cache_trackE_btc_ml_proba.npy" proba = None if cache and not no_cache and cpath.exists(): arr = np.load(cpath) if len(arr) == len(df): proba = arr print(f" [S1 ML] loaded cached proba ({cpath.name})") if proba is None: print(f" [S1 ML] walk-forward LogisticRegression W{W} H{H} (slow ~1-2min)...") t0 = time.time() X, names, fvalid = build_features(df) warmup = int(np.argmax(fvalid)) if fvalid.any() else 0 y, _fwd, lvalid = forward_labels(df, H) proba = walk_forward_proba(X, y, fvalid, lvalid, warmup, W, H, RETRAIN_K, lr_factory) np.save(cpath, proba) print(f" [S1 ML] done ({time.time()-t0:.0f}s), cached.") n = len(df) entries = proba_to_entries(proba, thr, H, n) m = backtest_signals(df, entries, fee_rt=FEE, asset="BTC", tf="1h") return m, df, f"BTC-ML W{W}H{H}thr{thr}" def trend_sleeve_btc(): """Trend-1h sleeve: Donchian N=200 H=12 on BTC (the only cross-asset-robust trend cell).""" df = load("BTC", "1h") entries = sig_donchian(df, lookback=200, hold=12) m = backtest_signals(df, entries, fee_rt=FEE, asset="BTC", tf="1h") return m, df, "BTC-Trend Donchian200/12" def metrics_to_returns(m): """Per-bar return series from a harness Metrics equity, indexed by its timestamps.""" eq = m.equity.astype(float) ts = m.eq_index rets = np.zeros(len(eq)) rets[1:] = eq[1:] / np.where(eq[:-1] == 0, np.nan, eq[:-1]) - 1.0 rets = np.nan_to_num(rets) return pd.Series(rets, index=pd.DatetimeIndex(ts)) def part2_ensemble(rv_best, rv_data, quick=False, no_cache=False): print("\n" + "=" * 104) print("PART 2 — ENSEMBLE (combine weakly-correlated residual sleeves into one portfolio)") print("=" * 104) sleeves = {} # name -> pd.Series of per-bar returns indexed by ts # S2 trend (fast, always) mt, dft, tname = trend_sleeve_btc() sleeves["S2_trend"] = metrics_to_returns(mt) print(f" [S2] {tname:<28} net={mt.net_return*100:>+7.0f}% Sh={mt.sharpe:+.2f} " f"DD={mt.max_dd*100:.0f}% €/d={mt.daily_profit(2000):+.2f}") # S3 relative value (from PART 1) if rv_best is not None: params, full, oos, tstat, fam = rv_best cB, cE, ts = rv_data build_fn = {"xs_momentum": xs_momentum, "ratio_trend": ratio_trend, "ratio_meanrev": ratio_meanrev}[fam] posB, posE = build_fn(cB, cE, **params) rv_rets = pair_returns(cB, cE, posB, posE, fee_rt=FEE) sleeves["S3_relval"] = pd.Series(rv_rets, index=pd.DatetimeIndex(ts)) print(f" [S3] RV {fam} {params} net={full['net']*100:>+7.0f}% " f"Sh={full['sharpe']:+.2f} DD={full['max_dd']*100:.0f}% €/d={full['eur_day']:+.2f}") else: print(" [S3] no relative-value sleeve (none was OOS net-positive in PART 1).") # S1 ML (slow; skipped in --quick) if not quick: m1, df1, mlname = ml_sleeve_btc(no_cache=no_cache) sleeves["S1_ml"] = metrics_to_returns(m1) print(f" [S1] {mlname:<28} net={m1.net_return*100:>+7.0f}% Sh={m1.sharpe:+.2f} " f"DD={m1.max_dd*100:.0f}% €/d={m1.daily_profit(2000):+.2f}") else: print(" [S1] ML sleeve SKIPPED (--quick).") # ---- align all sleeves on a common 1h timeline (BTC clock) ---- master = sleeves["S2_trend"].index aligned = pd.DataFrame(index=master) for name, s in sleeves.items(): aligned[name] = s.reindex(master).fillna(0.0) # the portfolio is only meaningful where the slowest sleeve is live. # find first bar where each sleeve has produced non-zero activity, take the max. starts = {} for name in aligned.columns: nz = np.nonzero(aligned[name].to_numpy() != 0.0)[0] starts[name] = nz[0] if len(nz) else len(aligned) start = max(starts.values()) aligned = aligned.iloc[start:] ts_a = pd.Series(aligned.index) print(f"\n Common active window: {aligned.index[0].date()} → {aligned.index[-1].date()} " f"({len(aligned)} bars). Sleeves: {list(aligned.columns)}") # ---- sleeve correlation matrix (per-bar returns over common window) ---- print("\n SLEEVE CORRELATION MATRIX (per-bar returns, common window):") corr = aligned.corr() cols = list(aligned.columns) print(" " + "".join(f"{c:>10}" for c in cols)) for c in cols: print(f" {c:>9} " + "".join(f"{corr.loc[c, c2]:>+10.3f}" for c2 in cols)) # ---- per-sleeve stats on the COMMON window (apples-to-apples) ---- print("\n PER-SLEEVE (common window, equal $ scale):") sl_stats = {} for c in cols: st = stat_block(aligned[c].to_numpy(), ts_a) sl_stats[c] = st print(f" {c:>9}: {fmt(st)}") # ---- ensemble: equal-weight (honest, no in-sample tuning) ---- w = 1.0 / len(cols) ens_eq_w = aligned.to_numpy() @ (np.ones(len(cols)) * w) ens = stat_block(ens_eq_w, ts_a) # ---- ensemble: inverse-vol weights (flagged: weights use full-sample vol = mild IS) ---- vols = np.array([np.std(aligned[c].to_numpy()) for c in cols]) iv = (1.0 / np.where(vols == 0, np.nan, vols)) iv = np.nan_to_num(iv); iv = iv / iv.sum() ens_iv = stat_block(aligned.to_numpy() @ iv, ts_a) print("\n ENSEMBLE PORTFOLIO (common window):") best_single = max(sl_stats.values(), key=lambda s: s["sharpe"]) best_single_name = max(sl_stats, key=lambda c: sl_stats[c]["sharpe"]) print(f" best single sleeve : {best_single_name} {fmt(best_single)}") print(f" EQUAL-WEIGHT (1/N) : {fmt(ens)}") print(f" inverse-vol (IS wts): {fmt(ens_iv)} [weights use full-sample vol — mild in-sample]") # ---- OOS check on the ensemble (65/35 of the common window) ---- cut = int(len(ens_eq_w) * 0.65) ens_is = stat_block(ens_eq_w[:cut], ts_a.iloc[:cut]) ens_oos = stat_block(ens_eq_w[cut:], ts_a.iloc[cut:]) print(f"\n EQUAL-WEIGHT IS : {fmt(ens_is)}") print(f" EQUAL-WEIGHT OOS : {fmt(ens_oos)} (OOS starts {ts_a.iloc[cut].date()})") # per-year of the equal-weight ensemble print("\n Equal-weight ensemble per-year:") for y, v in sorted(yearly_returns(ens_eq_w, ts_a).items()): print(f" {y}: {v*100:>+7.1f}%") # ---- verdict on diversification ---- print("\n DIVERSIFICATION CHECK:") print(f" ensemble Sharpe {ens['sharpe']:+.2f} vs best single {best_single['sharpe']:+.2f} " f"({'BEATS' if ens['sharpe'] > best_single['sharpe'] else 'does NOT beat'} best single)") print(f" ensemble maxDD {ens['max_dd']*100:.0f}% vs best single {best_single['max_dd']*100:.0f}% " f"({'LOWER' if ens['max_dd'] < best_single['max_dd'] else 'NOT lower'} than best single)") # RISK-MATCHED: lever the ensemble to the best-single maxDD, compare €/day at equal risk. # (Sharpe is leverage-invariant; this isolates 'more return per unit of drawdown'.) if ens["max_dd"] > 0 and best_single["eur_day"] != 0: lev = best_single["max_dd"] / ens["max_dd"] rm = stat_block(ens_eq_w * lev, ts_a) print(f" RISK-MATCHED: lever ensemble {lev:.2f}x to ~{best_single['max_dd']*100:.0f}% DD " f"→ €/d={rm['eur_day']:+.2f} (DD {rm['max_dd']*100:.0f}%) vs best-single €/d={best_single['eur_day']:+.2f}") print(f" → at equal drawdown the ensemble earns " f"{'MORE' if rm['eur_day'] > best_single['eur_day'] else 'LESS'} than the best single sleeve " f"(ratio {rm['eur_day']/best_single['eur_day']:.2f}); this tracks the Sharpe ratio.") if ens["eur_day"] > 0: print(f" ensemble €/day(2k) {ens['eur_day']:+.2f} vs target ~50.00 " f"→ ~{(50.0/ens['eur_day']):.0f}x short of the goal.") else: print(" ensemble €/day(2k) <= 0 → no earning engine.") return ens, sl_stats, corr # =========================================================================== def main(): ap = argparse.ArgumentParser() ap.add_argument("--quick", action="store_true", help="skip slow ML sleeve + smaller RV grid") ap.add_argument("--no-cache", action="store_true", help="recompute ML walk-forward proba") args = ap.parse_args() t0 = time.time() rv_best, rv_data = part1_relative_value(quick=args.quick) part2_ensemble(rv_best, rv_data, quick=args.quick, no_cache=args.no_cache) print(f"\n(elapsed {time.time()-t0:.0f}s)") print("\n" + "=" * 104) print("See docs/diary/2026-06-19-trackE-xsec-ensemble.md for the full honest write-up.") print("=" * 104) if __name__ == "__main__": main()