From 2a4a82c8efc7f6cd18d8845735955a86001d7268 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 May 2026 19:50:56 +0000 Subject: [PATCH] docs(specs): IV-RV adaptive gate design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026-05-08-iv-rv-adaptive-gate-design.md | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md diff --git a/docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md b/docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md new file mode 100644 index 0000000..135f6ca --- /dev/null +++ b/docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md @@ -0,0 +1,288 @@ +# 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`: + +```python +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`): +```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`): +```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: + +```python +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`: + +```python +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 |