feat(state): Repository.iv_rv_history + dvol_lookback per gate adaptive

Due nuovi metodi che leggono market_snapshots filtrando NULL e
fetch_ok=0. iv_rv_history limita a max_days; dvol_lookback trova
il tick più vicino a un istante con tolerance configurabile.

Tests: ordered ASC, asset filter, NULL skip, fetch_ok=0 skip,
lookback closest, gap returns None.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-08 22:53:19 +00:00
parent d36cdff609
commit 395191ea13
2 changed files with 197 additions and 1 deletions
+138
View File
@@ -0,0 +1,138 @@
"""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)
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)
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)
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)
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