feat(core): Vol-of-Vol guard in validate_entry + tests
Blocca entry se |DVOL_now - DVOL_24h_ago| >= threshold (default 5 pt). Fail-open quando dvol_24h_ago è None (gap dati). Independente dal gate IV-RV: i due gate sono additivi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,20 @@ def validate_entry(ctx: EntryContext, cfg: StrategyConfig) -> EntryDecision:
|
|||||||
f"(IV-RV={ctx.iv_minus_rv} < {entry_cfg.iv_minus_rv_min} vol pts)"
|
f"(IV-RV={ctx.iv_minus_rv} < {entry_cfg.iv_minus_rv_min} vol pts)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# §4-quater roadmap: vol-of-vol guard. Blocca entry quando il
|
||||||
|
# regime di volatilità sta cambiando bruscamente, anche se IV-RV
|
||||||
|
# è alto. Fail-open su gap dati 24h fa.
|
||||||
|
if (
|
||||||
|
entry_cfg.vol_of_vol_guard_enabled
|
||||||
|
and ctx.dvol_24h_ago is not None
|
||||||
|
):
|
||||||
|
delta = abs(ctx.dvol_now - ctx.dvol_24h_ago)
|
||||||
|
if delta >= entry_cfg.vol_of_vol_threshold_pt:
|
||||||
|
reasons.append(
|
||||||
|
f"DVOL shifted {delta} pt in {entry_cfg.vol_of_vol_lookback_hours}h "
|
||||||
|
f"(threshold {entry_cfg.vol_of_vol_threshold_pt})"
|
||||||
|
)
|
||||||
|
|
||||||
return EntryDecision(accepted=not reasons, reasons=reasons)
|
return EntryDecision(accepted=not reasons, reasons=reasons)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -430,3 +430,54 @@ def test_iv_minus_rv_none_skips_gate_in_both_modes() -> None:
|
|||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
assert decision.accepted is True
|
assert decision.accepted is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vol-of-Vol guard
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _vov_cfg(threshold: Decimal = Decimal("5")) -> StrategyConfig:
|
||||||
|
return golden_config(entry={
|
||||||
|
"vol_of_vol_guard_enabled": True,
|
||||||
|
"vol_of_vol_threshold_pt": threshold,
|
||||||
|
"vol_of_vol_lookback_hours": 24,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_vov_guard_blocks_on_large_dvol_shift() -> None:
|
||||||
|
cfg = _vov_cfg()
|
||||||
|
decision = validate_entry(
|
||||||
|
_good_ctx(dvol_now=Decimal("56"), dvol_24h_ago=Decimal("50")), cfg
|
||||||
|
)
|
||||||
|
assert decision.accepted is False
|
||||||
|
assert any("DVOL shifted" in r for r in decision.reasons)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vov_guard_passes_on_small_dvol_shift() -> None:
|
||||||
|
cfg = _vov_cfg()
|
||||||
|
decision = validate_entry(
|
||||||
|
_good_ctx(dvol_now=Decimal("52"), dvol_24h_ago=Decimal("50")), cfg
|
||||||
|
)
|
||||||
|
assert decision.accepted is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_vov_guard_passes_when_lookback_missing() -> None:
|
||||||
|
"""fail-open su gap dati: se dvol_24h_ago=None il guard non scatta."""
|
||||||
|
cfg = _vov_cfg()
|
||||||
|
decision = validate_entry(
|
||||||
|
_good_ctx(dvol_now=Decimal("99"), dvol_24h_ago=None), cfg
|
||||||
|
)
|
||||||
|
# dvol_now=99 sarebbe oltre dvol_max=90; testiamo solo l'effetto VoV
|
||||||
|
# consultando le reasons (dvol_now potrebbe avere altre reason ma non
|
||||||
|
# quella VoV).
|
||||||
|
assert not any("DVOL shifted" in r for r in decision.reasons)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vov_guard_disabled_does_nothing() -> None:
|
||||||
|
cfg = golden_config(entry={"vol_of_vol_guard_enabled": False})
|
||||||
|
decision = validate_entry(
|
||||||
|
_good_ctx(dvol_now=Decimal("55"), dvol_24h_ago=Decimal("50")), cfg
|
||||||
|
)
|
||||||
|
# Nessuna reason VoV (il delta=5 sarebbe oltre soglia se attivo)
|
||||||
|
assert not any("DVOL shifted" in r for r in decision.reasons)
|
||||||
|
|||||||
Reference in New Issue
Block a user