"""TDD per Repository.iv_rv_history e Repository.dvol_lookback.""" from __future__ import annotations import sqlite3 from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest from cerbero_bite.state.db import connect, run_migrations from cerbero_bite.state.models import MarketSnapshotRecord from cerbero_bite.state.repository import Repository @pytest.fixture def db_with_history(tmp_path) -> sqlite3.Connection: """SQLite temp con 96 tick ETH a 15min ciascuno (1 giorno) e fetch_ok=1.""" db_path = tmp_path / "test.sqlite" conn = connect(str(db_path)) run_migrations(conn) repo = Repository() base = datetime(2026, 5, 1, 0, 0, tzinfo=UTC) for i in range(96): repo.record_market_snapshot( conn, MarketSnapshotRecord( timestamp=base + timedelta(minutes=15 * i), asset="ETH", spot=Decimal("2000"), dvol=Decimal("50") + Decimal(i) / Decimal("10"), realized_vol_30d=Decimal("48"), iv_minus_rv=Decimal("2") + Decimal(i) / Decimal("100"), 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() return conn def test_iv_rv_history_returns_ordered_asc(db_with_history) -> None: repo = Repository() history = repo.iv_rv_history( db_with_history, asset="ETH", max_days=60, as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC), ) assert len(history) == 96 assert history == sorted(history) assert history[0] == Decimal("2.00") def test_iv_rv_history_filters_other_asset(db_with_history) -> None: repo = Repository() history = repo.iv_rv_history( db_with_history, asset="BTC", max_days=60, as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC), ) assert history == [] def test_iv_rv_history_skips_null_values(db_with_history) -> None: repo = Repository() repo.record_market_snapshot( db_with_history, MarketSnapshotRecord( timestamp=datetime(2026, 5, 2, 0, 0, tzinfo=UTC), asset="ETH", spot=Decimal("2000"), dvol=Decimal("50"), realized_vol_30d=None, iv_minus_rv=None, 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, ), ) db_with_history.commit() history = repo.iv_rv_history( db_with_history, asset="ETH", max_days=60, as_of=datetime(2026, 5, 3, 0, 0, tzinfo=UTC), ) assert len(history) == 96 def test_iv_rv_history_skips_fetch_failed(db_with_history) -> None: repo = Repository() repo.record_market_snapshot( db_with_history, MarketSnapshotRecord( timestamp=datetime(2026, 5, 3, 0, 0, tzinfo=UTC), asset="ETH", spot=Decimal("2000"), dvol=Decimal("50"), realized_vol_30d=None, iv_minus_rv=Decimal("99"), funding_perp_annualized=Decimal("0"), funding_cross_annualized=Decimal("0"), dealer_net_gamma=None, gamma_flip_level=None, oi_delta_pct_4h=None, liquidation_long_risk=None, liquidation_short_risk=None, macro_days_to_event=None, fetch_ok=False, fetch_errors_json='{"x":"y"}', ), ) db_with_history.commit() history = repo.iv_rv_history( db_with_history, asset="ETH", max_days=60, as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC), ) assert Decimal("99") not in history def test_dvol_lookback_returns_closest_tick(db_with_history) -> None: repo = Repository() base = datetime(2026, 5, 1, 0, 0, tzinfo=UTC) target = base + timedelta(hours=12) out = repo.dvol_lookback( db_with_history, asset="ETH", reference=target, tolerance_minutes=15 ) # i=48 → dvol = 50 + 4.8 = 54.8 assert out == Decimal("54.8") def test_dvol_lookback_returns_none_when_gap(db_with_history) -> None: repo = Repository() target = datetime(2025, 1, 1, 0, 0, tzinfo=UTC) out = repo.dvol_lookback( db_with_history, asset="ETH", reference=target, tolerance_minutes=15 ) assert out is None def test_iv_rv_history_rejects_non_positive_max_days(db_with_history) -> None: repo = Repository() with pytest.raises(ValueError, match="max_days must be positive"): repo.iv_rv_history( db_with_history, asset="ETH", max_days=0, as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC), ) def test_iv_rv_history_rejects_naive_as_of(db_with_history) -> None: repo = Repository() with pytest.raises(ValueError, match="timezone-aware"): repo.iv_rv_history( db_with_history, asset="ETH", max_days=60, as_of=datetime(2026, 5, 2, 0, 0), # naive ) def test_dvol_lookback_rejects_naive_reference(db_with_history) -> None: repo = Repository() with pytest.raises(ValueError, match="timezone-aware"): repo.dvol_lookback( db_with_history, asset="ETH", reference=datetime(2026, 5, 1, 12, 0), # naive )