feat(state): dvol_history multi-asset (ETH+BTC) + backfill ETH legacy rows
Migration 0006 promuove dvol_history da PK=(timestamp) mono-ETH a PK=(timestamp, asset), rinomina eth_spot -> spot, e backfilla con asset='ETH' le righe storiche. market_snapshot_cycle ora scrive sia per ETH che per BTC; monitor_cycle resta ETH-only via WHERE asset='ETH' nella lookup di return_4h. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,9 @@ def _ctx(tmp_path: Path) -> MagicMock:
|
||||
|
||||
# Default: every feed succeeds with sane mock values.
|
||||
ctx.deribit = MagicMock()
|
||||
ctx.deribit.spot_perp_price = AsyncMock(return_value=Decimal("3000"))
|
||||
ctx.deribit.spot_perp_price = AsyncMock(
|
||||
side_effect=lambda asset: Decimal("65000") if asset == "BTC" else Decimal("3000")
|
||||
)
|
||||
ctx.deribit.latest_dvol = AsyncMock(return_value=Decimal("55"))
|
||||
ctx.deribit.realized_vol = AsyncMock(
|
||||
return_value={
|
||||
@@ -181,31 +183,35 @@ def _read_dvol_history(ctx: MagicMock) -> list[dict]:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_eth_snapshot_mirrors_into_dvol_history(tmp_path: Path) -> None:
|
||||
async def test_snapshot_mirrors_each_asset_into_dvol_history(tmp_path: Path) -> None:
|
||||
ctx = _ctx(tmp_path)
|
||||
await collect_market_snapshot(ctx, assets=("ETH", "BTC"), now=_now())
|
||||
rows = _read_dvol_history(ctx)
|
||||
assert len(rows) == 1
|
||||
assert Decimal(str(rows[0]["dvol"])) == Decimal("55")
|
||||
assert Decimal(str(rows[0]["eth_spot"])) == Decimal("3000")
|
||||
by_asset = {r["asset"]: r for r in rows}
|
||||
assert set(by_asset) == {"ETH", "BTC"}
|
||||
assert Decimal(str(by_asset["ETH"]["spot"])) == Decimal("3000")
|
||||
assert Decimal(str(by_asset["BTC"]["spot"])) == Decimal("65000")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_btc_only_snapshot_does_not_touch_dvol_history(
|
||||
async def test_btc_only_snapshot_mirrors_into_dvol_history(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
ctx = _ctx(tmp_path)
|
||||
await collect_market_snapshot(ctx, assets=("BTC",), now=_now())
|
||||
assert _read_dvol_history(ctx) == []
|
||||
rows = _read_dvol_history(ctx)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["asset"] == "BTC"
|
||||
assert Decimal(str(rows[0]["spot"])) == Decimal("65000")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_eth_snapshot_skips_dvol_history_when_dvol_missing(
|
||||
async def test_snapshot_skips_dvol_history_when_dvol_missing(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
ctx = _ctx(tmp_path)
|
||||
ctx.deribit.latest_dvol = AsyncMock(side_effect=RuntimeError("no dvol"))
|
||||
await collect_market_snapshot(ctx, assets=("ETH",), now=_now())
|
||||
# market_snapshots row still persisted, but dvol_history must stay empty
|
||||
# because its schema enforces NOT NULL on dvol/eth_spot.
|
||||
# because its schema enforces NOT NULL on dvol/spot.
|
||||
assert _read_dvol_history(ctx) == []
|
||||
|
||||
Reference in New Issue
Block a user