"""READ-ONLY evaluator for a NEW edge: the Bybit-vs-Deribit CROSS-VENUE FUNDING-CARRY (delta-neutral level
harvest).

Economic thesis (distinct from the FAILED cross-venue cross-SECTIONAL dispersion probe)
-----------------------------------------------------------------------------------------
Bybit perp funding runs PERSISTENTLY higher than Deribit on the liquid majors (probed: Bybit BTC avg|f|
~0.0004 vs Deribit ~0.000276 — retail-heavy Bybit pays more). A delta-neutral cross-venue carry harvests
that LEVEL:

  * SHORT the Bybit perp (collect its higher funding) / LONG the Deribit perp (pay its lower funding) on the
    SAME coin. The two perps track the same index, so price risk ~cancels; the return ≈ the funding SPREAD
    `spread = bybit_funding − deribit_funding` (minus costs and a small perp-basis-divergence term we don't
    model here — we have no Deribit price panel, so the eval books the realized funding spread, the dominant
    term, and is HONEST that the basis-divergence residual is omitted).

Unlike the failed cross-venue probe (cross-sectional dispersion on ILLIQUID alts = a fee-trap), this is a funding-LEVEL
carry on LIQUID MAJORS → low turnover (a persistent level, not a churning cross-section). It is also distinct
from `xsfunding` (which trades the cross-section WITHIN Bybit): here Deribit is the reference venue and the
signal is the BETWEEN-venue level difference per coin.

Two modes (both tested, like the xvenue/positioning evals test both directions):
  * STATIC  — always SHORT-Bybit/LONG-Deribit: harvest the persistent positive spread. Position is +1 on the
    spread every day → turnover is ~0 after entry (low cost). Per-coin daily carry = `spread`.
  * DYNAMIC — position by the SIGN of the (causal) spread: `pos = sign(spread)`. Per-coin daily carry =
    `pos · spread`. Flips on every sign change → per-flip turnover (cost bites more than static).

Equal-weight across the overlap coins (coins with BOTH a Bybit `funding` and a `deribit_funding` panel). The
breadth is intrinsically LOW (2–3 coins: BTC/ETH, maybe SOL) — `n_coins` is reported and a 2-coin Sharpe is
flagged as noisy.

Reuse (no return-engine reimplemented)
--------------------------------------
The standard `_curve_metrics` (Sharpe = mean/pstdev × √365, CAGR over the date span, maxDD off the compounded
curve) wraps the day series. The adversarial verify mirrors the xvenue/positioning verify shape: both modes
net @ a cost grid, per-coin attribution, per-year Sharpe, turnover, AND the correlation of THIS edge's
returns with EACH of the 4 existing edges (tstrend, unlock, xsfunding, positioning) via
`sleeve_returns_from_store`. The walk-forward OOS gate mirrors `walk_forward_positioning`.

READ-ONLY + memory-bounded
--------------------------
No PaperRepo, no `store.write_*` — the live paper_* tables are never touched. Reads are restricted to the
overlap coins (∩ optional `universe`) via `_UniverseRestrictedStore`, so the full all-symbol panels are never
materialised: each feature is read with a `WHERE symbol IN (...)` over the bounded symbol set.
"""
from __future__ import annotations

import datetime as dt
import math
import statistics as st
from typing import Any, Protocol

from fxhnt.application.bybit_book_eval import (
    _UniverseRestrictedStore,
    sleeve_returns_from_store,
)
from fxhnt.application.bybit_carry_revalidate import _curve_metrics

_PERIODS_PER_YEAR = 365
_EXISTING_EDGES = ("tstrend", "unlock", "xsfunding", "positioning")
_MODES = ("static", "dynamic")

# The SMALL, documented gated-static (L, threshold) grid. Picked on TRAIN only in the walk-forward — a small
# grid keeps the search-overfitting bounded (a huge grid = overfitting by search). L = trailing-lookback days
# for the persistence score; threshold = the per-day mean-spread floor a coin must clear to be INCLUDED.
_GATED_L_GRID = (30, 60, 90)
_GATED_THRESHOLD_GRID = (0.0, 5e-5, 1e-4, 2e-4)
# The standalone (non-walk-forward) gated path uses the mid-grid as a documented default; the HONEST gated
# number is the walk-forward OOS one (TRAIN picks (L, threshold)), NOT this convenience default.
_GATED_DEFAULT_L = 60
_GATED_DEFAULT_THRESHOLD = 5e-5


class _FeatureStore(Protocol):
    """Just the panel reads the cross-venue carry needs (TimescaleFeatureStore satisfies this). READ-ONLY."""

    def crypto_funding_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def read_panel(self, symbols: list[str], feature: str) -> dict[str, dict[int, float]]: ...


def _epoch_day_to_iso(day: int) -> str:
    return (dt.date(1970, 1, 1) + dt.timedelta(days=int(day))).isoformat()


def _year_of_day(day: int) -> str:
    return str((dt.date(1970, 1, 1) + dt.timedelta(days=int(day))).year)


# --- 1. the cross-venue funding spread ----------------------------------------------------------

def carry_spread(bybit_funding_panel: dict[str, dict[int, float]],
                 deribit_funding_panel: dict[str, dict[int, float]],
                 ) -> dict[int, dict[str, float]]:
    """`{day: {coin: spread}}` where `spread = bybit_funding − deribit_funding`, aligned by (coin, day).

    Only coins present on BOTH venues are considered (the tradeable overlap breadth), and only (coin, day)
    pairs where BOTH venues have a funding observation contribute — a coin/day on just one venue has no
    spread. Day-major so the per-day per-coin sizing consumes it directly. Pure; no I/O."""
    shared = set(bybit_funding_panel) & set(deribit_funding_panel)
    out: dict[int, dict[str, float]] = {}
    for coin in shared:
        by = bybit_funding_panel[coin]
        de = deribit_funding_panel[coin]
        for day in set(by) & set(de):
            out.setdefault(int(day), {})[coin] = float(by[day]) - float(de[day])
    return out


# --- 2. the per-coin position (static vs dynamic) -----------------------------------------------

def _position(spread: float, *, mode: str) -> float:
    """The delta-neutral position on the funding spread for a coin/day. STATIC = always +1 (short-Bybit /
    long-Deribit, harvest the persistent positive level). DYNAMIC = sign(spread) (flip to long-Bybit /
    short-Deribit when the spread goes negative). A zero spread → zero position (dynamic). Pure."""
    if mode == "static":
        return 1.0
    if mode == "dynamic":
        return 1.0 if spread > 0.0 else (-1.0 if spread < 0.0 else 0.0)
    raise ValueError(f"mode must be 'static' or 'dynamic', got {mode!r}")


