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:
@@ -376,7 +376,12 @@ def test_adaptive_pass_when_iv_rv_above_p25() -> None:
|
||||
cfg = _adaptive_cfg()
|
||||
history = tuple(Decimal(i) for i in range(1, 201))
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("80"), iv_rv_history=history), cfg
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("80"),
|
||||
iv_rv_history=history,
|
||||
iv_rv_n_days=30,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is True
|
||||
assert not any("IV richness" in r for r in decision.reasons)
|
||||
@@ -386,16 +391,27 @@ def test_adaptive_blocks_when_iv_rv_below_p25() -> None:
|
||||
cfg = _adaptive_cfg()
|
||||
history = tuple(Decimal(i) for i in range(1, 201))
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("20"), iv_rv_history=history), cfg
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("20"),
|
||||
iv_rv_history=history,
|
||||
iv_rv_n_days=30,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness" in r and "rolling" in r for r in decision.reasons)
|
||||
|
||||
|
||||
def test_adaptive_with_empty_history_passes_warmup() -> None:
|
||||
def test_adaptive_with_n_days_zero_passes_warmup() -> None:
|
||||
"""Warmup hard: nessun giorno coperto → gate skip (fail-open)."""
|
||||
cfg = _adaptive_cfg()
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("0.1"), iv_rv_history=()), cfg
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("0.1"),
|
||||
iv_rv_history=(),
|
||||
iv_rv_n_days=0,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is True
|
||||
|
||||
@@ -404,7 +420,12 @@ def test_adaptive_with_floor_floor_binds_when_p25_low() -> None:
|
||||
cfg = _adaptive_cfg(iv_minus_rv_min=Decimal("3"))
|
||||
history = tuple(Decimal("0.5") for _ in range(200))
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("1"), iv_rv_history=history), cfg
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("1"),
|
||||
iv_rv_history=history,
|
||||
iv_rv_n_days=30,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness" in r for r in decision.reasons)
|
||||
@@ -417,7 +438,8 @@ def test_legacy_static_gate_still_works_when_adaptive_disabled() -> None:
|
||||
"iv_minus_rv_min": Decimal("3"),
|
||||
})
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("2"), iv_rv_history=()), cfg
|
||||
_good_ctx(iv_minus_rv=Decimal("2"), iv_rv_history=(), iv_rv_n_days=0),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness below floor" in r for r in decision.reasons)
|
||||
@@ -426,12 +448,45 @@ def test_legacy_static_gate_still_works_when_adaptive_disabled() -> None:
|
||||
def test_iv_minus_rv_none_skips_gate_in_both_modes() -> None:
|
||||
cfg = _adaptive_cfg()
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=None, iv_rv_history=tuple(Decimal(i) for i in range(1, 201))),
|
||||
_good_ctx(
|
||||
iv_minus_rv=None,
|
||||
iv_rv_history=tuple(Decimal(i) for i in range(1, 201)),
|
||||
iv_rv_n_days=30,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
def test_adaptive_with_n_days_one_uses_history_for_percentile() -> None:
|
||||
"""Singolo giorno disponibile (cadenza qualunque): gate attivo,
|
||||
soglia = P25 della finestra ricevuta. Dimostra che il warmup hard
|
||||
finisce a n_days=1 (non 30 come nella vecchia implementazione)."""
|
||||
cfg = _adaptive_cfg()
|
||||
history = tuple(Decimal(i) for i in range(1, 101)) # 1..100, P25 = 25.75
|
||||
# IV-RV sopra P25 → pass
|
||||
pass_decision = validate_entry(
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("30"),
|
||||
iv_rv_history=history,
|
||||
iv_rv_n_days=1,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert pass_decision.accepted is True
|
||||
# IV-RV sotto P25 → block
|
||||
block_decision = validate_entry(
|
||||
_good_ctx(
|
||||
iv_minus_rv=Decimal("10"),
|
||||
iv_rv_history=history,
|
||||
iv_rv_n_days=1,
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
assert block_decision.accepted is False
|
||||
assert any("IV richness" in r and "rolling" in r for r in block_decision.reasons)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vol-of-Vol guard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user