Files
Cerbero-Bite/docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md
T
root 2a4a82c8ef docs(specs): IV-RV adaptive gate design
Spec del gate IV-RV adattivo (P25 rolling 60g + Vol-of-Vol guard 5pt
24h) — riprende roadmap §4-quater di 13-strategia-spiegata.md punti
1 e 2 e li promuove a design pronto per implementazione.

Decisioni emerse dal brainstorming:
- Hybrid (percentile rolling + VoV guard), non regime detection
- Window target 60g, min 30g, sotto usa storia disponibile (warmup)
- Floor assoluto via vecchio iv_minus_rv_min (backwards compat)
- Inline nel validator, stateless, no DB cache
- GUI Calibrazione: pannello informativo, slider esistenti invariati
- Fail-open su tutti i casi di dato mancante

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:50:56 +00:00

12 KiB
Raw Blame History

IV-RV adaptive entry gate — design

Status: drafted, awaiting implementation plan Date: 2026-05-08 Author: brainstorming session (operator + Claude) Roadmap origin: docs/13-strategia-spiegata.md §4-quater, hardening punti 1 e 2

1. Problema

Il gate IV-richness in core/entry_validator.py:140-152 confronta ctx.iv_minus_rv con la soglia statica entry.iv_minus_rv_min (config). I dati raccolti in market_snapshots mostrano due problemi sul campione 2026-05-01 → 2026-05-08:

metrica ETH BTC
IV-RV p25 1.87 5.48
IV-RV p50 2.70 6.88
IV-RV p90 8.52 8.26
Pearson(|drift%|, IV-RV mean) 0.02 +0.54
Trend giornaliero IV-RV mean 1.64 → 8.96 (+5.4×) 4.78 → 8.11 (+1.7×)

ETH mostra un IV richening monotono decoupled dal drift realised — la soglia statica min=3 (profilo Aggressiva) avrebbe escluso il 50%+ dei tick nei primi 4 giorni e il 0% degli ultimi 3, sopra una distribuzione che non è stazionaria. Su BTC è meno drammatico ma il problema è strutturale: il regime di IV cambia, la soglia no.

2. Obiettivo

Sostituire la soglia statica con un meccanismo adattivo che si auto-calibra al regime corrente, senza richiedere intervento manuale dell'operatore. Il gate deve restare semanticamente "non vendere vol senza margine misurabile sopra la RV", solo che il "margine misurabile" è ora derivato dalla distribuzione storica recente invece che hardcoded.

3. Approccio scelto: Hybrid (P25 rolling + Vol-of-Vol guard)

Decisione presa nel brainstorming dopo aver scartato:

  • Solo percentile rolling: insufficiente, non protegge da regime shift bruschi (DVOL salta di 5+ pt in 24h)
  • Solo regime detection (HMM/cluster): troppo opaco e ad alto rischio di overfit con 8 giorni di dati

L'hybrid bilancia due controlli additivi:

  1. Soglia adattiva = P25 di IV-RV nella finestra rolling
  2. Vol-of-Vol guard = blocco se |ΔDVOL_24h| ≥ 5 pt (regime shift detector)

4. Comportamento del gate

def validate_iv_richness_adaptive(ctx, cfg, repo):
    if not cfg.entry.iv_minus_rv_filter_enabled:
        return PASS                               # gate off

    # 1) Soglia adattiva
    history = repo.iv_rv_history(asset=ctx.asset, max_days=cfg.window_target_days)
    n_days = len(history) / 96                    # 96 tick/giorno

    if n_days < 1:
        return PASS                               # warmup hard: <1g, no gate

    if n_days >= cfg.window_target_days:
        window = history[-cfg.window_target_days*96:]   # ≥60g → finestra fissa 60g
    elif n_days >= cfg.window_min_days:
        window = history[-cfg.window_min_days*96:]      # 30-60g → finestra fissa 30g
    else:
        window = history                                # 1-30g → tutta la storia disponibile

    threshold = max(percentile(window, cfg.percentile),
                    cfg.absolute_floor)

    if ctx.iv_minus_rv < threshold:
        return SKIP("IV richness below P25 rolling")

    # 2) Vol-of-vol guard (additivo)
    if cfg.vol_of_vol_guard_enabled:
        dvol_24h_ago = repo.dvol_lookback(asset=ctx.asset, hours=24)
        if dvol_24h_ago is not None and \
           abs(ctx.dvol - dvol_24h_ago) >= cfg.vol_of_vol_threshold:
            return SKIP("DVOL shifted ≥5pt in 24h")

    return PASS

4.1 Warmup behavior

