"""Strategia page — documento operativo + lettura live dei segnali.

Renderizza il documento canonico ``docs/13-strategia-spiegata.md`` e
sopra di esso un pannello che mostra l'ultimo tick di
``market_snapshots`` confrontato con le soglie di ``strategy.yaml``.
Lo scopo è far vedere subito, ogni volta che si apre la pagina:
"a cosa serve il dato che il bot sta raccogliendo adesso".

La pagina è di sola lettura: non chiama MCP, non scrive sul DB.
"""

from __future__ import annotations

import math
import os
from dataclasses import dataclass
from pathlib import Path

import streamlit as st

from cerbero_bite.config.loader import load_strategy
from cerbero_bite.gui.data_layer import (
    DEFAULT_DB_PATH,
    humanize_dt,
    load_market_snapshots,
)
from cerbero_bite.state.models import MarketSnapshotRecord


_DOC_FILENAME = "13-strategia-spiegata.md"
_DOC_CANDIDATES: tuple[Path, ...] = (
    Path("/app/docs") / _DOC_FILENAME,  # in-container shipped via Dockerfile
    Path(__file__).resolve().parents[4] / "docs" / _DOC_FILENAME,  # repo dev
    Path(__file__).resolve().parents[3] / "docs" / _DOC_FILENAME,
)


def _resolve_db() -> Path:
    return Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH))


def _load_doc() -> str | None:
    for candidate in _DOC_CANDIDATES:
        if candidate.is_file():
            try:
                return candidate.read_text(encoding="utf-8")
            except OSError:
                continue
    return None


@dataclass(frozen=True)
class _GateRow:
    label: str
    value: str
    threshold: str
    status: str  # "pass" | "fail" | "n/a"
    note: str = ""


def _fmt_decimal(v: object, *, fmt: str = "{:.4g}", suffix: str = "") -> str:
    if v is None:
        return "—"
    try:
        return fmt.format(float(v)) + suffix
    except (TypeError, ValueError):
        return "—"