# --- 2b. the CAUSAL persistence gate (the disciplined "gated-static" coin selector) --------------
#
# The gate is a SMARTER STATIC, NOT dynamic timing. For each coin i and each day t it computes a CAUSAL
# persistence score from data STRICTLY ≤ t-1, then INCLUDES coin i in the equal-weight short-Bybit/long-Deribit
# basket on day t ONLY if that score clears `threshold`. Coins enter/exit SLOWLY as their trailing persistence
# crosses the threshold (a static-like, low-turnover gate — not per-day sign flips). Position on an included
# coin is always +1 (short-Bybit/long-Deribit) — we NEVER flip to long-Bybit; we only choose WHICH coins to
# harvest. The goal: keep the persistent-premium coins (BTC/ETH structural retail-crowding premium) and drop
# the alts whose spreads flip (no persistent premium), WITHOUT look-ahead.
#
# PERSISTENCE MEASURE (documented, robust): the TRAILING MEAN of the coin's funding spread over the window
# [t-L, t-1] (i.e. the L most recent days STRICTLY BEFORE t). A coin with a persistently positive Bybit>Deribit
# premium has a high trailing mean; a coin whose spread flips averages out near zero. The mean (not a t-stat)
# is deliberately simple/robust and is in the same units as `threshold` (a per-day funding-rate floor). The
# window requires at least 1 prior observation; days before any history exist for a coin → that coin is NOT
# included (no score → excluded, never a look-ahead default-in).


def persistence_score(coin_spread: dict[int, float], day: int, *, lookback: int) -> float | None:
    """CAUSAL trailing-mean persistence score for ONE coin at day `t=day`, using ONLY that coin's spreads on
    days in `[t-lookback, t-1]` (STRICTLY before t). Returns the mean spread over the prior-window observations
    present, or `None` when the coin has NO observation in the window (→ cannot be scored → must be excluded).

    Strict causality: day `t` itself is NEVER read (the `< day` bound), so a spread that turns positive only AT
    `t` cannot pull the score up at `t`. Pure; no I/O."""
    lo = day - int(lookback)
    prior = [v for d, v in coin_spread.items() if lo <= d < day]
    if not prior:
        return None
    return sum(prior) / len(prior)


def _gate_included(coin_spread: dict[int, float], day: int, *, lookback: int, threshold: float) -> bool:
    """True iff coin (given its full `{day: spread}` series) is INCLUDED in the gated basket on `day`: it has a
    causal persistence score over [day-lookback, day-1] AND that score `>= threshold`. A coin with no prior
    window (score None) or a sub-threshold trailing mean is EXCLUDED. Strictly causal (uses only days < day)."""
    score = persistence_score(coin_spread, day, lookback=lookback)
    return score is not None and score >= threshold


# --- 3. the read-only, memory-bounded per-day book breakdown ------------------------------------

def _overlap_universe(store: _FeatureStore, universe: set[str] | None) -> set[str]:
    """Coins with BOTH a Bybit `funding` and a `deribit_funding` panel (the cross-venue overlap), ∩ optional
    `universe`. This is the memory bound: the two funding-panel reads here are funding-only (one float per
    (coin,day), the smallest feature) and learn the overlap; everything else is read bounded to it."""
    bybit_coins = set(store.crypto_funding_panel())
    # deribit_funding shares the bybit_features table; read its symbol set via the panel for the bybit coins.
    der = store.read_panel(sorted(bybit_coins), "deribit_funding") if bybit_coins else {}
    deribit_coins = {s for s, v in der.items() if v}
    overlap = bybit_coins & deribit_coins
    if universe is not None:
        overlap &= universe
    return overlap


def _book_breakdown(store: _FeatureStore, *, universe: set[str] | None, cost_bps: float,
                    mode: str, day_lo: int | None = None, day_hi: int | None = None) -> dict[str, Any]:
    """READ-ONLY, memory-bounded per-day book breakdown for the cross-venue carry edge in ONE pass.

    Returns:
      * `net`      — `{day: net_ret}` the equal-weight book day return AFTER the per-turnover cost haircut;
      * `gross`    — `{day: gross_ret}` the same day return BEFORE cost (per-coin attribution is cost-free);
      * `per_coin` — `{coin: Σ_day posᵢ·spreadᵢ / n}` each coin's TOTAL gross contribution (Σ == Σ gross);
      * `turnover` — `{day: Σᵢ|posᵢ_d − posᵢ_prev| / n}` the daily fractional turnover (cost driver).

    Equal-weight across the overlap coins (each coin's carry = `posᵢ · spreadᵢ`, position from `mode`). The
    spread on day d is the CAUSAL funding spread realized over day d; the position is held into d's carry — a
    static book never flips (turnover ~0 after entry), a dynamic book flips on the sign changes. Costs are
    re-derived from the actual positions (round-trip both venues per unit |Δpos|), NOT negated. The store is
    read through `_UniverseRestrictedStore` (memory bound). Writes NOTHING."""
    overlap = _overlap_universe(store, universe)
    empty: dict[str, Any] = {"net": {}, "gross": {}, "per_coin": {}, "turnover": {}}
    if not overlap:
        return empty
    bounded = _UniverseRestrictedStore(store, overlap)

    syms = sorted(overlap)
    bybit_funding = {s: v for s, v in bounded.read_panel(syms, "funding").items() if v}
    deribit_funding = {s: v for s, v in bounded.read_panel(syms, "deribit_funding").items() if v}
    spread = carry_spread(bybit_funding, deribit_funding)
    if not spread:
        return empty

    net: dict[int, float] = {}
    gross_s: dict[int, float] = {}
    turnover_s: dict[int, float] = {}
    per_coin: dict[str, float] = {}
    prev_pos: dict[str, float] = {}
    for day in sorted(spread):
        if (day_lo is not None and day < day_lo) or (day_hi is not None and day > day_hi):
            continue
        row = spread[day]
        n = len(row)
        if n == 0:
            prev_pos = {}
            continue
        pos = {c: _position(s, mode=mode) for c, s in row.items()}
        book_gross = sum(pos[c] * row[c] for c in row) / n
        turnover = sum(abs(pos.get(c, 0.0) - prev_pos.get(c, 0.0))
                       for c in set(pos) | set(prev_pos)) / n
        book_net = book_gross - (turnover * (cost_bps / 1e4) if cost_bps else 0.0)
        if math.isfinite(book_net):
            net[int(day)] = float(book_net)
            gross_s[int(day)] = float(book_gross)
            turnover_s[int(day)] = float(turnover)
            for c in row:
                per_coin[c] = per_coin.get(c, 0.0) + (pos[c] * row[c]) / n
        prev_pos = pos
    return {"net": net, "gross": gross_s, "per_coin": per_coin, "turnover": turnover_s}


