"""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_exit (reverted to mean) or after max_bars. Stateful, honest at close[i]."""
    logr = np.log(cE / cB)
    s = pd.Series(logr)
    ma = s.rolling(lookback).mean().values
    sd = s.rolling(lookback).std(ddof=0).values
    z = (logr - ma) / sd
    n = len(cB)
    posB = np.zeros(n); posE = np.zeros(n)
    state = 0      # +1 long ratio, -1 short ratio, 0 flat
    bars_in = 0
    for i in range(n):
        if not np.isfinite(z[i]):
            posB[i] = 0.0; posE[i] = 0.0; continue
        if state == 0:
            if z[i] >= 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()
