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:
root
2026-05-12 13:38:34 +00:00
parent 76d1a4a32d
commit 19695e4730
10 changed files with 111 additions and 33 deletions
@@ -181,19 +181,18 @@ async def collect_market_snapshot(
try:
with transaction(conn):
ctx.repository.record_market_snapshot(conn, record)
# Mirror ETH spot+DVOL into dvol_history so monitor_cycle's
# return_4h lookup has local samples even in data-only mode.
if (
record.asset == "ETH"
and record.spot is not None
and record.dvol is not None
):
# Mirror spot+DVOL into dvol_history (per asset) so
# monitor_cycle's return_4h lookup has local samples even
# in data-only mode. dvol_history enforces NOT NULL on
# dvol/spot so skip if either is missing.
if record.spot is not None and record.dvol is not None:
ctx.repository.record_dvol_snapshot(
conn,
DvolSnapshot(
timestamp=record.timestamp,
asset=record.asset,
dvol=record.dvol,
eth_spot=record.spot,
spot=record.spot,
),
)
finally:
+3 -3
View File
@@ -173,8 +173,8 @@ async def _fetch_return_4h(ctx: RuntimeContext, *, now: datetime) -> Decimal:
conn = connect_state(ctx.db_path)
try:
row = conn.execute(
"SELECT timestamp, eth_spot FROM dvol_history "
"WHERE timestamp <= ? AND timestamp >= ? "
"SELECT timestamp, spot FROM dvol_history "
"WHERE asset = 'ETH' AND timestamp <= ? AND timestamp >= ? "
"ORDER BY timestamp DESC LIMIT 1",
(cutoff.isoformat(), floor.isoformat()),
).fetchone()
@@ -239,7 +239,7 @@ async def run_monitor_cycle(
with transaction(conn):
ctx.repository.record_dvol_snapshot(
conn,
DvolSnapshot(timestamp=when, dvol=dvol, eth_spot=spot),
DvolSnapshot(timestamp=when, asset="ETH", dvol=dvol, spot=spot),
)
positions = ctx.repository.list_positions(conn, status="open")
finally:
@@ -0,0 +1,30 @@
-- 0006_dvol_history_multi_asset.sql — promote dvol_history to multi-asset
--
-- Original schema (0001_init.sql) treated dvol_history as ETH-only:
-- PRIMARY KEY (timestamp) and a column named eth_spot. With the
-- orchestrator now snapshotting BTC in addition to ETH (commit
-- e978a44), the table needs an asset dimension so we can store a
-- DVOL/spot sample per asset per tick.
--
-- Forward-only. The 1028 existing rows are all ETH (the only writer
-- was the ETH branch of market_snapshot_cycle) so we backfill
-- asset='ETH' before swapping the table in place.
CREATE TABLE dvol_history_v2 (
timestamp TEXT NOT NULL,
asset TEXT NOT NULL,
dvol NUMERIC NOT NULL,
spot NUMERIC NOT NULL,
PRIMARY KEY (timestamp, asset)
);
INSERT INTO dvol_history_v2(timestamp, asset, dvol, spot)
SELECT timestamp, 'ETH', dvol, eth_spot FROM dvol_history;
DROP TABLE dvol_history;
ALTER TABLE dvol_history_v2 RENAME TO dvol_history;
CREATE INDEX idx_dvol_history_asset_ts
ON dvol_history(asset, timestamp DESC);
PRAGMA user_version = 6;
+2 -1
View File
@@ -116,8 +116,9 @@ class DvolSnapshot(BaseModel):
model_config = ConfigDict(extra="forbid")
timestamp: datetime
asset: str # "ETH", "BTC"
dvol: Decimal
eth_spot: Decimal
spot: Decimal
class MarketSnapshotRecord(BaseModel):
+4 -3
View File
@@ -339,12 +339,13 @@ class Repository:
self, conn: sqlite3.Connection, snapshot: DvolSnapshot
) -> None:
conn.execute(
"INSERT OR REPLACE INTO dvol_history(timestamp, dvol, eth_spot) "
"VALUES (?,?,?)",
"INSERT OR REPLACE INTO dvol_history(timestamp, asset, dvol, spot) "
"VALUES (?,?,?,?)",
(
_enc_dt(snapshot.timestamp),
snapshot.asset,
_enc_dec(snapshot.dvol),
_enc_dec(snapshot.eth_spot),
_enc_dec(snapshot.spot),
),
)