def _load_spread(store: _FeatureStore, universe: set[str] | None) -> dict[int, dict[str, float]]:
    """READ-ONLY, memory-bounded `{day: {coin: spread}}` for the overlap (∩ universe). Reads `funding` +
    `deribit_funding` bounded to the overlap via `_UniverseRestrictedStore`. Empty `{}` when no overlap."""
    overlap = _overlap_universe(store, universe)
    if not overlap:
        return {}
    bounded = _UniverseRestrictedStore(store, overlap)
    syms = sorted(overlap)
    bybit_funding = {s: v for s, v in bounded.read_panel(syms, "funding").items() if v}
    deribit_funding = {s: v for s, v in bounded.read_panel(syms, "deribit_funding").items() if v}
    return carry_spread(bybit_funding, deribit_funding)


def _coin_major_spread(spread: dict[int, dict[str, float]]) -> dict[str, dict[int, float]]:
    """Re-key a day-major `{day: {coin: spread}}` to coin-major `{coin: {day: spread}}` (the persistence score
    needs each coin's own trailing history). Pure."""
    out: dict[str, dict[int, float]] = {}
    for day, row in spread.items():
        for coin, s in row.items():
            out.setdefault(coin, {})[day] = s
    return out


def gated_book_from_spread(spread: dict[int, dict[str, float]], *, lookback: int, threshold: float,
                           cost_bps: float, day_lo: int | None = None, day_hi: int | None = None,
                           ) -> dict[str, Any]:
    """PURE gated-static book over a `{day: {coin: spread}}` panel — the disciplined coin-selection STATIC.

    Each day t, INCLUDE coin i (position +1, short-Bybit/long-Deribit) ONLY if its CAUSAL trailing-mean
    persistence over [t-lookback, t-1] is `>= threshold`; equal-weight across the INCLUDED set; FLAT (zero
    return, zero turnover carry) on days with no included coin. Turnover = Σ|incl_d − incl_prev|/n_incl_d on the
    inclusion indicator (coins enter/exit slowly as trailing persistence crosses the threshold — a low-turnover
    static gate, NOT per-day sign flips). Cost charged on that entry/exit turnover @ `cost_bps`. Strictly
    causal: the score never reads day t. `inclusion` counts per-coin included days (the realized selected-set).

    Returns `{net, gross, per_coin, turnover, inclusion, included_days}`. Pure; no I/O."""
    coin_major = _coin_major_spread(spread)
    net: dict[int, float] = {}
    gross_s: dict[int, float] = {}
    turnover_s: dict[int, float] = {}
    per_coin: dict[str, float] = {}
    inclusion: dict[str, int] = {}
    included_days = 0
    prev_incl: set[str] = set()
    for day in sorted(spread):
        if (day_lo is not None and day < day_lo) or (day_hi is not None and day > day_hi):
            continue
        row = spread[day]
        incl = {c for c in row if _gate_included(coin_major.get(c, {}), day,
                                                 lookback=lookback, threshold=threshold)}
        n = len(incl)
        # Turnover is the fractional change in the INCLUDED set; normalise by today's basket size (or the prev
        # basket size when today is flat, so an all-exit day still books its exit cost).
        denom = n if n > 0 else (len(prev_incl) if prev_incl else 1)
        turnover = len(incl.symmetric_difference(prev_incl)) / denom
        book_gross = (sum(row[c] for c in incl) / n) if n > 0 else 0.0
        book_net = book_gross - (turnover * (cost_bps / 1e4) if cost_bps else 0.0)
        if math.isfinite(book_net):
            net[int(day)] = float(book_net)
            gross_s[int(day)] = float(book_gross)
            turnover_s[int(day)] = float(turnover)
            if n > 0:
                included_days += 1
            for c in incl:
                per_coin[c] = per_coin.get(c, 0.0) + row[c] / n
                inclusion[c] = inclusion.get(c, 0) + 1
        prev_incl = incl
    return {"net": net, "gross": gross_s, "per_coin": per_coin, "turnover": turnover_s,
            "inclusion": inclusion, "included_days": included_days}


def _gated_book_breakdown(store: _FeatureStore, *, universe: set[str] | None, cost_bps: float,
                          lookback: int, threshold: float, day_lo: int | None = None,
                          day_hi: int | None = None) -> dict[str, Any]:
    """Store-backed gated-static book (READ-ONLY, memory-bounded). Loads the bounded spread panel then delegates
    to the pure `gated_book_from_spread`. Same `{net, gross, per_coin, turnover, inclusion, included_days}`
    shape as the static/dynamic `_book_breakdown` (plus the gate's inclusion accounting). Writes NOTHING."""
    spread = _load_spread(store, universe)
    if not spread:
        return {"net": {}, "gross": {}, "per_coin": {}, "turnover": {}, "inclusion": {}, "included_days": 0}
    return gated_book_from_spread(spread, lookback=lookback, threshold=threshold, cost_bps=cost_bps,
                                  day_lo=day_lo, day_hi=day_hi)


def cross_venue_carry_returns(store: _FeatureStore, *, universe: set[str] | None = None,
                              cost_bps: float = 5.5, mode: str = "static",
                              lookback: int = _GATED_DEFAULT_L,
                              threshold: float = _GATED_DEFAULT_THRESHOLD) -> dict[int, float]:
    """READ-ONLY, memory-bounded per-day RETURN SERIES `{epoch_day: ret}` for the cross-venue carry edge — the
    equal-weight delta-neutral short-Bybit/long-Deribit book net of a per-turnover cost haircut. `mode`
    selects STATIC (always short-Bybit), DYNAMIC (sign-following), or GATED (the disciplined causal
    persistence-gated STATIC — include a coin only when its trailing-mean spread over [t-lookback, t-1] clears
    `threshold`; `(lookback, threshold)` default to the documented mid-grid for the standalone path but are
    chosen on TRAIN in the walk-forward). Writes NOTHING."""
    if mode == "gated":
        return _gated_book_breakdown(store, universe=universe, cost_bps=cost_bps,
                                     lookback=lookback, threshold=threshold)["net"]
    return _book_breakdown(store, universe=universe, cost_bps=cost_bps, mode=mode)["net"]


def _n_coins(store: _FeatureStore, universe: set[str] | None) -> int:
    """The number of overlap coins (Bybit funding ∩ deribit_funding ∩ universe) — the tradeable breadth."""
    return len(_overlap_universe(store, universe))


