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:
@@ -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
|
||||
Reference in New Issue
Block a user