"""Liquid-universe selection for the TRUSTWORTHY Bybit carry re-validation.

The naive Bybit carry re-validation diversified across ~600+ illiquid alt coins → a Sharpe-13.92 ARTIFACT
(over-diversification across names that are not realistically tradeable). `liquid_universe` restricts the
carry universe to the symbols whose MEDIAN daily turnover (USD notional traded) over a trailing window clears
a dollar-volume floor — the MiCA-tradeable-proxy subset, mirroring how the legacy combined book scopes to
liquid names. Median (not mean) so a single spike day on an otherwise-thin coin does NOT lift it into the universe.

Pure given the store (the only I/O is the `turnover` panel read). The `turnover` feature is written by the
upgraded `ingest_bybit_klines` (perp leg, kline row idx 6).
"""
from __future__ import annotations

import statistics as st
from typing import Protocol


class _TurnoverStore(Protocol):
    """Just the read `liquid_universe` needs (TimescaleFeatureStore satisfies this)."""

    def crypto_close_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 liquid_universe(store: _TurnoverStore, *, min_dollar_vol: float = 10_000_000.0,
                    lookback_days: int = 90, as_of: int | None = None,
                    suffix: str = "USDT") -> set[str]:
    """The set of symbols whose MEDIAN daily turnover over the trailing `lookback_days` (ending at `as_of`,
    epoch-day inclusive; default = the latest turnover day across all symbols) is >= `min_dollar_vol`.

    Median (robust to spikes) — a thin coin with one huge turnover day but otherwise near-zero stays OUT,
    whereas a consistently-deep coin clears the floor. A symbol with NO turnover rows in the window is
    excluded. Returns a set of symbols (the carry caller intersects this with the close/funding/spot panels).
    Pure given the store. Empty store / no turnover → empty set."""
    # The candidate symbols are those with a close panel (the carry universe); read their turnover.
    symbols = sorted(store.crypto_close_panel(suffix=suffix))
    if not symbols:
        return set()
    turnover = store.read_panel(symbols, "turnover")

    # Default the window's right edge to the latest turnover day observed across all symbols.
    if as_of is None:
        all_days = [d for days in turnover.values() for d in days]
        if not all_days:
            return set()
        as_of = max(all_days)
    lo = as_of - lookback_days + 1   # inclusive trailing window [lo, as_of]

    keep: set[str] = set()
    for sym in symbols:
        vals = [v for d, v in turnover.get(sym, {}).items() if lo <= d <= as_of]
        if not vals:
            continue
        if st.median(vals) >= min_dollar_vol:
            keep.add(sym)
    return keep