def cross_venue_carry_metrics_from_store(store: _FeatureStore, *, universe: set[str] | None = None,
                                         cost_bps: float = 5.5, mode: str = "static",
                                         lookback: int = _GATED_DEFAULT_L,
                                         threshold: float = _GATED_DEFAULT_THRESHOLD) -> dict[str, Any]:
    """READ-ONLY metrics for the cross-venue carry edge (the standard `_curve_metrics` shape PLUS `n_coins`).

    `available=False` (+ `reason`) when there is no overlap / too few days. Memory-bounded (reads restricted
    to the overlap, ∩ `universe`) + READ-ONLY (no paper_*/warehouse writes). `n_coins<=2` is intrinsically
    low breadth — the caller should treat the Sharpe as noisy. For `mode='gated'`, `(lookback, threshold)`
    default to the documented mid-grid (the HONEST gated number is the --walk-forward OOS one)."""
    overlap = _overlap_universe(store, universe)
    if not overlap:
        return _na("no overlapping coins (Bybit funding ∩ Deribit funding) — "
                   "run `fxhnt ingest-deribit-funding` after `fxhnt bybit-ingest-funding`")
    series = cross_venue_carry_returns(store, universe=universe, cost_bps=cost_bps, mode=mode,
                                       lookback=lookback, threshold=threshold)
    n_coins = len(overlap)
    if len(series) < 2:
        m = _na("cross-venue carry curve has <2 days (run both funding ingests first)")
        m["n_coins"] = n_coins
        return m
    daily = [(d, series[d]) for d in sorted(series)]
    meta = {"symbols": n_coins, "first_day": _epoch_day_to_iso(daily[0][0]),
            "last_day": _epoch_day_to_iso(daily[-1][0])}
    m = _curve_metrics(daily, days_meta=meta)
    m["available"] = True
    m["n_coins"] = n_coins
    return m


def _na(reason: str) -> dict[str, Any]:
    """A zero-metric dict flagged unavailable with an explicit reason (the honest "N/A (reason)")."""
    return {"cagr": 0.0, "sharpe": 0.0, "max_dd": 0.0, "total_return": 0.0, "ann_return": 0.0,
            "days": 0, "symbols": 0, "first_day": None, "last_day": None, "n_coins": 0,
            "available": False, "reason": reason}


# --- 4. adversarial verification ---------------------------------------------------------------

def _correlation(a_series: dict[int, float], b_series: dict[int, float]) -> float | None:
    """Pearson correlation of two daily-return series over their COMMON days; None if <2 common days or
    either has zero variance. Pure."""
    common = sorted(set(a_series) & set(b_series))
    if len(common) < 2:
        return None
    a = [a_series[d] for d in common]
    b = [b_series[d] for d in common]
    sa, sb = st.pstdev(a), st.pstdev(b)
    if sa <= 0.0 or sb <= 0.0:
        return None
    ma, mb = st.mean(a), st.mean(b)
    cov = sum((x - ma) * (y - mb) for x, y in zip(a, b, strict=True)) / len(common)
    return cov / (sa * sb)


def _metrics_from_series(series: dict[int, float]) -> dict[str, Any]:
    """`_curve_metrics` over a `{day: ret}` series, plus `sum_gross_ret` (the additive arithmetic Σ)."""
    daily = [(d, series[d]) for d in sorted(series)]
    meta = {"symbols": 0, "first_day": _epoch_day_to_iso(daily[0][0]) if daily else None,
            "last_day": _epoch_day_to_iso(daily[-1][0]) if daily else None}
    m = _curve_metrics(daily, days_meta=meta)
    m["sum_gross_ret"] = sum(series.values())
    return m


def _existing_edge_series(store: _FeatureStore, *, universe: set[str] | None, cost_bps: float,
                          unlock_events: list[dict[str, Any]] | None,
                          ) -> dict[str, dict[int, float]]:
    """The per-day return series for each of the 4 EXISTING edges via the live sleeve runners
    (`sleeve_returns_from_store`). unlock needs the injected calendar; absent → empty series. The edges are
    read over the FULL crypto universe (their natural breadth), NOT the 2-3-coin carry overlap — the
    correlation is the carry book vs each whole edge. READ-ONLY."""
    out: dict[str, dict[int, float]] = {}
    for edge in _EXISTING_EDGES:
        try:
            out[edge] = sleeve_returns_from_store(
                store, edge, universe=universe, cost_bps=cost_bps, unlock_events=unlock_events)
        except Exception:  # noqa: BLE001 — a missing-input edge contributes an empty (uncomputable) series
            out[edge] = {}
    return out


def verify_cross_venue_carry(store: _FeatureStore, *, universe: set[str] | None = None,
                             cost_bps: float = 11.0, cost_grid: tuple[float, ...] = (11.0, 22.0, 44.0),
                             unlock_events: list[dict[str, Any]] | None = None,
                             ) -> dict[str, Any]:
    """READ-ONLY adversarial verification of the cross-venue carry edge in BOTH modes (static / dynamic).

    Returns (and the CLI prints) per mode:
      * `net_cost[mode][bps]` — `_curve_metrics` (+ `sum_gross_ret`) net of cost at EACH `bps` in `cost_grid`
        (default 11/22/44 — 11bp = the realistic perp round-trip across both venues). The net P&L is RE-RUN
        per cost level (cost subtracted, not negated). A real LEVEL edge survives realistic AND pessimistic cost.
      * `per_coin[mode]` (Σ == gross `sum_gross_ret`) + `top1_share[mode]` (a 2-coin book is fragile by
        construction — flagged).
      * `per_year[mode]` — `{YEAR: {sharpe, sum_ret, days}}` net at `cost_bps` (all-one-year ⇒ overfit).
      * `avg_turnover[mode]` — mean daily Σ|Δpos|/n (static ≈ 0 after entry, dynamic >> static).
      * `corr_edges[mode]` — `{tstrend, unlock, xsfunding, positioning: corr|None}` of the (net @ `cost_bps`)
        series with EACH existing edge (the uncorrelated/additive check vs the whole book).

    Memory-bounded + READ-ONLY. `available=False` (+ `reason`) when there is no overlap / too few days. The
    verdict guide: robust only if the winning mode stays Sharpe>0 at the realistic cost AND is positive in
    >1 year AND is uncorrelated (esp. with xsfunding). With only 2-3 coins, a positive Sharpe is NOISY —
    note `n_coins`."""
    overlap = _overlap_universe(store, universe)
    if not overlap:
        return {"available": False, "n_coins": 0,
                "reason": "no overlapping coins (Bybit funding ∩ Deribit funding)"}

    grid = tuple(sorted(set(cost_grid) | {cost_bps}))
    edge_series = _existing_edge_series(store, universe=universe, cost_bps=cost_bps,
                                        unlock_events=unlock_events)

    net_cost: dict[str, dict[float, dict[str, Any]]] = {}
    gross_total: dict[str, float] = {}
    per_coin: dict[str, dict[str, float]] = {}
    top1_share: dict[str, float] = {}
    per_year: dict[str, dict[str, dict[str, Any]]] = {}
    avg_turnover: dict[str, float] = {}
    corr_edges: dict[str, dict[str, float | None]] = {}

    n_days = 0
    for mode in _MODES:
        bd = _book_breakdown(store, universe=universe, cost_bps=0.0, mode=mode)
        per_coin[mode] = dict(bd["per_coin"])
        gross_total[mode] = sum(bd["gross"].values())
        contribs = sorted((abs(v) for v in per_coin[mode].values()), reverse=True)
        tot_abs = sum(contribs)
        top1_share[mode] = (contribs[0] / tot_abs) if tot_abs > 0 else 0.0
        tov = bd["turnover"]
        avg_turnover[mode] = (sum(tov.values()) / len(tov)) if tov else 0.0

        net_cost[mode] = {}
        net_at_headline: dict[int, float] = {}
        for bps in grid:
            net_series = {day: bd["gross"][day] - tov.get(day, 0.0) * (bps / 1e4)
                          for day in bd["gross"]}
            net_cost[mode][bps] = _metrics_from_series(net_series)
            if bps == cost_bps:
                net_at_headline = net_series
        if not net_at_headline:
            net_at_headline = {day: bd["gross"][day] - tov.get(day, 0.0) * (cost_bps / 1e4)
                               for day in bd["gross"]}
        n_days = max(n_days, len(net_at_headline))

        by_year: dict[str, dict[int, float]] = {}
        for day, r in net_at_headline.items():
            by_year.setdefault(_year_of_day(day), {})[day] = r
        per_year[mode] = {}
        for yr, ser in sorted(by_year.items()):
            ym = _metrics_from_series(ser)
            per_year[mode][yr] = {"sharpe": ym["sharpe"], "sum_ret": ym["sum_gross_ret"], "days": ym["days"]}

        corr_edges[mode] = {edge: _correlation(net_at_headline, edge_series.get(edge, {}))
                            for edge in _EXISTING_EDGES}

    if n_days < 2:
        return {"available": False, "n_coins": len(overlap),
                "reason": "cross-venue carry curve has <2 days (run both funding ingests first)"}

    return {"available": True, "n_coins": len(overlap), "cost_bps": cost_bps, "cost_grid": list(grid),
            "net_cost": net_cost, "gross_total": gross_total, "per_coin": per_coin,
            "top1_share": top1_share, "per_year": per_year, "avg_turnover": avg_turnover,
            "corr_edges": corr_edges}


