feat(core): IV-RV adaptive gate in validate_entry + tests
Quando iv_minus_rv_adaptive_enabled=True, la soglia diventa max(P_q rolling, iv_minus_rv_min). Path legacy (statico) e None-bypass restano invariati. Aggiunge anche due model_validator a StrategyConfig per fail-fast su config invalida (window_min_days < target_days, percentile in (0,1)) — risponde alla code review T1. Tests: pass/skip su rolling, warmup hard, floor binding, backwards compat statico, None bypass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,16 @@ class StrategyConfig(BaseModel):
|
||||
if self.entry.dvol_min >= self.entry.dvol_max:
|
||||
raise ValueError("dvol_min must be < dvol_max")
|
||||
|
||||
e = self.entry
|
||||
if e.iv_minus_rv_window_min_days >= e.iv_minus_rv_window_target_days:
|
||||
raise ValueError(
|
||||
"iv_minus_rv_window_min_days must be < iv_minus_rv_window_target_days"
|
||||
)
|
||||
if not (Decimal("0") < e.iv_minus_rv_percentile < Decimal("1")):
|
||||
raise ValueError(
|
||||
"iv_minus_rv_percentile must be in (0, 1)"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -153,16 +153,34 @@ def validate_entry(ctx: EntryContext, cfg: StrategyConfig) -> EntryDecision:
|
||||
# §2.9: IV richness gate. Vendere vol senza un margine misurabile
|
||||
# fra IV e RV è statisticamente neutro: l'edge della strategia
|
||||
# esiste solo quando il premio è "ricco" rispetto a quanto il
|
||||
# mercato si è effettivamente mosso.
|
||||
if (
|
||||
entry_cfg.iv_minus_rv_filter_enabled
|
||||
and ctx.iv_minus_rv is not None
|
||||
and ctx.iv_minus_rv < entry_cfg.iv_minus_rv_min
|
||||
):
|
||||
reasons.append(
|
||||
f"IV richness below floor "
|
||||
f"(IV-RV={ctx.iv_minus_rv} < {entry_cfg.iv_minus_rv_min} vol pts)"
|
||||
)
|
||||
# mercato si è effettivamente mosso. La modalità adattiva calcola
|
||||
# la soglia come max(P_q rolling, iv_minus_rv_min) sulla storia
|
||||
# disponibile in market_snapshots; altrimenti fallback alla
|
||||
# soglia statica `iv_minus_rv_min`.
|
||||
if entry_cfg.iv_minus_rv_filter_enabled and ctx.iv_minus_rv is not None:
|
||||
if entry_cfg.iv_minus_rv_adaptive_enabled:
|
||||
from cerbero_bite.core.adaptive_threshold import (
|
||||
compute_adaptive_threshold,
|
||||
)
|
||||
|
||||
threshold = compute_adaptive_threshold(
|
||||
history=ctx.iv_rv_history,
|
||||
percentile=entry_cfg.iv_minus_rv_percentile,
|
||||
absolute_floor=entry_cfg.iv_minus_rv_min,
|
||||
min_days=entry_cfg.iv_minus_rv_window_min_days,
|
||||
target_days=entry_cfg.iv_minus_rv_window_target_days,
|
||||
)
|
||||
if threshold is not None and ctx.iv_minus_rv < threshold:
|
||||
pct = int(entry_cfg.iv_minus_rv_percentile * 100)
|
||||
reasons.append(
|
||||
f"IV richness below P{pct} rolling "
|
||||
f"(IV-RV={ctx.iv_minus_rv} < {threshold} vol pts)"
|
||||
)
|
||||
elif ctx.iv_minus_rv < entry_cfg.iv_minus_rv_min:
|
||||
reasons.append(
|
||||
f"IV richness below floor "
|
||||
f"(IV-RV={ctx.iv_minus_rv} < {entry_cfg.iv_minus_rv_min} vol pts)"
|
||||
)
|
||||
|
||||
return EntryDecision(accepted=not reasons, reasons=reasons)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user