refactor(core): IV-RV adattivo distinct-days policy + backfill Deribit
Sblocca il warmup hard del gate IV-RV adattivo (~21 giorni residui)
permettendo di mischiare cadenze diverse (tick live 15min + backfill
giornaliero) senza assumere il fattore costante 96 tick/giorno.
API change (no backwards-compat shims):
* compute_adaptive_threshold(history, *, n_days, percentile,
absolute_floor): rimossi `min_days`/`target_days`. La selezione
finestra (target_days/min_days/intera storia) si sposta al caller.
Warmup hard quando `n_days == 0`.
* repository: rimosso `iv_rv_history`; aggiunti
`count_iv_rv_distinct_days` (COUNT DISTINCT substr(ts,1,10)) e
`iv_rv_values_for_window`.
* EntryContext aggiunge `iv_rv_n_days: int = 0`. entry_cycle calcola
n_days, sceglie window_days e popola il context. Audit
`iv_rv_n_days` reale (non più len/96).
* GUI Calibrazione: counter giorni distinti tramite set di date.
* Spec aggiornata con errata 2026-05-10 e nuova warmup table.
Backfill (scripts/backfill_iv_rv.py, stdlib-only):
* Fetch DVOL daily + ETH/BTC-PERPETUAL closes da Deribit public REST.
* Calcolo RV30d annualizzato (stdev log-return × √365 × 100).
* INSERT OR REPLACE in market_snapshots con timestamp 12:00 UTC e
fetch_errors_json='{"backfill":true}' per distinzione audit.
* Compute layer testato (9 test): RV su prezzi costanti/monotoni/
alternati, build_records con cutoff e missing data.
Verifica live post-deploy (10 mag 2026 08:50 UTC):
* ETH: n_days=46, P25=2.21 vol pt, IV-RV=10.05 → gate PASS
* BTC: n_days=46, P25=5.69 vol pt, IV-RV=8.60 → gate PASS
509 test passati (500 esistenti + 9 backfill), ruff pulito.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,26 +35,33 @@ L'hybrid bilancia due controlli additivi:
|
||||
|
||||
## 4. Comportamento del gate
|
||||
|
||||
> **Errata 2026-05-10** — design originale assumeva `n_days = len(history) // 96` (cadenza fissa 96 tick/giorno). Refattorizzato a **distinct-days policy**: il caller interroga il repository per (a) il numero di giorni di calendario distinti coperti e (b) i valori della finestra scelta. Questo permette di mischiare cadenze (tick live 15 min + backfill daily) senza assumere un fattore costante. Sotto il pseudo-codice aggiornato.
|
||||
|
||||
```
|
||||
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
|
||||
# 1) Soglia adattiva — distinct-days policy
|
||||
n_days = repo.count_iv_rv_distinct_days(
|
||||
asset=ctx.asset, max_days=cfg.window_target_days,
|
||||
)
|
||||
|
||||
if n_days < 1:
|
||||
return PASS # warmup hard: <1g, no gate
|
||||
return PASS # warmup hard: nessun giorno coperto
|
||||
|
||||
if n_days >= cfg.window_target_days:
|
||||
window = history[-cfg.window_target_days*96:] # ≥60g → finestra fissa 60g
|
||||
window_days = cfg.window_target_days # ≥60g → finestra fissa 60g
|
||||
elif n_days >= cfg.window_min_days:
|
||||
window = history[-cfg.window_min_days*96:] # 30-60g → finestra fissa 30g
|
||||
window_days = cfg.window_min_days # 30-60g → finestra fissa 30g
|
||||
else:
|
||||
window = history # 1-30g → tutta la storia disponibile
|
||||
window_days = cfg.window_target_days # 1-30g → query tutta la storia disp.
|
||||
|
||||
threshold = max(percentile(window, cfg.percentile),
|
||||
history = repo.iv_rv_values_for_window(
|
||||
asset=ctx.asset, window_days=window_days,
|
||||
)
|
||||
|
||||
threshold = max(percentile(history, cfg.percentile),
|
||||
cfg.absolute_floor)
|
||||
|
||||
if ctx.iv_minus_rv < threshold:
|
||||
@@ -72,13 +79,17 @@ def validate_iv_richness_adaptive(ctx, cfg, repo):
|
||||
|
||||
### 4.1 Warmup behavior
|
||||
|
||||
Tutte le soglie sono espresse in **giorni di calendario distinti** coperti da almeno un record valido (`fetch_ok=1` ∧ `iv_minus_rv IS NOT NULL`).
|
||||
|
||||
| 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 |
|
||||
| 0 giorni distinti | — | gate disabled (PASS), log `GATE_WARMUP_INSUFFICIENT` |
|
||||
| 1 g ≤ giorni < 30 g | tutta la storia | percentile della finestra disponibile (decisione utente) |
|
||||
| 30 g ≤ giorni < 60 g | ultimi 30 g | finestra fissa 30g |
|
||||
| ≥ 60 g | ultimi 60 g | finestra fissa 60g (target) |
|
||||
|
||||
I valori della finestra contribuiscono uno-a-uno al percentile: un tick a 15 min e un record di backfill daily hanno lo stesso peso. Mix di cadenze diverse è statisticamente sbilanciato finché i tick live non saturano la finestra; questa è una scelta deliberata per non rinunciare allo storico backfill.
|
||||
|
||||
### 4.2 Soglia = `max(P25, floor)`
|
||||
|
||||
`floor` è il vecchio `iv_minus_rv_min` riutilizzato come *absolute floor*. Permette:
|
||||
@@ -137,29 +148,38 @@ Se `iv_minus_rv_adaptive_enabled=False` e `iv_minus_rv_filter_enabled=True`, il
|
||||
|
||||
## 6. Architettura
|
||||
|
||||
### 6.1 Nuovo modulo `core/adaptive_threshold.py`
|
||||
### 6.1 Modulo `core/adaptive_threshold.py`
|
||||
|
||||
Funzione pura, testabile senza I/O:
|
||||
Funzione pura, testabile senza I/O. La selezione della finestra è
|
||||
delegata al caller (separation of concerns):
|
||||
|
||||
```python
|
||||
def compute_adaptive_threshold(
|
||||
history: list[Decimal],
|
||||
history: Sequence[Decimal],
|
||||
*,
|
||||
n_days: int,
|
||||
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)."""
|
||||
"""Ritorna None se warmup hard (n_days==0 o history vuota),
|
||||
altrimenti max(P_q(history), absolute_floor)."""
|
||||
```
|
||||
|
||||
### 6.2 Repository (`state/repository.py`)
|
||||
|
||||
Due nuovi metodi su `MarketSnapshotRepository`:
|
||||
Tre metodi su `Repository` (uno preesistente):
|
||||
|
||||
```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 count_iv_rv_distinct_days(
|
||||
self, *, asset: str, max_days: int, as_of: datetime | None = None,
|
||||
) -> int:
|
||||
"""Numero di giorni di calendario distinti con almeno un IV-RV
|
||||
valido nell'intervallo [as_of - max_days, as_of]."""
|
||||
|
||||
def iv_rv_values_for_window(
|
||||
self, *, asset: str, window_days: int, as_of: datetime | None = None,
|
||||
) -> list[Decimal]:
|
||||
"""Valori IV-RV ordinati ASC su [as_of - window_days, as_of]."""
|
||||
|
||||
def dvol_lookback(self, *, asset: str, hours: int) -> Decimal | None:
|
||||
"""DVOL del tick più vicino a now-hours, ±15min tolerance. None se gap."""
|
||||
@@ -219,15 +239,23 @@ Principio: **fail-open** in tutti i casi di dato mancante. Il gate adattivo è a
|
||||
|
||||
### 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`
|
||||
- Warmup: `n_days=0` → None
|
||||
- Warmup difensivo: `n_days=0` ma history non vuota → None
|
||||
- Difensivo: history vuota con `n_days>0` → None
|
||||
- `n_days=1`, 96 tick → P25 sui 96
|
||||
- Mix di cadenze (30 daily + 96 live) → percentile uno-a-uno
|
||||
- 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
|
||||
- Validation: percentile ∉ [0,1] o `n_days<0` → ValueError
|
||||
|
||||
### 9.1bis Unit — `state/repository.py`
|
||||
|
||||
- `count_iv_rv_distinct_days`: 1 giorno → 1; 3 giorni misti → 3
|
||||
- esclusione asset diversi, NULL e fetch_ok=0
|
||||
- rispetto del cutoff `max_days`
|
||||
- ValueError su `as_of` naive o `max_days≤0`
|
||||
- `iv_rv_values_for_window`: ordine ASC, filtri equivalenti, ValueError input
|
||||
|
||||
### 9.2 Unit — `core/entry_validator.py`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user