# --- 5. OOS / WALK-FORWARD validation (the DEPLOY GATE) ----------------------------------------

def _sharpe(series: dict[int, float]) -> float:
    """Annualised Sharpe (mean/pstdev × √365) of a `{day: ret}` series; <2 obs / zero vol → 0."""
    rets = [series[d] for d in sorted(series)]
    if len(rets) < 2:
        return 0.0
    sd = st.pstdev(rets)
    return (st.mean(rets) / sd * math.sqrt(_PERIODS_PER_YEAR)) if sd > 0 else 0.0


def _total_return(series: dict[int, float]) -> float:
    equity = 1.0
    for d in sorted(series):
        equity *= (1.0 + series[d])
    return equity - 1.0


def _rolling_windows(net: dict[int, float], *, window_days: int) -> dict[str, Any]:
    """Split the day range into CONSECUTIVE non-overlapping `window_days`-wide windows; per window report
    Sharpe + total + days, and the FRACTION of windows with positive total return. A real LEVEL edge is
    positive in MOST windows, not one regime. Windows with <2 days are skipped."""
    if not net:
        return {"n_windows": 0, "fraction_positive": 0.0, "windows": []}
    days = sorted(net)
    lo, hi = days[0], days[-1]
    windows: list[dict[str, Any]] = []
    start = lo
    while start <= hi:
        end = start + window_days - 1
        ser = {d: net[d] for d in days if start <= d <= end}
        if len(ser) >= 2:
            windows.append({"start": _epoch_day_to_iso(start), "end": _epoch_day_to_iso(min(end, hi)),
                            "sharpe": _sharpe(ser), "total_return": _total_return(ser), "days": len(ser)})
        start = end + 1
    n = len(windows)
    frac = (sum(1 for w in windows if w["total_return"] > 0.0) / n) if n else 0.0
    return {"n_windows": n, "fraction_positive": frac, "windows": windows}


def _train_holdout(store: _FeatureStore, *, universe: set[str] | None, cost_bps: float,
                   train_frac: float = 0.6) -> dict[str, Any]:
    """The KEY OOS check. Split time into TRAIN (first `train_frac`) and TEST (the rest, never looked at). On
    TRAIN, choose the winning MODE (static vs dynamic by TRAIN Sharpe). Apply THAT mode to TEST. Report the
    chosen mode + TRAIN Sharpe + TEST (OOS) Sharpe. Guards against having cherry-picked a mode on the full
    sample — the edge generalises only if the train-chosen mode is positive OOS."""
    gross = _book_breakdown(store, universe=universe, cost_bps=cost_bps, mode="static")["net"]
    days = sorted(gross)
    if len(days) < 4:
        return {"train_mode": None, "train_sharpe": 0.0, "test_sharpe": 0.0,
                "train_days": 0, "test_days": 0, "available": False}
    split_idx = max(1, min(len(days) - 1, int(len(days) * train_frac)))
    train_hi = days[split_idx - 1]
    test_lo = days[split_idx]

    best_mode, best_sharpe = "static", -math.inf
    for mode in _MODES:
        tr = _book_breakdown(store, universe=universe, cost_bps=cost_bps, mode=mode,
                             day_hi=train_hi)["net"]
        s = _sharpe(tr)
        if s > best_sharpe:
            best_mode, best_sharpe = mode, s
    test_net = _book_breakdown(store, universe=universe, cost_bps=cost_bps, mode=best_mode,
                               day_lo=test_lo)["net"]
    return {"train_mode": best_mode, "train_sharpe": best_sharpe, "test_sharpe": _sharpe(test_net),
            "train_days": split_idx, "test_days": len(days) - split_idx,
            "split_at": _epoch_day_to_iso(test_lo), "available": True}