def _build_gates(
    snap: MarketSnapshotRecord, strategy: object
) -> list[_GateRow]:
    """Costruisce le righe del pannello live dai gate §2 della strategia."""
    rows: list[_GateRow] = []

    entry = getattr(strategy, "entry", None)
    structure = getattr(strategy, "structure", None)

    # --- DVOL band -------------------------------------------------
    dvol_min = float(getattr(entry, "dvol_min", 35.0)) if entry else 35.0
    dvol_max = float(getattr(entry, "dvol_max", 90.0)) if entry else 90.0
    dvol_v = float(snap.dvol) if snap.dvol is not None else None
    if dvol_v is None:
        rows.append(
            _GateRow(
                "DVOL in banda 35–90",
                "—",
                f"{dvol_min:.0f} ≤ DVOL ≤ {dvol_max:.0f}",
                "n/a",
                "Dato non disponibile in questo tick.",
            )
        )
    else:
        ok = dvol_min <= dvol_v <= dvol_max
        rows.append(
            _GateRow(
                "DVOL in banda",
                f"{dvol_v:.2f}",
                f"{dvol_min:.0f} … {dvol_max:.0f}",
                "pass" if ok else "fail",
                "Premio adeguato e regime non-stress."
                if ok
                else "Sotto banda = premio magro; sopra = stress, no entry.",
            )
        )

    # --- Funding perp annualized ----------------------------------
    fund_max = (
        float(getattr(entry, "funding_perp_abs_max_annualized", 0.80))
        if entry
        else 0.80
    )
    fp = (
        float(snap.funding_perp_annualized)
        if snap.funding_perp_annualized is not None
        else None
    )
    if fp is None:
        rows.append(
            _GateRow(
                "Funding perp |·| ≤ soglia",
                "—",
                f"|f| ≤ {fund_max:.0%}",
                "n/a",
            )
        )
    else:
        ok = abs(fp) <= fund_max
        rows.append(
            _GateRow(
                "Funding perp |·|",
                f"{fp:+.2%}",
                f"≤ {fund_max:.0%}",
                "pass" if ok else "fail",
                "Filtra regimi di liquidazioni a cascata imminenti.",
            )
        )

    # --- Cross-exchange funding (bias) ---------------------------
    bull_th = (
        float(getattr(entry, "funding_bull_threshold_annualized", 0.20))
        if entry
        else 0.20
    )
    bear_th = (
        float(getattr(entry, "funding_bear_threshold_annualized", -0.20))
        if entry
        else -0.20
    )
    fc = (
        float(snap.funding_cross_annualized)
        if snap.funding_cross_annualized is not None
        else None
    )
    if fc is None:
        bias_funding = "—"
        rows.append(
            _GateRow(
                "Funding cross (bias)",
                "—",
                f"bull ≥ {bull_th:+.0%} · bear ≤ {bear_th:+.0%}",
                "n/a",
            )
        )
    else:
        if fc >= bull_th:
            bias_funding = "BULL"
        elif fc <= bear_th:
            bias_funding = "BEAR"
        else:
            bias_funding = "NEUTRO"
        rows.append(
            _GateRow(
                "Funding cross (bias)",
                f"{fc:+.2%} → {bias_funding}",
                f"bull ≥ {bull_th:+.0%} · bear ≤ {bear_th:+.0%}",
                "pass" if bias_funding != "NEUTRO" else "fail",
                "Mediana 4 maggiori exchange. Discordante col trend = no entry.",
            )
        )

    # --- Macro days to event --------------------------------------
    dte_target = (
        int(getattr(structure, "dte_target", 18)) if structure else 18
    )
    macro_d = snap.macro_days_to_event
    if macro_d is None:
        rows.append(
            _GateRow(
                "Macro fuori finestra DTE",
                "nessun evento",
                f"> {dte_target}g",
                "pass",
                "Nessun evento ad alta severità entro la scadenza target.",
            )
        )
    else:
        ok = macro_d > dte_target
        rows.append(
            _GateRow(
                "Macro fuori finestra DTE",
                f"{macro_d} g al prossimo",
                f"> {dte_target} g",
                "pass" if ok else "fail",
                "FOMC/CPI/NFP/ECB/Powell entro DTE = no entry.",
            )
        )

    # --- Dealer gamma ---------------------------------------------
    gamma_min = (
        float(getattr(entry, "dealer_gamma_min", 0.0)) if entry else 0.0
    )
    gamma_enabled = (
        bool(getattr(entry, "dealer_gamma_filter_enabled", True))
        if entry
        else True
    )
    g = (
        float(snap.dealer_net_gamma)
        if snap.dealer_net_gamma is not None
        else None
    )
    if not gamma_enabled:
        rows.append(
            _GateRow(
                "Dealer gamma filter",
                _fmt_decimal(g, fmt="{:,.0f}", suffix=" USD")
                if g is not None
                else "—",
                "filtro DISABILITATO",
                "n/a",
            )
        )
    elif g is None:
        rows.append(
            _GateRow(
                "Dealer net gamma > soglia",
                "—",
                f"> {gamma_min:,.0f} USD",
                "n/a",
            )
        )
    else:
        ok = g > gamma_min
        rows.append(
            _GateRow(
                "Dealer net gamma",
                f"{g:,.0f} USD",
                f"> {gamma_min:,.0f} USD",
                "pass" if ok else "fail",
                "Long-gamma regime sopprime la vol → ideale per vendere spread.",
            )
        )

    # --- Liquidation risks ----------------------------------------
    liq_enabled = (
        bool(getattr(entry, "liquidation_filter_enabled", True))
        if entry
        else True
    )
    long_r = snap.liquidation_long_risk or "—"
    short_r = snap.liquidation_short_risk or "—"
    lr_status = "n/a"
    if liq_enabled and snap.liquidation_long_risk and snap.liquidation_short_risk:
        worst = max(
            ("low", "med", "high").index(snap.liquidation_long_risk)
            if snap.liquidation_long_risk in ("low", "med", "high")
            else 0,
            ("low", "med", "high").index(snap.liquidation_short_risk)
            if snap.liquidation_short_risk in ("low", "med", "high")
            else 0,
        )
        lr_status = "fail" if worst == 2 else "pass"
    rows.append(
        _GateRow(
            "Liquidation risk (long / short)",
            f"{long_r} / {short_r}",
            "non `high`" if liq_enabled else "filtro DISABILITATO",
            lr_status,
            "Densità liquidazioni vicine al spot. `high` su un lato = scarta setup.",
        )
    )

    # --- IV − RV (richness) — gate §2.9 ---------------------------
    rv = (
        float(snap.realized_vol_30d) if snap.realized_vol_30d is not None else None
    )
    iv_minus_rv = (
        float(snap.iv_minus_rv) if snap.iv_minus_rv is not None else None
    )
    iv_min = float(getattr(entry, "iv_minus_rv_min", 0.0)) if entry else 0.0
    iv_enabled = (
        bool(getattr(entry, "iv_minus_rv_filter_enabled", False)) if entry else False
    )
    if not iv_enabled:
        rows.append(
            _GateRow(
                "IV − RV (richness)",
                (
                    f"{iv_minus_rv:+.2f} pt vol"
                    if iv_minus_rv is not None
                    else "—"
                ),
                "filtro DISABILITATO (info-only)",
                "n/a",
                f"RV30={rv:.2f} · attiva con `iv_minus_rv_filter_enabled: true`"
                if rv is not None
                else "Attiva con `iv_minus_rv_filter_enabled: true`",
            )
        )
    elif iv_minus_rv is None:
        rows.append(
            _GateRow(
                "IV − RV ≥ soglia",
                "—",
                f"≥ {iv_min:.1f} pt vol",
                "n/a",
                "Dato non disponibile in questo tick (best-effort skip).",
            )
        )
    else:
        ok = iv_minus_rv >= iv_min
        rows.append(
            _GateRow(
                "IV − RV ≥ soglia",
                f"{iv_minus_rv:+.2f} pt vol",
                f"≥ {iv_min:.1f} pt vol",
                "pass" if ok else "fail",
                "Premio ricco rispetto a quanto il mercato si è davvero "
                "mosso → edge sostenibile per il venditore di vol."
                + (f" RV30={rv:.2f}" if rv is not None else ""),
            )
        )

    return rows


