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:
@@ -351,3 +351,82 @@ def test_bias_zero_division_safe(cfg: StrategyConfig) -> None:
|
||||
# eth_30d_ago == 0 must not crash; treat as no-bias (neutral)
|
||||
ctx = _trend(eth_now="3000", eth_30d_ago="0", funding_cross="0", dvol="60", adx="15")
|
||||
assert compute_bias(ctx, cfg) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IV-RV adaptive gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _adaptive_cfg(**entry_overrides: object) -> StrategyConfig:
|
||||
"""Golden config con gate adattivo abilitato di default per test."""
|
||||
base_entry: dict[str, object] = {
|
||||
"iv_minus_rv_filter_enabled": True,
|
||||
"iv_minus_rv_adaptive_enabled": True,
|
||||
"iv_minus_rv_min": Decimal("0"),
|
||||
"iv_minus_rv_percentile": Decimal("0.25"),
|
||||
"iv_minus_rv_window_target_days": 60,
|
||||
"iv_minus_rv_window_min_days": 30,
|
||||
}
|
||||
base_entry.update(entry_overrides)
|
||||
return golden_config(entry=base_entry)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
assert decision.accepted is True
|
||||
assert not any("IV richness" in r for r in decision.reasons)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
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:
|
||||
cfg = _adaptive_cfg()
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("0.1"), iv_rv_history=()), cfg
|
||||
)
|
||||
assert decision.accepted is True
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness" in r for r in decision.reasons)
|
||||
|
||||
|
||||
def test_legacy_static_gate_still_works_when_adaptive_disabled() -> None:
|
||||
cfg = golden_config(entry={
|
||||
"iv_minus_rv_filter_enabled": True,
|
||||
"iv_minus_rv_adaptive_enabled": False,
|
||||
"iv_minus_rv_min": Decimal("3"),
|
||||
})
|
||||
decision = validate_entry(
|
||||
_good_ctx(iv_minus_rv=Decimal("2"), iv_rv_history=()), cfg
|
||||
)
|
||||
assert decision.accepted is False
|
||||
assert any("IV richness below floor" in r for r in decision.reasons)
|
||||
|
||||
|
||||
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))),
|
||||
cfg,
|
||||
)
|
||||
assert decision.accepted is True
|
||||
|
||||
Reference in New Issue
Block a user