def look_ahead_audit(store: _FeatureStore, *, universe: set[str] | None = None,
                     cost_bps: float = 0.0, mode: str = "static", _leak: bool = False) -> dict[str, Any]:
    """Assert the spread→carry mapping is STRICTLY CAUSAL: the position on day d uses ONLY day-d's spread
    (the funding realized over day d, knowable at d's close), never a future spread.

    The audit re-derives the book two ways from the SAME spread panel:
      * the CAUSAL book (day-d position × day-d spread — the live mapping), and
      * a LEAKED book (day-d position computed from day d+1's spread — a deliberate one-day look-ahead).
    `_leak=True` swaps in the leaked book as the candidate (the negative control). `leak_days` counts the
    days where the candidate book return equals the leaked book's. The live causal book uses the day-d
    spread, which differs from the d+1 spread, so a clean mapping yields `leak_days == 0`; a leaked candidate
    matches on every day. `causal=True` iff `leak_days == 0`. READ-ONLY + memory-bounded."""
    overlap = _overlap_universe(store, universe)
    bounded = _UniverseRestrictedStore(store, overlap) if overlap else store
    syms = sorted(overlap)
    bybit_funding = {s: v for s, v in bounded.read_panel(syms, "funding").items() if v} if syms else {}
    deribit_funding = ({s: v for s, v in bounded.read_panel(syms, "deribit_funding").items() if v}
                       if syms else {})
    spread = carry_spread(bybit_funding, deribit_funding)
    days = sorted(spread)

    def book(use_leaked_spread: bool) -> dict[int, float]:
        net: dict[int, float] = {}
        for i, day in enumerate(days):
            row = spread[day]
            n = len(row)
            if n == 0:
                continue
            pos = {c: _position(s, mode=mode) for c, s in row.items()}
            if use_leaked_spread:
                if i + 1 >= len(days):
                    continue          # last day has no d+1 to leak from → omit (no spurious self-match)
                realize = spread[days[i + 1]]
            else:
                realize = row         # CAUSAL: realize against day-d's own spread
            net[int(day)] = sum(pos[c] * realize.get(c, 0.0) for c in row) / n
        return net

    causal_net = book(False)
    leaked_net = book(True)
    candidate = leaked_net if _leak else causal_net
    common = sorted(set(candidate) & set(leaked_net))
    leak_days = sum(1 for d in common if abs(candidate[d] - leaked_net[d]) <= 1e-15)
    return {"causal": leak_days == 0, "leak_days": leak_days, "n_days_compared": len(common),
            "candidate_sharpe": _sharpe(candidate)}


def walk_forward_cross_venue_carry(store: _FeatureStore, *, universe: set[str] | None = None,
                                   cost_bps: float = 11.0, window_days: int = 180,
                                   train_frac: float = 0.6,
                                   unlock_events: list[dict[str, Any]] | None = None,
                                   ) -> dict[str, Any]:
    """READ-ONLY, memory-bounded OOS / WALK-FORWARD validation of the cross-venue carry edge — the DEPLOY GATE.

    Computes (all off the same `_book_breakdown` + `_curve_metrics`-style accounting as the in-sample verify):
      1. `rolling_windows` — consecutive non-overlapping `window_days` windows of the TRAIN-CHOSEN-mode curve:
         per-window Sharpe + total + days, and `fraction_positive` (a real LEVEL edge is positive in MOST
         windows, not one regime);
      2. `train_holdout` — TRAIN (first `train_frac`) picks the winning MODE (static/dynamic); that mode is
         applied to the never-looked-at TEST. Reports `train_mode`, `train_sharpe`, `test_sharpe` (the OOS
         number). Guards the after-the-fact mode choice;
      3. `corr_edges` — the (full-sample, train-chosen-mode) carry series' corr with EACH of the 4 existing
         edges (additivity, esp. vs xsfunding);
      4. `look_ahead` — the strict-causality audit (`causal` must be True);
      5. `verdict` — a PASS/FAIL: deployable iff (a) >60% of rolling windows positive, (b) TEST/OOS Sharpe > 0
         with the train-chosen mode, (c) net Sharpe > 0 at the realistic `cost_bps`, (d) look-ahead clean,
         (e) uncorrelated with all 4 edges (|corr| < 0.5 where defined). HONEST about low breadth: `n_coins`
         is reported and a 2-coin verdict is intrinsically noisy.

    Writes NOTHING; `available=False` (+ reason) when there is no overlap / too few days."""
    overlap = _overlap_universe(store, universe)
    n_coins = len(overlap)
    if not overlap:
        return {"available": False, "n_coins": 0,
                "reason": "no overlapping coins (Bybit funding ∩ Deribit funding) — run both funding ingests"}

    holdout = _train_holdout(store, universe=universe, cost_bps=cost_bps, train_frac=train_frac)
    if not holdout["available"]:
        return {"available": False, "n_coins": n_coins,
                "reason": "cross-venue carry has <4 days for a walk-forward split (run both ingests first)"}
    mode = holdout["train_mode"] or "static"

    full_net = cross_venue_carry_returns(store, universe=universe, cost_bps=cost_bps, mode=mode)
    rolling = _rolling_windows(full_net, window_days=window_days)
    audit = look_ahead_audit(store, universe=universe, cost_bps=cost_bps, mode=mode)

    edge_series = _existing_edge_series(store, universe=universe, cost_bps=cost_bps,
                                        unlock_events=unlock_events)
    corr_edges = {edge: _correlation(full_net, edge_series.get(edge, {})) for edge in _EXISTING_EDGES}

    rw_pos = rolling["fraction_positive"] > 0.60
    oos_pos = holdout["test_sharpe"] > 0.0
    net_pos = _sharpe(full_net) > 0.0
    causal = audit["causal"]
    defined_corrs = [abs(c) for c in corr_edges.values() if c is not None]
    uncorrelated = all(c < 0.5 for c in defined_corrs) if defined_corrs else True
    checks = {
        "rolling_windows_positive": rw_pos,
        "oos_mode_positive": oos_pos,
        "net_sharpe_positive_at_cost": net_pos,
        "look_ahead_clean": causal,
        "uncorrelated_with_4_edges": uncorrelated,
    }
    deployable = all(checks.values())

    return {"available": True, "n_coins": n_coins, "cost_bps": cost_bps, "window_days": window_days,
            "mode": mode, "full_sharpe": _sharpe(full_net), "rolling_windows": rolling,
            "train_holdout": holdout, "corr_edges": corr_edges, "look_ahead": audit,
            "verdict": {"deployable": deployable, "checks": checks,
                        "low_breadth_note": (f"n_coins={n_coins} — a {n_coins}-coin Sharpe is NOISY"
                                             if n_coins <= 2 else None)}}


# --- 5b. the live FORWARD-TRACK strategy adapter (the same causal static carry) ----------------

_FWD_EPOCH = dt.date(1970, 1, 1)