def _render_gates(rows: list[_GateRow]) -> None:
    icons = {"pass": "✅", "fail": "❌", "n/a": "⚪"}
    for r in rows:
        icon = icons.get(r.status, "⚪")
        col1, col2, col3 = st.columns([4, 4, 4])
        col1.markdown(f"{icon} **{r.label}**")
        col2.markdown(f"`{r.value}`")
        col3.markdown(f"_{r.threshold}_")
        if r.note:
            st.caption(r.note)
        st.divider()


def _profile_caps(strategy: object | None) -> dict[str, float]:
    """Estrae le sole leve di sizing da una strategia (o usa default conservativi)."""
    out = {
        "cap_pertrade_eur": 200.0,
        "cap_aggregate_eur": 1000.0,
        "kelly": 0.13,
        "max_n": 4.0,
        "max_concurrent": 1.0,
        "width_pct": 0.04,
        "credit_ratio": 0.30,
        "profit_take": 0.50,
        "stop_mult": 2.50,
    }
    if strategy is None:
        return out
    try:
        out["cap_pertrade_eur"] = float(strategy.sizing.cap_per_trade_eur)  # type: ignore[attr-defined]
        out["cap_aggregate_eur"] = float(strategy.sizing.cap_aggregate_open_eur)  # type: ignore[attr-defined]
        out["kelly"] = float(strategy.sizing.kelly_fraction)  # type: ignore[attr-defined]
        out["max_n"] = float(strategy.sizing.max_contracts_per_trade)  # type: ignore[attr-defined]
        out["max_concurrent"] = float(strategy.sizing.max_concurrent_positions)  # type: ignore[attr-defined]
        out["width_pct"] = float(strategy.structure.spread_width.target_pct_of_spot)  # type: ignore[attr-defined]
        out["credit_ratio"] = float(strategy.structure.credit_to_width_ratio_min)  # type: ignore[attr-defined]
        out["profit_take"] = float(strategy.exit.profit_take_pct_of_credit)  # type: ignore[attr-defined]
        out["stop_mult"] = float(strategy.exit.stop_loss_mark_x_credit)  # type: ignore[attr-defined]
    except Exception:
        pass
    return out


def _detect_features(strategy: object | None) -> dict[str, bool]:
    """Quali miglioramenti del PR FDAC sono ATTIVI in questa strategia.

    - **A** (delta dinamico): `short_strike.delta_by_dvol` non vuoto.
    - **D** (vol-harvest): `exit.vol_harvest_dvol_decrease > 0`.
    - **F** (auto-pause): `auto_pause.enabled = true`.
    - **IV** (IV-richness gate, dal PR precedente): `entry.iv_minus_rv_filter_enabled`.
    """
    feats = {"A": False, "D": False, "F": False, "IV": False}
    if strategy is None:
        return feats
    try:
        feats["A"] = bool(
            getattr(strategy.structure.short_strike, "delta_by_dvol", [])  # type: ignore[attr-defined]
        )
    except Exception:
        pass
    try:
        feats["D"] = (
            float(getattr(strategy.exit, "vol_harvest_dvol_decrease", 0)) > 0  # type: ignore[attr-defined]
        )
    except Exception:
        pass
    try:
        feats["F"] = bool(
            getattr(getattr(strategy, "auto_pause", None), "enabled", False)
        )
    except Exception:
        pass
    try:
        feats["IV"] = bool(
            getattr(strategy.entry, "iv_minus_rv_filter_enabled", False)  # type: ignore[attr-defined]
        )
    except Exception:
        pass
    return feats


