"""End-to-end test del gate IV-RV adattivo + Vol-of-Vol guard via Repository. Verifica che la nuova API distinct-days componga correttamente repository helpers + ``compute_adaptive_threshold``. """ from __future__ import annotations from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest from cerbero_bite.core.adaptive_threshold import compute_adaptive_threshold from cerbero_bite.state.db import connect, run_migrations from cerbero_bite.state.models import MarketSnapshotRecord from cerbero_bite.state.repository import Repository def _seed_history( conn, repo: Repository, asset: str, base: datetime, n_ticks: int, iv_rv_value: Decimal, dvol_value: Decimal, ) -> None: for i in range(n_ticks): repo.record_market_snapshot( conn, MarketSnapshotRecord( timestamp=base + timedelta(minutes=15 * i), asset=asset, spot=Decimal("2000"), dvol=dvol_value, realized_vol_30d=Decimal("48"), iv_minus_rv=iv_rv_value, funding_perp_annualized=Decimal("0"), funding_cross_annualized=Decimal("0"), dealer_net_gamma=Decimal("0"), gamma_flip_level=None, oi_delta_pct_4h=None, liquidation_long_risk="low", liquidation_short_risk="low", macro_days_to_event=None, fetch_ok=True, fetch_errors_json=None, ), ) conn.commit() @pytest.fixture def db_30d(tmp_path): """30 giorni di storia con IV-RV bimodale: prima metà 1.0, seconda metà 5.0.""" db_path = tmp_path / "e2e.sqlite" conn = connect(str(db_path)) run_migrations(conn) repo = Repository() base = datetime(2026, 4, 1, 0, 0, tzinfo=UTC) _seed_history(conn, repo, "ETH", base, 1440, Decimal("1.0"), Decimal("50")) _seed_history( conn, repo, "ETH", base + timedelta(days=15), 1440, Decimal("5.0"), Decimal("50"), ) return conn, repo def test_distinct_days_count_matches_calendar_days(db_30d) -> None: """30 giorni di calendario seedati → COUNT DISTINCT = 30.""" conn, repo = db_30d n = repo.count_iv_rv_distinct_days( conn, asset="ETH", max_days=60, as_of=datetime(2026, 5, 1, 0, 0, tzinfo=UTC), ) assert n == 30 def test_window_values_returned_for_full_history(db_30d) -> None: conn, repo = db_30d values = repo.iv_rv_values_for_window( conn, asset="ETH", window_days=60, as_of=datetime(2026, 5, 1, 0, 0, tzinfo=UTC), ) assert len(values) == 2880 # Bimodale: 1440 valori 1.0 e 1440 valori 5.0 assert sum(1 for v in values if v == Decimal("1.0")) == 1440 assert sum(1 for v in values if v == Decimal("5.0")) == 1440 def test_p25_of_bimodal_history_picks_low_regime(db_30d) -> None: """Comporre repository + adaptive_threshold come fa entry_cycle.""" conn, repo = db_30d as_of = datetime(2026, 5, 1, 0, 0, tzinfo=UTC) n_days = repo.count_iv_rv_distinct_days( conn, asset="ETH", max_days=60, as_of=as_of ) values = repo.iv_rv_values_for_window( conn, asset="ETH", window_days=60, as_of=as_of ) threshold = compute_adaptive_threshold( history=values, n_days=n_days, percentile=Decimal("0.25"), absolute_floor=Decimal("0"), ) # P25 di 2880 valori bimodali: 1440 ×1.0, 1440 ×5.0 → soglia = 1.0 assert threshold == Decimal("1.0") def test_dvol_lookback_within_tolerance(db_30d) -> None: conn, repo = db_30d base = datetime(2026, 4, 1, 0, 0, tzinfo=UTC) out = repo.dvol_lookback(conn, asset="ETH", reference=base + timedelta(hours=24)) assert out == Decimal("50") def test_dvol_lookback_returns_none_outside_tolerance(db_30d) -> None: conn, repo = db_30d out = repo.dvol_lookback( conn, asset="ETH", reference=datetime(2025, 1, 1, tzinfo=UTC), tolerance_minutes=15, ) assert out is None