feat(entry): IV richness gate (§2.9) + golden config bump 1.0.0 → 1.1.0
Aggiunge il filtro a maggior impatto sul win-rate atteso: l'entry
salta se la IV implicita non sta pagando un margine misurabile sopra
la realized vol. La letteratura short-vol systematic indica che
l'edge sostenibile della strategia esiste solo quando IV30g − RV30g
supera una soglia di alcuni punti vol; senza questo gate il selling
vol nudo è strutturalmente neutro a win-rate 70-72%.
Implementazione end-to-end:
- `EntryConfig`: due nuovi campi `iv_minus_rv_min` e
`iv_minus_rv_filter_enabled`, con default `0` / `false` per non
rompere setup pre-calibrazione.
- `validate_entry`: §2.9 hard gate che blocca l'entry se
`iv_minus_rv < iv_minus_rv_min` (skip silenzioso quando il dato è
`None`, coerente con il pattern §2.8 dei filtri quant).
- `entry_cycle._gather_snapshot`: nuovo `_safe_iv_minus_rv` che
legge `deribit.realized_vol("ETH")["iv_minus_rv_30d"]` in
best-effort e lo propaga via `_MarketSnapshot.iv_minus_rv` →
`EntryContext.iv_minus_rv` → audit `inputs.snapshot.iv_minus_rv`.
- `tests/unit/test_entry_validator.py`: 5 nuovi casi (default
permissivo, gate sotto/sopra/uguale soglia, dato mancante).
- `tests/integration/test_entry_cycle.py`: stub `get_realized_vol`
nel mock helper così tutti gli scenari di happy/edge path
continuano a passare.
Configurazione di profili coerente con la disciplina:
- `strategy.yaml` (golden 1.1.0) e `strategy.conservativa.yaml`:
gate `enabled=false, min=0`. Manteniamo i lunedì pre-calibrazione
per accumulare dati sulla distribuzione di `iv_minus_rv`.
- `strategy.aggressiva.yaml` (1.1.0-aggressiva): gate
`enabled=true, min=3`. Coerente con la filosofia del profilo —
size più grande pretende win-rate più alto. La soglia 3 è
conservativa; la documentazione raccomanda 5 dopo 4-8 settimane di
calibrazione.
Doc + GUI:
- `docs/13-strategia-spiegata.md` §4-quater: spiega gate, parametri,
default per profilo, effetto atteso sul P/L (trade/anno scendono
ma E[trade] sale → APR cresce comunque), roadmap di hardening
(soglia adattiva, vol-of-vol guard, multi-asset).
- pagina `📚 Strategia`: la riga "IV − RV" passa da informativa a
pass/fail reale; mostra "filtro DISABILITATO (info-only)" quando
spento, ✅/❌ contro la soglia di config quando acceso.
Bump versioni e hash di tutti e tre i file YAML
(`config_version: 1.1.0`, hash ricalcolato). Test pinning aggiornato
(`test_load_repo_strategy_yaml`).
Suite: 410 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,62 @@ def test_dealer_gamma_filter_disabled_in_config(cfg: StrategyConfig) -> None:
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IV richness gate (§2.9)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _strict_iv_rv_cfg(
|
||||
cfg: StrategyConfig, *, threshold: Decimal = Decimal("5")
|
||||
) -> StrategyConfig:
|
||||
return golden_config(
|
||||
entry=EntryConfig(
|
||||
**{
|
||||
**cfg.entry.model_dump(),
|
||||
"iv_minus_rv_filter_enabled": True,
|
||||
"iv_minus_rv_min": threshold,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_iv_richness_gate_disabled_by_default_lets_thin_premium_pass(
|
||||
cfg: StrategyConfig,
|
||||
) -> None:
|
||||
# Default config: filter disabled. Anche con IV-RV negativa (RV>IV)
|
||||
# l'entry deve passare per non rompere setup pre-calibrazione.
|
||||
decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("-2")), cfg)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
def test_iv_richness_gate_blocks_when_below_floor(cfg: StrategyConfig) -> None:
|
||||
strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
|
||||
decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("3")), strict)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness" in r for r in decision.reasons)
|
||||
|
||||
|
||||
def test_iv_richness_gate_passes_when_above_floor(cfg: StrategyConfig) -> None:
|
||||
strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
|
||||
decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("6")), strict)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
def test_iv_richness_gate_passes_at_exact_threshold(cfg: StrategyConfig) -> None:
|
||||
# Soglia inclusiva: IV-RV == soglia → accettato (gate è "<", non "<=").
|
||||
strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
|
||||
decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("5")), strict)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
def test_iv_richness_gate_skipped_when_data_missing(cfg: StrategyConfig) -> None:
|
||||
# MCP irraggiungibile: best-effort skip, non bloccare l'entry per
|
||||
# un problema di infrastruttura.
|
||||
strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
|
||||
decision = validate_entry(_good_ctx(iv_minus_rv=None), strict)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
def test_validate_entry_accumulates_all_reasons(cfg: StrategyConfig) -> None:
|
||||
decision = validate_entry(
|
||||
_good_ctx(
|
||||
|
||||
Reference in New Issue
Block a user