storia disponibile finestra usata comportamento
< 1 giorno (96 tick) gate disabled (PASS), log GATE_WARMUP_INSUFFICIENT
1 g ≤ storia < 30 g tutta la storia percentile della finestra disponibile (decisione utente)
30 g ≤ storia < 60 g ultimi 30 g finestra fissa 30g
≥ 60 g ultimi 60 g finestra fissa 60g (target)

4.2 Soglia = max(P25, floor)

floor è il vecchio iv_minus_rv_min riutilizzato come absolute floor. Permette:

  • backwards compat: se adaptive_enabled=False, comportamento identico ad oggi
  • safety: anche se P25 storico fosse ≈0 (regime IV bassa persistente), l'operatore può tenere un floor minimo (es. 1 vol pt) per evitare di vendere vol mai

5. Schema config (config/schema.py)

Aggiunte alla classe EntryConfig:

class EntryConfig(BaseModel):
    # campi esistenti
    iv_minus_rv_filter_enabled: bool = False
    iv_minus_rv_min: Decimal = Decimal("0")     # ora è absolute_floor

    # nuovi — gate adattivo
    iv_minus_rv_adaptive_enabled: bool = False
    iv_minus_rv_percentile: Decimal = Decimal("0.25")
    iv_minus_rv_window_target_days: int = 60
    iv_minus_rv_window_min_days: int = 30

    # nuovi — vol-of-vol guard
    vol_of_vol_guard_enabled: bool = False
    vol_of_vol_threshold_pt: Decimal = Decimal("5")
    vol_of_vol_lookback_hours: int = 24

5.1 Profili predefiniti

Conservativa / golden (config/golden.yaml):

entry:
  iv_minus_rv_filter_enabled: false
  iv_minus_rv_adaptive_enabled: false
  vol_of_vol_guard_enabled: false

Comportamento invariato rispetto a oggi.

Aggressiva (config/aggressive.yaml):

entry:
  iv_minus_rv_filter_enabled: true
  iv_minus_rv_adaptive_enabled: true
  iv_minus_rv_min: 0                # floor 0, lascia decidere il P25 rolling
  iv_minus_rv_percentile: 0.25
  iv_minus_rv_window_target_days: 60
  iv_minus_rv_window_min_days: 30
  vol_of_vol_guard_enabled: true
  vol_of_vol_threshold_pt: 5

5.2 Backwards compat

Se iv_minus_rv_adaptive_enabled=False e iv_minus_rv_filter_enabled=True, il validator usa il path legacy iv_rv < iv_minus_rv_min esattamente come oggi. Nessuna regressione comportamentale per chi non ha attivato l'adaptive.

6. Architettura

6.1 Nuovo modulo core/adaptive_threshold.py

Funzione pura, testabile senza I/O:

def compute_adaptive_threshold(
    history: list[Decimal],
    *,
    percentile: Decimal,
    absolute_floor: Decimal,
    min_days: int,
    target_days: int,
) -> Decimal | None:
    """Ritorna None se warmup hard (<96 tick), altrimenti max(P_q, floor)."""

6.2 Repository (state/repository.py)

Due nuovi metodi su MarketSnapshotRepository:

def iv_rv_history(self, *, asset: str, max_days: int) -> list[Decimal]:
    """IV-RV ordinato ASC, finestra max_days, fetch_ok=1, NULL filtrati."""

def dvol_lookback(self, *, asset: str, hours: int) -> Decimal | None:
    """DVOL del tick più vicino a now-hours, ±15min tolerance. None se gap."""

Usa l'index esistente idx_market_snapshots_asset_ts. Nessuna nuova migration.

6.3 Inline nel validator

core/entry_validator.py chiama compute_adaptive_threshold con i dati dal repo. Nessun caching, stateless. La query per finestra 60g (5760 righe per asset) costa ms-level con index — non vale la pena introdurre cache da invalidare.

6.4 Audit / logging

Ogni entry cycle scrive in decisions:

  • inputs_json: {iv_rv_now, threshold_used, dvol_now, dvol_24h_ago, n_history, window_used_days}
  • outputs_json: {gate: "iv_richness_adaptive", verdict: PASS|SKIP, reason}

Permette ricostruzione ex-post: perché un trade è stato saltato e con quali numeri.

7. GUI Calibrazione (pages/6_📐_Calibrazione.py)

Aggiunta sezione "Gate adattivo" sopra ai percentili statici esistenti — questi ultimi NON vengono modificati (restano per analisi).

┌─ 🎯 Gate IV-RV adattivo ──────────────────────────────────┐
│  Status:    🟢 Attivo (Aggressiva) | 🟡 Warmup (n=8/30g)  │
│                                                          │
│  Soglia P25 rolling (corrente)         2.74 vol pts      │
│  IV-RV ultimo tick                     8.96 vol pts  ✅   │
│  Floor assoluto                        0.00 vol pts      │
│                                                          │
│  Evoluzione 7g (sparkline)   ▁▂▂▃▄▆▇                     │
│                                                          │
│  ── VoV guard ──                                          │
│  ΔDVOL ultime 24h                      0.43 pt        ✅  │
│  Soglia VoV                            5.00 pt           │
│                                                          │
│  Decisione hypothetical: PASS                            │
└──────────────────────────────────────────────────────────┘