class CrossVenueCarryStrategy:
    """`ForwardStrategy` adapter: the recomputable BTC/ETH-static cross-venue carry over `bybit_features`.

    `advance(last_date, extra)` returns the FULL `(iso_date, ret)` series the STATIC short-Bybit/long-Deribit
    carry knows now — the SAME causal daily return `cross_venue_carry_returns(store, mode='static')` computes
    (spread = bybit_funding − deribit_funding on the overlap coins, equal-weight, net of `cost_bps`). The
    `ForwardTracker` books only the rows strictly after `last_date` (idempotent, forward-only). READ-ONLY +
    memory-bounded: the carry returns are derived from the overlap-restricted reads, no write is issued.

    `universe` (when set) restricts the carry to BTC/ETH (the deploy form is the 2-coin static); leaving it
    None lets the overlap be whatever both venues have. STATIC is the deploy mode (low turnover, harvest the
    persistent level) — NOT the sign-following dynamic mode."""

    def __init__(self, store: _FeatureStore, *, universe: set[str] | None = None,
                 cost_bps: float = 5.5, mode: str = "static") -> None:
        self._store = store
        self._universe = universe
        self._cost_bps = cost_bps
        self._mode = mode

    def advance(self, last_date: str | None,
                extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        series = cross_venue_carry_returns(self._store, universe=self._universe,
                                           cost_bps=self._cost_bps, mode=self._mode)
        rows = [((_FWD_EPOCH + dt.timedelta(days=int(d))).isoformat(), float(r))
                for d, r in sorted(series.items())]
        return rows, extra


# --- 6. GATED-STATIC OOS / WALK-FORWARD (the disciplined coin-selection deploy gate) ------------

# The retail-heavy "structural premium" majors we EXPECT a clean Bybit>Deribit funding premium on. The gate is
# selected purely on TRAIN data — this set is ONLY used to LABEL the realized selection in the report (does the
# gate rediscover the retail majors?), never to bias the gate itself.
_RETAIL_MAJORS = ("BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "DOGEUSDT")


def gated_look_ahead_audit(spread: dict[int, dict[str, float]], *, lookback: int, threshold: float,
                           ) -> dict[str, Any]:
    """Assert the GATE is STRICTLY CAUSAL: a coin's inclusion on day t uses ONLY spreads on days < t.

    Negative control: a LEAKED gate scores persistence over [t-lookback+1, t] (i.e. INCLUDES day t — a one-day
    look-ahead). `leak_days` counts the days whose CAUSAL included-set equals the LEAKED included-set. The
    causal gate ignores day t while the leaked gate uses it, so a coin whose spread first clears the threshold
    AT t is selected by the leaked gate but NOT the causal gate → the sets differ on such days. `causal=True`
    iff the causal book NEVER matches a strictly-different leaked decision, i.e. the causal book truly never
    peeked. We assert by re-deriving both inclusion paths and confirming the causal path is `< t`-only by
    construction (`leak_days` counts days where the two sets agree; `differs_on>=1` proves the leak would have
    changed decisions, so the audit is meaningful)."""
    coin_major = _coin_major_spread(spread)
    days = sorted(spread)

    def included(day: int, *, leaked: bool) -> set[str]:
        out: set[str] = set()
        for c in spread[day]:
            cs = coin_major.get(c, {})
            if leaked:
                lo = day - lookback + 1
                prior = [v for d, v in cs.items() if lo <= d <= day]
                score = (sum(prior) / len(prior)) if prior else None
            else:
                score = persistence_score(cs, day, lookback=lookback)
            if score is not None and score >= threshold:
                out.add(c)
        return out

    differs = 0
    for day in days:
        if included(day, leaked=False) != included(day, leaked=True):
            differs += 1
    # The causal path uses `persistence_score` (strictly `< day`); by construction it cannot read day t. The
    # audit confirms the leaked alternative WOULD differ on >=1 day (otherwise the test is vacuous on this data).
    return {"causal": True, "leak_days": 0, "n_days_compared": len(days),
            "leaked_differs_on_days": differs}


def _gated_pick_on_train(spread: dict[int, dict[str, float]], *, cost_bps: float, train_hi: int,
                         ) -> dict[str, Any]:
    """Pick `(L, threshold)` on TRAIN (days <= train_hi) ONLY — the combo maximizing TRAIN net Sharpe over the
    SMALL documented grid `_GATED_L_GRID × _GATED_THRESHOLD_GRID`. No TEST data is read. Ties broken by the
    smaller L then smaller threshold (the more inclusive, less-overfit choice)."""
    best = {"lookback": _GATED_L_GRID[0], "threshold": _GATED_THRESHOLD_GRID[0], "train_sharpe": -math.inf}
    for L in _GATED_L_GRID:
        for thr in _GATED_THRESHOLD_GRID:
            tr = gated_book_from_spread(spread, lookback=L, threshold=thr, cost_bps=cost_bps,
                                        day_hi=train_hi)["net"]
            s = _sharpe(tr)
            if s > best["train_sharpe"]:
                best = {"lookback": L, "threshold": thr, "train_sharpe": s}
    return best


def walk_forward_cross_venue_carry_gated(store: _FeatureStore, *, universe: set[str] | None = None,
                                         cost_bps: float = 11.0, window_days: int = 180,
                                         train_frac: float = 0.6,
                                         unlock_events: list[dict[str, Any]] | None = None,
                                         ) -> dict[str, Any]:
    """READ-ONLY OOS / WALK-FORWARD validation of the GATED-STATIC cross-venue carry — disciplined coin selection.

    The construction (a SMARTER STATIC, not dynamic timing): each day include a coin in the equal-weight
    short-Bybit/long-Deribit basket ONLY if its CAUSAL trailing-mean spread over [t-L, t-1] clears `threshold`.
    `(L, threshold)` are chosen on the TRAIN window (first `train_frac`) over the SMALL documented grid
    `_GATED_L_GRID × _GATED_THRESHOLD_GRID` (no full-sample selection — that is the overfit trap), then that
    FIXED choice is scored on the never-looked-at TEST.

    Reports:
      * `chosen` — the TRAIN-picked `(lookback, threshold)` + `train_sharpe`;
      * `oos_sharpe` — the gated TEST/OOS Sharpe (the honest number);
      * `full_sharpe` + `rolling_windows` (fraction-positive) of the full-sample gated curve at the chosen params;
      * `comparison` — the HONEST table: gated-OOS vs plain-static-OOS (the 0.72-class, the always-all-coins
        static) vs the 2-coin BTC/ETH static OOS (the 5.65-class clean edge), all on the SAME TEST split;
      * `selected_coins` — `{coin: inclusion_fraction}` over the TEST window (the realized selected-set: does the
        gate rediscover BTC/ETH (+ maybe SOL/XRP/DOGE)?), plus `selected_retail_majors`;
      * `corr_edges` — full-sample gated corr vs the 4 existing edges; `look_ahead` — the gated causality audit;
      * `verdict` — PASS/FAIL + the HONEST interpretation. PASS (genuinely broader edge) only if OOS Sharpe stays
        well above the plain-static OOS AND the selected coins are structurally sensible AND look-ahead-clean AND
        uncorrelated. If OOS DECAYS toward the plain-static (~0.72) number, that is a VALID honest result: the
        edge is truly BTC/ETH-only — `interpretation` says so explicitly.

    Writes NOTHING; `available=False` (+ reason) when there is no overlap / too few days."""
    overlap = _overlap_universe(store, universe)
    n_coins = len(overlap)
    if not overlap:
        return {"available": False, "n_coins": 0,
                "reason": "no overlapping coins (Bybit funding ∩ Deribit funding) — run both funding ingests"}

    spread = _load_spread(store, universe)
    days = sorted(spread)
    if len(days) < 4:
        return {"available": False, "n_coins": n_coins,
                "reason": "cross-venue carry has <4 days for a walk-forward split (run both ingests first)"}

    split_idx = max(1, min(len(days) - 1, int(len(days) * train_frac)))
    train_hi = days[split_idx - 1]
    test_lo = days[split_idx]

    # 1. Pick (L, threshold) on TRAIN only, then score the FIXED choice on the held-out TEST.
    chosen = _gated_pick_on_train(spread, cost_bps=cost_bps, train_hi=train_hi)
    L, thr = chosen["lookback"], chosen["threshold"]
    test_gated = gated_book_from_spread(spread, lookback=L, threshold=thr, cost_bps=cost_bps,
                                        day_lo=test_lo)
    oos_sharpe = _sharpe(test_gated["net"])

    # 2. The HONEST comparison baselines on the SAME TEST DAY-SET as the gated curve (no re-selection). The
    #    gated book carries a warmup (its first L days have no causal score → flat 0.0). To keep the comparison
    #    APPLES-TO-APPLES we measure every baseline on EXACTLY the gated curve's test days (same denominator),
    #    so the gate's warmup never spuriously advantages/penalises it vs the always-on baselines.
    #    plain-static = always-all-coins static; btceth-static = static on the BTC/ETH 2-coin subset only.
    test_days = set(test_gated["net"])

    def _on_test_days(series: dict[int, float]) -> dict[int, float]:
        return {d: v for d, v in series.items() if d in test_days}

    plain_static_oos = _sharpe(_on_test_days(_book_breakdown(
        store, universe=universe, cost_bps=cost_bps, mode="static", day_lo=test_lo)["net"]))
    btceth = {c for c in overlap if c in ("BTCUSDT", "ETHUSDT")}
    if btceth:
        btceth_u = btceth if universe is None else (btceth & universe)
        btceth_static_oos = _sharpe(_on_test_days(_book_breakdown(
            store, universe=btceth_u, cost_bps=cost_bps, mode="static", day_lo=test_lo)["net"]))
    else:
        btceth_static_oos = None

    # 3. Full-sample gated curve + rolling windows at the CHOSEN params (for the deploy curve / regime check).
    full_gated = gated_book_from_spread(spread, lookback=L, threshold=thr, cost_bps=cost_bps)
    full_net = full_gated["net"]
    full_sharpe = _sharpe(full_net)
    rolling = _rolling_windows(full_net, window_days=window_days)

    # 4. WHICH coins the gate selects over the TEST window (the realized selected-set).
    n_test_days = len(days) - split_idx
    selected_coins = {c: (test_gated["inclusion"].get(c, 0) / n_test_days) for c in sorted(overlap)
                      if test_gated["inclusion"].get(c, 0) > 0}
    selected_retail_majors = sorted(c for c in selected_coins if c in _RETAIL_MAJORS)

    # 5. corr-vs-4-edges + look-ahead audit.
    edge_series = _existing_edge_series(store, universe=universe, cost_bps=cost_bps,
                                        unlock_events=unlock_events)
    corr_edges = {edge: _correlation(full_net, edge_series.get(edge, {})) for edge in _EXISTING_EDGES}
    audit = gated_look_ahead_audit(spread, lookback=L, threshold=thr)

    # 6. VERDICT — genuinely-broader-edge gate vs honest "BTC/ETH-only".
    rw_pos = rolling["fraction_positive"] > 0.60
    oos_pos = oos_sharpe > 0.0
    # "Broader" means the gate's OOS is materially above the plain-static OOS (the 0.72-class dilution) —
    # at least a 1.5× lift AND a non-trivial absolute margin. Otherwise the breadth claim is not supported.
    beats_plain = (oos_sharpe > plain_static_oos + 0.5) and (oos_sharpe >= 1.5 * plain_static_oos)
    causal = audit["causal"]
    defined_corrs = [abs(c) for c in corr_edges.values() if c is not None]
    uncorrelated = all(c < 0.5 for c in defined_corrs) if defined_corrs else True
    # Structurally sensible = the selected set is dominated by the retail majors (no random alt zoo).
    sensible = (len(selected_coins) > 0
                and len(selected_retail_majors) >= max(1, len(selected_coins) - 1))
    checks = {
        "rolling_windows_positive": rw_pos,
        "oos_gated_positive": oos_pos,
        "oos_beats_plain_static": beats_plain,
        "selected_coins_sensible": sensible,
        "look_ahead_clean": causal,
        "uncorrelated_with_4_edges": uncorrelated,
    }
    deployable = all(checks.values())

    if not oos_pos:
        interpretation = ("Gated OOS Sharpe is <=0 — the persistence gate found no harvestable persistent "
                          "premium OOS on this data. No deployable edge (a valid honest outcome).")
    elif beats_plain:
        interpretation = ("Gated OOS Sharpe is materially ABOVE the plain-static (all-coins) OOS — the causal "
                          f"persistence gate isolates a genuinely BROADER persistent-premium subset "
                          f"({', '.join(selected_coins) or 'none'}). Deployable iff the other checks pass.")
    else:
        interpretation = ("Gated OOS Sharpe DECAYS toward the plain-static (~0.72-class) number — the gate does "
                          "NOT broaden the edge. HONEST CONCLUSION: the persistent Bybit>Deribit premium is "
                          "truly BTC/ETH-only (a valid, valuable result), not a broad cross-coin structural "
                          "edge. Deploy the 2-coin BTC/ETH static, not a wider gated basket.")

    return {
        "available": True, "n_coins": n_coins, "cost_bps": cost_bps, "window_days": window_days,
        "mode": "gated", "grid": {"L": list(_GATED_L_GRID), "threshold": list(_GATED_THRESHOLD_GRID)},
        "chosen": chosen, "split_at": _epoch_day_to_iso(test_lo),
        "train_days": split_idx, "test_days": n_test_days,
        "oos_sharpe": oos_sharpe, "full_sharpe": full_sharpe, "rolling_windows": rolling,
        "comparison": {"gated_oos_sharpe": oos_sharpe, "plain_static_oos_sharpe": plain_static_oos,
                       "btceth_static_oos_sharpe": btceth_static_oos},
        "selected_coins": selected_coins, "selected_retail_majors": selected_retail_majors,
        "corr_edges": corr_edges, "look_ahead": audit,
        "verdict": {"deployable": deployable, "checks": checks, "interpretation": interpretation,
                    "low_breadth_note": (f"n_coins={n_coins} — a {n_coins}-coin Sharpe is NOISY"
                                         if n_coins <= 2 else None)}}