def _compute_pl(
    caps: dict[str, float],
    *,
    capital: float,
    spot: float,
    win_rate: float,
    trades_per_year: int,
    eur_to_usd: float = 1.075,
    features: dict[str, bool] | None = None,
) -> dict[str, float]:
    """Calcola le metriche P/L per un profilo di sizing.

    Quando ``features`` è popolato, applica gli effetti stimati dei
    miglioramenti del PR FDAC + IV-RV gate:

    - ``IV`` (IV-richness gate, §2.9): +5 pp win-rate, −25% trade/anno.
    - ``A`` (delta dinamico, §3.2): +1.5 pp win-rate, sl_loss × 0.95.
    - ``D`` (vol-harvest, §7-bis): 5% delle would-be-loss diventano
      harvest exit a +0.20 × credito.
    - ``F`` (auto-pause, §7-bis): −8% trade/anno (skip-day dopo
      streak), e nei calcoli di drawdown atteso il streak_99 è
      cappato a lookback_trades=5.

    Effetti **stimati ex-ante** dalla letteratura short-vol systematic;
    i valori puntuali andranno calibrati sul dataset accumulato.
    """
    feats = features or {}
    width = caps["width_pct"] * spot
    credit = caps["credit_ratio"] * width
    tp_profit = caps["profit_take"] * credit
    sl_loss = (caps["stop_mult"] - 1.0) * credit

    # === Effetti dei miglioramenti =====================================
    win_rate_eff = win_rate
    trades_eff = float(trades_per_year)
    sl_loss_eff = sl_loss
    extra_harvest_ev = 0.0
    prob_harvest = 0.0

    if feats.get("IV"):
        # Skip più aggressivo + qualità migliore: +5 pp win, −25% trade.
        win_rate_eff = min(0.95, win_rate_eff + 0.05)
        trades_eff *= 0.75
    if feats.get("A"):
        # Migliore strike picking → +1.5 pp win-rate; riduzione del
        # tail della perdita (5%) per le bande high-DVOL.
        win_rate_eff = min(0.95, win_rate_eff + 0.015)
        sl_loss_eff *= 0.95
    if feats.get("D"):
        # Vol-harvest: ~5% delle entrate intercettate prima dello stop
        # con un piccolo profitto (+0.20×credit). Sottrae lo stesso
        # volume dalle prob_loss.
        prob_harvest = 0.05
        extra_harvest_ev = 0.20 * credit
    # F (auto-pause) agisce su streak_99 più sotto, e sul trades_eff.
    if feats.get("F"):
        trades_eff *= 0.92

    cap_pertrade_usd = caps["cap_pertrade_eur"] * eur_to_usd
    risk_target = min(caps["kelly"] * capital, cap_pertrade_usd)
    # Max loss per contratto = width − credit (NON width). Su un put
    # spread incassi `credit` upfront, quindi la perdita massima è la
    # larghezza meno il credito (vedi core/sizing_engine.py).
    max_loss_per_contract = max(width - credit, 1e-6)
    n_kelly = int(risk_target // max_loss_per_contract)
    n_per_trade = max(0, min(n_kelly, int(caps["max_n"])))

    prob_time_stop = 0.07
    prob_other_stop = 0.03
    prob_loss = max(
        0.0,
        1.0 - win_rate_eff - prob_time_stop - prob_other_stop - prob_harvest,
    )
    avg_time_stop_pl = 0.10 * credit

    e_trade_gross = (
        win_rate_eff * tp_profit
        - prob_loss * sl_loss_eff
        + prob_time_stop * avg_time_stop_pl
        + prob_harvest * extra_harvest_ev
    )
    fees = 0.0003 * spot * 2
    slippage = 0.03 * credit
    e_trade_net = e_trade_gross - fees - slippage

    concurrency = max(1.0, caps["max_concurrent"])
    annual_pl = trades_eff * n_per_trade * concurrency * e_trade_net
    apr = (annual_pl / capital) if capital > 0 else 0.0

    # --- Max drawdown -------------------------------------------------
    # Due metriche distinte:
    #
    # 1. **Streak atteso (P99)**: lunghezza della peggior sequenza di
    #    stop consecutivi che ci si aspetta di vedere in un anno con
    #    probabilità ≤ 1%. Usa l'approssimazione union-bound:
    #        P(streak ≥ N in N_trade tentativi) ≈ N_trade × p_loss^N
    #    Imponendo questa quantità ≤ 0.01 e risolvendo per N:
    #        N = ceil( log(0.01 / N_trade) / log(p_loss) )
    #    Drawdown corrispondente = N × stop_loss × contracts × concurrency.
    #
    # 2. **Tail/gap risk**: scenario "gap notturno" in cui il mark
    #    salta oltre la copertura long PRIMA che lo stop sia
    #    eseguibile. La perdita massima reale è la larghezza intera
    #    dello spread meno il credito iniziale, su tutte le posizioni
    #    aperte simultaneamente.
    if prob_loss > 0 and prob_loss < 1 and trades_per_year > 0:
        streak_99 = max(
            1,
            int(math.ceil(
                math.log(0.01 / trades_per_year) / math.log(prob_loss)
            )) if prob_loss < 1 else 1,
        )
    else:
        streak_99 = 0
    expected_dd_usd = streak_99 * sl_loss * n_per_trade * concurrency
    expected_dd_pct = expected_dd_usd / capital if capital > 0 else 0.0

    tail_dd_usd = (width - credit) * n_per_trade * concurrency
    tail_dd_pct = tail_dd_usd / capital if capital > 0 else 0.0

    return {
        "width": width,
        "credit": credit,
        "tp_profit": tp_profit,
        "sl_loss": sl_loss_eff,
        "risk_target": risk_target,
        "n_per_trade": float(n_per_trade),
        "concurrency": concurrency,
        "e_trade_net": e_trade_net,
        "annual_pl": annual_pl,
        "apr": apr,
        "fees": fees,
        "slippage": slippage,
        "prob_loss": prob_loss,
        "prob_harvest": prob_harvest,
        "streak_99": float(streak_99),
        "expected_dd_usd": expected_dd_usd,
        "expected_dd_pct": expected_dd_pct,
        "tail_dd_usd": tail_dd_usd,
        "tail_dd_pct": tail_dd_pct,
        "win_rate_eff": win_rate_eff,
        "trades_eff": trades_eff,
    }


def _render_profile_card(
    label: str,
    caps: dict[str, float],
    metrics: dict[str, float],
    badge: str,
    features: dict[str, bool] | None = None,
    metrics_base: dict[str, float] | None = None,
) -> None:
    """Rendering di un profilo (conservativo o aggressivo) in una colonna."""
    st.markdown(f"### {label}  {badge}")
    st.caption(
        f"cap/trade {caps['cap_pertrade_eur']:.0f} EUR · "
        f"cap aggreg. {caps['cap_aggregate_eur']:.0f} EUR · "
        f"max {caps['max_n']:.0f} contratti × "
        f"{caps['max_concurrent']:.0f} pos. concorrenti"
    )

    if features:
        active = [k for k, v in features.items() if v]
        if active:
            st.caption(
                "🟢 Miglioramenti attivi: "
                + " · ".join(
                    {
                        "IV": "**IV-RV gate**",
                        "A": "**A** delta dinamico",
                        "D": "**D** vol-harvest",
                        "F": "**F** auto-pause",
                    }.get(k, k)
                    for k in active
                )
            )
        else:
            st.caption("⚪ Nessun miglioramento attivo (formula base)")

    cols = st.columns(2)
    cols[0].metric("Contratti per trade", f"{metrics['n_per_trade']:.0f}")
    cols[1].metric("Posizioni concorrenti", f"{metrics['concurrency']:.0f}")

    cols = st.columns(2)
    e_delta = (
        f"{metrics['e_trade_net'] - metrics_base['e_trade_net']:+.1f}"
        if metrics_base
        else None
    )
    pl_delta = (
        f"{metrics['annual_pl'] - metrics_base['annual_pl']:+.0f} USD vs base"
        if metrics_base
        else f"{metrics['apr']:+.1%} APR"
    )
    cols[0].metric(
        "E[trade] netto",
        f"{metrics['e_trade_net']:+.1f} USD",
        delta=e_delta,
        help=(
            f"win_rate effettivo={metrics['win_rate_eff']:.0%}, "
            f"prob_loss={metrics['prob_loss']:.0%}, "
            f"trade/anno={metrics['trades_eff']:.0f}"
        ),
    )
    cols[1].metric(
        "P/L annuo stimato",
        f"{metrics['annual_pl']:+.0f} USD",
        delta=f"{metrics['apr']:+.1%} APR" + (
            f" ({metrics['annual_pl'] - metrics_base['annual_pl']:+.0f} vs base)"
            if metrics_base
            else ""
        ),
    )

    cols = st.columns(2)
    cols[0].metric(
        "Max DD attesa (P99)",
        f"−{metrics['expected_dd_usd']:.0f} USD",
        delta=f"{-metrics['expected_dd_pct']:+.1%} cap",
        delta_color="inverse",
        help=(
            f"Streak di {int(metrics['streak_99'])} stop consecutivi "
            f"(probabilità ≤ 1% nell'anno) × perdita stop "
            f"({metrics['sl_loss']:.0f} USD) × contratti × posizioni "
            f"concorrenti. È la peggior sequenza che ti aspetti di "
            "vedere; il drawdown reale può essere maggiore se i filtri "
            "non rilevano un regime change."
        ),
    )
    cols[1].metric(
        "Max DD coda (gap)",
        f"−{metrics['tail_dd_usd']:.0f} USD",
        delta=f"{-metrics['tail_dd_pct']:+.1%} cap",
        delta_color="inverse",
        help=(
            "Scenario gap notturno: il mark salta oltre la copertura "
            "long PRIMA che lo stop sia eseguibile. Perdita = larghezza "
            "intera meno credito, su tutte le posizioni aperte. "
            "I filtri quant + macro lo riducono ma NON lo annullano."
        ),
    )

    if metrics["n_per_trade"] == 0:
        st.warning(
            "Sizing 0 contratti: capitale insufficiente per i cap di "
            "questo profilo."
        )


def _render_pl_panel(
    strategy_main: object | None,
    strategy_conservativa: object | None,
    strategy_aggressiva: object | None,
) -> None:
    """Pannello P/L: confronto Conservativa vs Aggressiva sugli stessi slider."""
    st.subheader("💰 P/L atteso — Conservativa vs Aggressiva")
    st.caption(
        "Slider parametrici: scegli **cap per trade** e **posizioni "
        "concorrenti**, il capitale richiesto viene calcolato in "
        "automatico (Kelly-binding × concurrency / kelly_fraction). "
        "Conservativa e Aggressiva ereditano dai rispettivi yaml SOLO "
        "le leve qualitative (width_pct, credit_ratio, kelly_fraction, "
        "feature attive); le leve di sizing (cap, concorrenza) le "
        "controlli qui sotto."
    )

    col_a, col_b, col_c, col_d, col_e = st.columns(5)
    cap_per_trade_eur = col_a.slider(
        "Cap/trade (EUR)", 50, 2000, value=200, step=10,
        help="Massima perdita per singolo trade. Bound al rischio.",
    )
    concurrency_override = col_b.slider(
        "Pos. concorrenti", 1, 10, value=3, step=1,
        help="Quanti trade simultanei. Cap aggregato = cap/trade × N.",
    )
    spot = col_c.slider("Spot ETH (USD)", 1500, 6000, value=3000, step=100)
    win_rate = col_d.slider(
        "Win rate atteso", 0.50, 0.90, value=0.75, step=0.01,
        help=(
            "Senza filtri quant ≈ 0.65–0.70. CON filtri (dealer gamma>0, "
            "no macro, IV−RV>0, liquidation_*_risk≠high) sale a 0.75–0.80."
        ),
    )
    trades_per_year = col_e.slider(
        "Trade / anno (post-filtri)", 20, 200, value=110, step=5,
        help=(
            "Crypto è 24/7: l'entry cycle gira ogni giorno alle 14:00 UTC "
            "(`0 14 * * *`). 365 candidature × ~30-50% pass-rate effettivo "
            "(post-filtri + cap concorrenza) ≈ 110-180/anno. Auto-pause F "
            "riduce ulteriormente di ~8% in regime drawdown."
        ),
    )

    cons_caps = _profile_caps(strategy_conservativa or strategy_main)
    aggr_caps = _profile_caps(strategy_aggressiva)
    # Override sizing dai slider (sostituisce le leve cap/trade,
    # cap_aggregate, max_concurrent dei yaml).
    eur_to_usd = 1.075
    cap_pertrade_usd = cap_per_trade_eur * eur_to_usd
    cap_aggregate_override = float(cap_per_trade_eur * concurrency_override)
    cons_caps = {
        **cons_caps,
        "cap_pertrade_eur": float(cap_per_trade_eur),
        "cap_aggregate_eur": cap_aggregate_override,
        "max_concurrent": float(concurrency_override),
    }
    aggr_caps = {
        **aggr_caps,
        "cap_pertrade_eur": float(cap_per_trade_eur),
        "cap_aggregate_eur": cap_aggregate_override,
        "max_concurrent": float(concurrency_override),
    }
    # Capitale richiesto: Kelly-binding aggregato.
    # Per ogni trade slot, kelly × capital ≥ cap_pertrade_usd → capital
    # ≥ cap_pertrade_usd / kelly. Per N concorrenti, scala linearmente
    # come limite conservativo del notional cumulato.
    kelly_cons = cons_caps.get("kelly", 0.13)
    kelly_aggr = aggr_caps.get("kelly", 0.13)
    capital_cons = int(
        cap_pertrade_usd * concurrency_override / max(kelly_cons, 1e-3)
    )
    capital_aggr = int(
        cap_pertrade_usd * concurrency_override / max(kelly_aggr, 1e-3)
    )
    capital = max(capital_cons, capital_aggr)
    cap_col1, cap_col2, cap_col3 = st.columns(3)
    cap_col1.metric("📊 Capitale richiesto", f"${capital:,}")
    cap_col2.metric(
        "💸 Cap aggregato (notional)",
        f"${int(cap_pertrade_usd * concurrency_override):,}",
    )
    cap_col3.metric("🎯 Cap per trade (USD)", f"${int(cap_pertrade_usd):,}")
    cons_feats = _detect_features(strategy_conservativa or strategy_main)
    aggr_feats = _detect_features(strategy_aggressiva)

    apply_features = st.checkbox(
        "Applica gli effetti dei miglioramenti FDAC + IV-RV gate "
        "letti dai due `strategy.*.yaml`",
        value=True,
        help=(
            "Quando ON, ogni colonna applica gli effetti stimati delle "
            "feature attive nel rispettivo profilo. OFF = formula base "
            "(senza miglioramenti) per confronto pulito."
        ),
    )

    feats_cons = cons_feats if apply_features else {}
    feats_aggr = aggr_feats if apply_features else {}

    # Calcoli "base" (senza feature) per la delta che mostriamo nel card.
    cons_base = _compute_pl(
        cons_caps,
        capital=capital,
        spot=spot,
        win_rate=win_rate,
        trades_per_year=trades_per_year,
    )
    aggr_base = _compute_pl(
        aggr_caps,
        capital=capital,
        spot=spot,
        win_rate=win_rate,
        trades_per_year=trades_per_year,
    )
    cons = _compute_pl(
        cons_caps,
        capital=capital,
        spot=spot,
        win_rate=win_rate,
        trades_per_year=trades_per_year,
        features=feats_cons,
    )
    aggr = _compute_pl(
        aggr_caps,
        capital=capital,
        spot=spot,
        win_rate=win_rate,
        trades_per_year=trades_per_year,
        features=feats_aggr,
    )

    cons_version = getattr(
        strategy_conservativa or strategy_main, "config_version", "?"
    )
    aggr_version = getattr(strategy_aggressiva, "config_version", "?")
    col_cons, col_aggr = st.columns(2)
    with col_cons:
        _render_profile_card(
            "🛡️ Conservativa",
            cons_caps,
            cons,
            f"_(golden config v{cons_version})_",
            features=feats_cons,
            metrics_base=cons_base if apply_features and any(feats_cons.values()) else None,
        )
    with col_aggr:
        _render_profile_card(
            "🔥 Aggressiva",
            aggr_caps,
            aggr,
            f"_(v{aggr_version} · deroga §11, richiede paper trading)_",
            features=feats_aggr,
            metrics_base=aggr_base if apply_features and any(feats_aggr.values()) else None,
        )

    if aggr["annual_pl"] > 0 and cons["annual_pl"] > 0:
        ratio = aggr["annual_pl"] / cons["annual_pl"]
        st.success(
            f"Profilo aggressivo: P/L atteso ≈ **{ratio:.1f}× il "
            f"conservativo** ({aggr['apr']:+.1%} vs {cons['apr']:+.1%} "
            "APR). Drawdown atteso scala con lo stesso fattore."
        )

    if cons["annual_pl"] < 0 and aggr["annual_pl"] < 0:
        st.error(
            f"**Entrambi i profili in perdita** (cons {cons['apr']:+.1%}, "
            f"aggr {aggr['apr']:+.1%} APR). Selling vol nudo a win rate "
            f"{win_rate:.0%} è strutturalmente non profittevole. L'edge "
            "sono i FILTRI (dealer gamma>0, no macro, liquidation≠high, "
            "bias chiaro) e i miglioramenti F+D+A+IV-RV gate, che alzano "
            "il win rate effettivo sopra ~0.75 e/o riducono i tail loss. "
            "Spunta l'opzione 'Applica gli effetti dei miglioramenti' qui "
            "sopra per vedere i numeri con i filtri attivi."
        )
    elif cons["annual_pl"] < 0:
        st.warning(
            f"**Conservativo in perdita** ({cons['apr']:+.1%} APR), "
            f"aggressivo positivo ({aggr['apr']:+.1%} APR). I miglioramenti "
            "F+D+A+IV-RV gate stanno facendo il loro lavoro sull'aggressivo. "
            "Sul conservativo i cap stretti riducono troppo il P/L atteso "
            "a questo win rate."
        )

    # === Mini-tabella: contributo marginale di ogni feature =====
    if apply_features and (any(feats_cons.values()) or any(feats_aggr.values())):
        st.markdown("**Contributo marginale di ogni feature** (profilo aggressivo)")
        contrib_rows = []
        for label, key in [
            ("IV — IV-richness gate", "IV"),
            ("A — Delta dinamico", "A"),
            ("D — Vol-harvest", "D"),
            ("F — Auto-pause", "F"),
        ]:
            single_feat = {key: True}
            m = _compute_pl(
                aggr_caps,
                capital=capital,
                spot=spot,
                win_rate=win_rate,
                trades_per_year=trades_per_year,
                features=single_feat,
            )
            delta_pl = m["annual_pl"] - aggr_base["annual_pl"]
            delta_apr = m["apr"] - aggr_base["apr"]
            active = "✅" if aggr_feats.get(key) else "—"
            contrib_rows.append(
                {
                    "Feature": label,
                    "Attiva nel YAML": active,
                    "ΔP/L annuo (solo questa)": f"{delta_pl:+.0f} USD",
                    "ΔAPR": f"{delta_apr:+.1%}",
                }
            )
        st.table(contrib_rows)
        st.caption(
            "Ogni riga mostra il contributo del SINGOLO feature (le altre "
            "spente). Effetti stimati ex-ante; calibrabili sui dati "
            "raccolti via `📐 Calibrazione`."
        )

    # Sensibilità win-rate per il profilo aggressivo (più informativo)
    st.markdown("**Sensibilità al win rate** (profilo aggressivo)")
    sens_rows = []
    for wr in (0.65, 0.70, 0.72, 0.75, 0.78, 0.80, 0.82):
        m_a = _compute_pl(
            aggr_caps,
            capital=capital,
            spot=spot,
            win_rate=wr,
            trades_per_year=trades_per_year,
            features=feats_aggr,
        )
        m_c = _compute_pl(
            cons_caps,
            capital=capital,
            spot=spot,
            win_rate=wr,
            trades_per_year=trades_per_year,
            features=feats_cons,
        )
        sens_rows.append(
            {
                "Win rate": f"{wr:.0%}",
                "Cons. APR": f"{m_c['apr']:+.1%}",
                "Cons. Max DD": f"−{m_c['expected_dd_pct']:.1%}",
                "Aggr. APR": f"{m_a['apr']:+.1%}",
                "Aggr. Max DD": f"−{m_a['expected_dd_pct']:.1%}",
            }
        )
    st.table(sens_rows)

    st.caption(
        "Costi: fee 0.03% notional × 2 leg, slippage 3% del credito "
        "(combo limit GTC al mid). Distribuzione esiti: profit-take = "
        "win_rate, time-stop ≈ 7%, altri-stop ≈ 3%, stop-loss = il resto. "
        "**Multi-asset (ETH+BTC) non è incluso nei numeri**: richiede "
        "modifiche di codice (single-asset attuale). Il moltiplicatore "
        "2× citato nel doc è la stima ex-ante di cosa otterresti DOPO."
    )


def render() -> None:
    st.title("📚 Strategia")
    st.caption(
        "Documento operativo che lega ogni regola del rule engine al "
        "dato osservabile da cui dipende. Il pannello live in alto mostra "
        "l'ultimo tick di `market_snapshots` confrontato con le soglie di "
        "`strategy.yaml`."
    )

    db_path = _resolve_db()

    asset = st.selectbox("Asset", options=["ETH", "BTC"], index=0)

    records = load_market_snapshots(asset=asset, db_path=db_path, limit=1)

    def _try_load(name: str) -> object | None:
        for base in (Path("/app"), Path.cwd(), Path(__file__).resolve().parents[4]):
            path = base / name
            if path.is_file():
                try:
                    # `_profile_caps` legge `.sizing.*` direttamente sul
                    # `StrategyConfig`, non sul wrapper `LoadedConfig`.
                    return load_strategy(path).config
                except Exception as exc:
                    st.warning(
                        f"`{name}`: {type(exc).__name__}: {exc}"
                    )
                    return None
        return None

    strategy = _try_load("strategy.yaml")
    strategy_conservativa = _try_load("strategy.conservativa.yaml")
    strategy_aggressiva = _try_load("strategy.aggressiva.yaml")

    st.divider()
    st.subheader("📡 Stato live dei gate di entry §2")
    if not records:
        st.info(
            "Nessuno snapshot disponibile per "
            f"`{asset}`. Il job `market_snapshot` (cron `*/15`) deve "
            "girare almeno una volta. Engine attivo? Controlla la pagina "
            "`📊 Status`."
        )
    else:
        latest = records[0]
        st.caption(
            f"Ultimo tick: {humanize_dt(latest.timestamp)} · "
            f"asset {latest.asset} · "
            f"fetch_ok = {'✅' if latest.fetch_ok else '⚠️'}"
        )
        if strategy is None:
            st.warning(
                "Senza `strategy.yaml` non posso valutare i gate; mostro "
                "solo i valori grezzi."
            )
            st.json(latest.model_dump(mode="json"))
        else:
            rows = _build_gates(latest, strategy)
            _render_gates(rows)

    st.divider()
    _render_pl_panel(strategy, strategy_conservativa, strategy_aggressiva)

    st.divider()
    st.subheader("📖 Documento esteso")
    doc = _load_doc()
    if doc is None:
        st.error(
            "Documento `docs/13-strategia-spiegata.md` non trovato. In "
            "locale verifica il path; in container assicurati che il "
            "Dockerfile copi `docs/` in `/app/docs/`."
        )
    else:
        st.markdown(doc, unsafe_allow_html=False)


render()