La GUI usa la stessa funzione compute_adaptive_threshold del validator → unica fonte di verità. Refresh manuale al page load (coerente con resto GUI).

8. Error handling

Principio: fail-open in tutti i casi di dato mancante. Il gate adattivo è additivo sopra ai gate hard esistenti (delta band, credit, ecc.); il dato mancante non deve trasformare un trade non voluto in trade fatto, ma neppure deve bloccare entry valide se è il dato del gate stesso a mancare.

Scenario Comportamento Loggato come
iv_rv_history ritorna 0 righe gate = PASS GATE_WARMUP_NO_DATA
Storia < 96 tick (1g) gate = PASS, threshold None GATE_WARMUP_INSUFFICIENT
dvol_lookback = None (gap dati 24h fa) VoV guard = PASS VOV_GUARD_NO_LOOKBACK
ctx.iv_minus_rv = None gate bypassato (riga 146 esistente) invariato
ctx.dvol = None VoV guard bypassato, gate adattivo prosegue invariato

9. Testing strategy

9.1 Unit — core/adaptive_threshold.py

  • Warmup: history vuota → None
  • Warmup: history < 96 tick → None
  • Sotto min_days: 200 tick → P25 sui 200
  • Tra min e target: 4000 tick → P25 sui 4000
  • Oltre target: 10000 tick → P25 sugli ultimi target_days*96
  • Floor binding: P25=0.5, floor=3 → 3
  • Floor non binding: P25=5, floor=0 → 5
  • Percentile diverso: percentile=0.5 → mediana
  • Boundary: esattamente min_days → window = min_days

9.2 Unit — core/entry_validator.py

Mock repo, focus sul flusso decisionale:

  • Adaptive disabled, statico passa → PASS legacy
  • Adaptive disabled, statico fail → SKIP legacy
  • Adaptive enabled, IV-RV sopra P25 → PASS
  • Adaptive enabled, IV-RV sotto P25 sopra floor → SKIP("rolling")
  • VoV guard ON, ΔDVOL=6 pt → SKIP("vov")
  • VoV guard ON, ΔDVOL=4 pt, gate principale pass → PASS
  • VoV guard ON, dvol_lookback=None → guard bypass
  • ctx.iv_minus_rv=None → bypass
  • decisions log popolato con threshold, n_history, dvol_lookback

9.3 Integration — tests/integration/test_entry_cycle_adaptive.py

SQLite temp + fixture market_snapshots con 5760 tick (60g):

  • Aggressiva con flag adattivo → entry passa solo nei tick sopra P25 della fixture
  • Golden → invariato
  • Warmup: DB con 50 tick → tutti pass, log GATE_WARMUP_INSUFFICIENT
  • Regime shift fixture: DVOL salta da 50 a 56 in 24h → VoV guard scatta

9.4 Backtest sanity

Aggiunta nel report del CLI backtest: count distinto skip-reasons (iv_rv_static, iv_rv_rolling, vov_guard) per analisi ex-post.

9.5 GUI smoke

Manuale al deploy:

  • Calibrazione carica con enabled=False (fallback grafico)
  • Calibrazione mostra warmup status quando DB < 30g
  • Refresh ricalcola coerente

9.6 Cosa NON testiamo

  • Performance query (5760 righe con index = trascurabile)
  • Concorrenza entry cycle / GUI (WAL abilitato)
  • Migrazione (nessuna tabella nuova)

10. Out of scope

  • Regime detection avanzato (HMM, cluster) — esplicitamente scartato per opacità
  • Soglie per-asset diverse — il P25 si calibra naturalmente per asset (history filtrata per asset)
  • Auto-attivazione adaptive su Conservativa quando warmup è completo — l'operatore decide manualmente quando passare al profilo aggressivo
  • Multi-asset (ETH+BTC simultanei) — già scope §4-ter, indipendente da questo design
  • Override manuale soglia da GUI — explicit no, l'obiettivo è autocalibrante

11. Decisioni prese durante brainstorming

# Domanda Scelta
1 Approccio Hybrid (percentile + VoV guard)
2 Warmup Percentile della finestra disponibile (anche se <30g)
3 Percentile P25 (allineato roadmap)
4 Window Target 60g, attivazione a 30g, sotto usa quel che c'è
5 VoV soglia 5 pt vol in 24h
6 Architettura Inline nel validator, stateless, no cache DB
7 GUI Pannello informativo aggiunto, slider esistenti invariati