feat(state+runtime+gui): market_snapshots — calibrazione soglie da dati
Sistema dedicato di raccolta dati per scegliere le soglie dei filtri sui percentili reali invece di valori a istinto. Nuovi componenti: * state/migrations/0003_market_snapshots.sql — tabella + index, PK composta (timestamp, asset). Ogni colonna numerica è NULL-able per preservare la continuità della serie quando un singolo MCP fallisce. * state/models.py — MarketSnapshotRecord Pydantic. * state/repository.py — record_market_snapshot, list_market_snapshots, _row_to_market_snapshot. * runtime/market_snapshot_cycle.py — collettore best-effort che chiama spot/dvol/realized_vol/dealer_gamma/funding_perp/funding_cross/ liquidation_heatmap/macro per ogni asset; raccoglie gli errori in fetch_errors_json e segna fetch_ok=false ma persiste comunque la riga. * clients/deribit.py — generalizzati dealer_gamma_profile(currency), realized_vol(currency), spot_perp_price(asset). dealer_gamma_profile_eth resta come alias per la chiamata dell'entry cycle. * runtime/orchestrator.py — nuovo job APScheduler `market_snapshot` cron */15 con assets configurabili (default ETH+BTC); il consumer manual_actions ora dispatcha anche kind=run_cycle cycle=market_snapshot per la GUI. * gui/data_layer.py — load_market_snapshots, enqueue_run_cycle accetta market_snapshot; tipo MarketSnapshotRecord esposto. * gui/pages/6_📐_Calibrazione.py — selezione asset+finestra, conteggio fetch_ok, per ogni metrica: istogramma, soglia da strategy.yaml come vline rossa, percentili P5/P10/P25/P50/P75/P90/P95, % di tick che la soglia avrebbe filtrato. * gui/pages/1_📊_Status.py — bottone "📐 Forza snapshot" (4° del pannello Forza ciclo) per popolare la tabella senza aspettare il cron. 5 nuovi test sul collector (happy, fault tolerance, asset switch, macro fail, empty assets); test_orchestrator job set aggiornato. 368/368 tests pass; ruff clean; mypy strict src clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
-- 0003_market_snapshots.sql — periodic market snapshot table.
|
||||
--
|
||||
-- Populated by the `market_snapshot` scheduler job (cron */15) for
|
||||
-- every asset traded by the engine (ETH primary, BTC as benchmark).
|
||||
-- The table backs the "Calibrazione" GUI page: histograms, percentiles
|
||||
-- and "% of ticks the current threshold would have blocked" let the
|
||||
-- operator pick filter thresholds from observed distributions instead
|
||||
-- of guessing.
|
||||
--
|
||||
-- Every column except (timestamp, asset, fetch_ok) is NULL-able: a
|
||||
-- single MCP call may fail and we still want to keep the row so the
|
||||
-- time series stays continuous. fetch_errors_json carries the per-feed
|
||||
-- error messages for offline debugging.
|
||||
|
||||
CREATE TABLE market_snapshots (
|
||||
timestamp TEXT NOT NULL,
|
||||
asset TEXT NOT NULL,
|
||||
spot NUMERIC,
|
||||
dvol NUMERIC,
|
||||
realized_vol_30d NUMERIC,
|
||||
iv_minus_rv NUMERIC,
|
||||
funding_perp_annualized NUMERIC,
|
||||
funding_cross_annualized NUMERIC,
|
||||
dealer_net_gamma NUMERIC,
|
||||
gamma_flip_level NUMERIC,
|
||||
oi_delta_pct_4h NUMERIC,
|
||||
liquidation_long_risk TEXT,
|
||||
liquidation_short_risk TEXT,
|
||||
macro_days_to_event INTEGER,
|
||||
fetch_ok INTEGER NOT NULL,
|
||||
fetch_errors_json TEXT,
|
||||
PRIMARY KEY (timestamp, asset)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_market_snapshots_asset_ts
|
||||
ON market_snapshots(asset, timestamp DESC);
|
||||
|
||||
PRAGMA user_version = 3;
|
||||
@@ -21,6 +21,7 @@ __all__ = [
|
||||
"DvolSnapshot",
|
||||
"InstructionRecord",
|
||||
"ManualAction",
|
||||
"MarketSnapshotRecord",
|
||||
"PositionRecord",
|
||||
"PositionStatus",
|
||||
"SystemStateRecord",
|
||||
@@ -118,6 +119,35 @@ class DvolSnapshot(BaseModel):
|
||||
eth_spot: Decimal
|
||||
|
||||
|
||||
class MarketSnapshotRecord(BaseModel):
|
||||
"""Row of the ``market_snapshots`` table.
|
||||
|
||||
Single point in time, single asset. Every numeric field is
|
||||
optional because the ``market_snapshot`` collector is best-effort:
|
||||
a single MCP failure NULLs the affected metric without dropping
|
||||
the row.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
timestamp: datetime
|
||||
asset: str # "ETH", "BTC"
|
||||
spot: Decimal | None = None
|
||||
dvol: Decimal | None = None
|
||||
realized_vol_30d: Decimal | None = None
|
||||
iv_minus_rv: Decimal | None = None
|
||||
funding_perp_annualized: Decimal | None = None
|
||||
funding_cross_annualized: Decimal | None = None
|
||||
dealer_net_gamma: Decimal | None = None
|
||||
gamma_flip_level: Decimal | None = None
|
||||
oi_delta_pct_4h: Decimal | None = None
|
||||
liquidation_long_risk: str | None = None
|
||||
liquidation_short_risk: str | None = None
|
||||
macro_days_to_event: int | None = None
|
||||
fetch_ok: bool
|
||||
fetch_errors_json: str | None = None
|
||||
|
||||
|
||||
class ManualAction(BaseModel):
|
||||
"""Row of the ``manual_actions`` table."""
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from cerbero_bite.state.models import (
|
||||
DvolSnapshot,
|
||||
InstructionRecord,
|
||||
ManualAction,
|
||||
MarketSnapshotRecord,
|
||||
PositionRecord,
|
||||
PositionStatus,
|
||||
SystemStateRecord,
|
||||
@@ -346,6 +347,66 @@ class Repository:
|
||||
),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# market_snapshots
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def record_market_snapshot(
|
||||
self, conn: sqlite3.Connection, snapshot: MarketSnapshotRecord
|
||||
) -> None:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO market_snapshots("
|
||||
"timestamp, asset, spot, dvol, realized_vol_30d, iv_minus_rv, "
|
||||
"funding_perp_annualized, funding_cross_annualized, "
|
||||
"dealer_net_gamma, gamma_flip_level, oi_delta_pct_4h, "
|
||||
"liquidation_long_risk, liquidation_short_risk, "
|
||||
"macro_days_to_event, fetch_ok, fetch_errors_json) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
_enc_dt(snapshot.timestamp),
|
||||
snapshot.asset,
|
||||
_enc_dec(snapshot.spot),
|
||||
_enc_dec(snapshot.dvol),
|
||||
_enc_dec(snapshot.realized_vol_30d),
|
||||
_enc_dec(snapshot.iv_minus_rv),
|
||||
_enc_dec(snapshot.funding_perp_annualized),
|
||||
_enc_dec(snapshot.funding_cross_annualized),
|
||||
_enc_dec(snapshot.dealer_net_gamma),
|
||||
_enc_dec(snapshot.gamma_flip_level),
|
||||
_enc_dec(snapshot.oi_delta_pct_4h),
|
||||
snapshot.liquidation_long_risk,
|
||||
snapshot.liquidation_short_risk,
|
||||
snapshot.macro_days_to_event,
|
||||
1 if snapshot.fetch_ok else 0,
|
||||
snapshot.fetch_errors_json,
|
||||
),
|
||||
)
|
||||
|
||||
def list_market_snapshots(
|
||||
self,
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
asset: str,
|
||||
start: datetime | None = None,
|
||||
end: datetime | None = None,
|
||||
limit: int = 5000,
|
||||
) -> list[MarketSnapshotRecord]:
|
||||
clauses: list[str] = ["asset = ?"]
|
||||
params: list[Any] = [asset]
|
||||
if start is not None:
|
||||
clauses.append("timestamp >= ?")
|
||||
params.append(_enc_dt(start))
|
||||
if end is not None:
|
||||
clauses.append("timestamp <= ?")
|
||||
params.append(_enc_dt(end))
|
||||
params.append(int(limit))
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM market_snapshots WHERE {' AND '.join(clauses)} "
|
||||
f"ORDER BY timestamp DESC LIMIT ?",
|
||||
params,
|
||||
).fetchall()
|
||||
return [_row_to_market_snapshot(r) for r in rows]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# manual_actions
|
||||
# ------------------------------------------------------------------
|
||||
@@ -559,6 +620,31 @@ def _row_to_manual(row: sqlite3.Row) -> ManualAction:
|
||||
)
|
||||
|
||||
|
||||
def _row_to_market_snapshot(row: sqlite3.Row) -> MarketSnapshotRecord:
|
||||
return MarketSnapshotRecord(
|
||||
timestamp=_dec_dt_required(row["timestamp"]),
|
||||
asset=row["asset"],
|
||||
spot=_dec_dec(row["spot"]),
|
||||
dvol=_dec_dec(row["dvol"]),
|
||||
realized_vol_30d=_dec_dec(row["realized_vol_30d"]),
|
||||
iv_minus_rv=_dec_dec(row["iv_minus_rv"]),
|
||||
funding_perp_annualized=_dec_dec(row["funding_perp_annualized"]),
|
||||
funding_cross_annualized=_dec_dec(row["funding_cross_annualized"]),
|
||||
dealer_net_gamma=_dec_dec(row["dealer_net_gamma"]),
|
||||
gamma_flip_level=_dec_dec(row["gamma_flip_level"]),
|
||||
oi_delta_pct_4h=_dec_dec(row["oi_delta_pct_4h"]),
|
||||
liquidation_long_risk=row["liquidation_long_risk"],
|
||||
liquidation_short_risk=row["liquidation_short_risk"],
|
||||
macro_days_to_event=(
|
||||
int(row["macro_days_to_event"])
|
||||
if row["macro_days_to_event"] is not None
|
||||
else None
|
||||
),
|
||||
fetch_ok=bool(int(row["fetch_ok"])),
|
||||
fetch_errors_json=row["fetch_errors_json"],
|
||||
)
|
||||
|
||||
|
||||
def _dec_dec_required(value: Any) -> Decimal:
|
||||
out = _dec_dec(value)
|
||||
if out is None:
|
||||
|
||||
Reference in New Issue
Block a user