c0a0ee416f
Aggiunge la persistence della option chain Deribit con cron settimanale ``55 13 * * MON`` (5 minuti prima del trigger entry alle 14:00 UTC), sbloccando il backtest non-stilizzato e la calibrazione empirica dello skew premium. **Schema (migrazione 0004)** Nuova tabella ``option_chain_snapshots`` con primary key composta ``(timestamp, instrument_name)`` — tutti i quote prelevati nello stesso tick condividono il timestamp, così le query "lo snapshot del 2026-05-04 alle 13:55" diventano una singola WHERE timestamp = X. Indici su (asset, timestamp DESC) e (asset, expiry) per supportare sia listing recenti sia query per scadenza specifica. Campi: instrument_name, strike, expiry, option_type (C/P), bid, ask, mid, iv, delta, gamma, theta, vega, open_interest, volume_24h, book_depth_top3. Tutti i numerici sono nullable: il collector è best-effort, un ticker mancante produce comunque una riga (utile per sapere che lo strumento esisteva ma non era quotato). **Modello + repository** - ``OptionChainQuoteRecord`` (Pydantic, in ``state/models.py``). - ``Repository.record_option_chain_snapshot`` (bulk insert idempotente). - ``Repository.list_option_chain_snapshots`` (filtri su asset, timestamp window, expiry window, limit default 50000). - ``Repository.latest_option_chain_timestamp`` (freshness check per dashboard GUI). **Collector** Nuovo ``runtime/option_chain_snapshot_cycle.py`` che: 1. Calcola la finestra scadenze ``[now+dte_min, now+dte_max]`` da ``cfg.structure``: niente richieste su scadenze che il rule engine non userebbe mai. 2. Chiama ``deribit.options_chain()`` con ``min_open_interest=cfg.liquidity.open_interest_min``. 3. Batch ``deribit.get_tickers()`` (max 20 per call, limite Deribit) con error-isolation per batch — un batch fallito non blocca gli altri. 4. NON chiama l'order book per ogni strike (rate-limit guard); ``book_depth_top3`` resta NULL e il liquidity gate live lo chiede on-the-fly per gli strike candidati al picker. Best-effort end-to-end: chain assente, get_tickers giù, persist fallito → ritorna 0 senza alzare eccezioni, logga sempre. **Schedulazione** Wired in ``Orchestrator.install_scheduler`` come job parallelo a ``market_snapshot``, attivo solo quando ``ENABLE_DATA_ANALYSIS=true``. Cron parametrizzabile via il nuovo kwarg ``option_chain_cron`` (default ``55 13 * * MON``). **Test** - 4 unit test del collector (happy path, ticker mancante, chain vuota, fetch fail best-effort) con mock di RuntimeContext. - Aggiornato ``test_install_scheduler_registers_canonical_jobs`` per includere il nuovo job nel set canonico. **Cosa sblocca** - Backtest non-stilizzato: il PR ``feat/backtest-engine`` può dropparsi il modello BS+skew_premium e leggere prezzi reali ``mid`` dalla chain registrata. - Calibrazione empirica dello skew premium (hardcoded a 1.5 nel backtest stilizzato): plot del rapporto fra quote reali Deribit e BS per delta/expiry, regressione → valore data-driven. - Validazione ex-post: "il delta-0.12 era davvero a 25% OTM in quella settimana?" diventa una query SELECT. - Dimensione attesa: ~50 strike × 3 scadenze × 1 snapshot/settimana × 17 colonne ≈ 12 KB/settimana, ~600 KB/anno. Trascurabile. Suite: 409 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
218 lines
5.8 KiB
Python
218 lines
5.8 KiB
Python
"""Pydantic record types mirroring the SQLite tables.
|
|
|
|
Every numeric column documented as ``NUMERIC`` in
|
|
``state/migrations/0001_init.sql`` is exposed as :class:`decimal.Decimal`
|
|
on the Python side. The repository layer is responsible for serialising
|
|
to ``TEXT`` (using ``str``) when writing and parsing back when reading,
|
|
so precision is never lost via ``float`` coercion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
__all__ = [
|
|
"DecisionRecord",
|
|
"DvolSnapshot",
|
|
"InstructionRecord",
|
|
"ManualAction",
|
|
"MarketSnapshotRecord",
|
|
"OptionChainQuoteRecord",
|
|
"PositionRecord",
|
|
"PositionStatus",
|
|
"SystemStateRecord",
|
|
]
|
|
|
|
|
|
PositionStatus = Literal[
|
|
"proposed",
|
|
"awaiting_fill",
|
|
"open",
|
|
"closing",
|
|
"closed",
|
|
"cancelled",
|
|
]
|
|
|
|
|
|
class PositionRecord(BaseModel):
|
|
"""Row of the ``positions`` table."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
proposal_id: UUID
|
|
spread_type: str
|
|
asset: str = "ETH"
|
|
expiry: datetime
|
|
short_strike: Decimal
|
|
long_strike: Decimal
|
|
short_instrument: str
|
|
long_instrument: str
|
|
n_contracts: int
|
|
spread_width_usd: Decimal
|
|
spread_width_pct: Decimal
|
|
credit_eth: Decimal
|
|
credit_usd: Decimal
|
|
max_loss_usd: Decimal
|
|
spot_at_entry: Decimal
|
|
dvol_at_entry: Decimal
|
|
delta_at_entry: Decimal
|
|
eth_price_at_entry: Decimal
|
|
proposed_at: datetime
|
|
opened_at: datetime | None = None
|
|
closed_at: datetime | None = None
|
|
close_reason: str | None = None
|
|
debit_paid_eth: Decimal | None = None
|
|
pnl_eth: Decimal | None = None
|
|
pnl_usd: Decimal | None = None
|
|
status: PositionStatus
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class InstructionRecord(BaseModel):
|
|
"""Row of the ``instructions`` table."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
instruction_id: UUID
|
|
proposal_id: UUID
|
|
kind: Literal["open_combo", "close_combo"]
|
|
payload_json: str
|
|
sent_at: datetime
|
|
acknowledged_at: datetime | None = None
|
|
filled_at: datetime | None = None
|
|
cancelled_at: datetime | None = None
|
|
actual_fill_eth: Decimal | None = None
|
|
actual_fees_eth: Decimal | None = None
|
|
|
|
|
|
class DecisionRecord(BaseModel):
|
|
"""Row of the ``decisions`` table.
|
|
|
|
``id`` is :class:`int` and may be ``None`` before the row has been
|
|
inserted; the repository sets it after the auto-increment fires.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
id: int | None = None
|
|
decision_type: Literal["entry_check", "exit_check", "kelly_recalib"]
|
|
proposal_id: UUID | None = None
|
|
timestamp: datetime
|
|
inputs_json: str
|
|
outputs_json: str
|
|
action_taken: str | None = None
|
|
notes: str | None = None
|
|
|
|
|
|
class DvolSnapshot(BaseModel):
|
|
"""Row of the ``dvol_history`` table."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
timestamp: datetime
|
|
dvol: Decimal
|
|
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 OptionChainQuoteRecord(BaseModel):
|
|
"""Row of the ``option_chain_snapshots`` table.
|
|
|
|
One row per (snapshot_ts, instrument) — the same ``timestamp`` is
|
|
shared by every quote prelevato nello stesso tick del cron. Tutti
|
|
i campi numerici sono opzionali perché il collector è
|
|
best-effort: un ticker mancante non invalida il resto della chain.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
timestamp: datetime
|
|
asset: str
|
|
instrument_name: str
|
|
strike: Decimal
|
|
expiry: datetime
|
|
option_type: Literal["C", "P"]
|
|
bid: Decimal | None = None
|
|
ask: Decimal | None = None
|
|
mid: Decimal | None = None
|
|
iv: Decimal | None = None
|
|
delta: Decimal | None = None
|
|
gamma: Decimal | None = None
|
|
theta: Decimal | None = None
|
|
vega: Decimal | None = None
|
|
open_interest: int | None = None
|
|
volume_24h: int | None = None
|
|
book_depth_top3: int | None = None
|
|
|
|
|
|
class ManualAction(BaseModel):
|
|
"""Row of the ``manual_actions`` table."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
id: int | None = None
|
|
kind: Literal[
|
|
"approve_proposal",
|
|
"reject_proposal",
|
|
"force_close",
|
|
"arm_kill",
|
|
"disarm_kill",
|
|
"run_cycle",
|
|
]
|
|
proposal_id: UUID | None = None
|
|
payload_json: str | None = None
|
|
created_at: datetime
|
|
consumed_at: datetime | None = None
|
|
consumed_by: str | None = None
|
|
result: str | None = None
|
|
|
|
|
|
class SystemStateRecord(BaseModel):
|
|
"""Singleton row of the ``system_state`` table."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
id: int = Field(default=1)
|
|
kill_switch: int = 0
|
|
kill_reason: str | None = None
|
|
kill_at: datetime | None = None
|
|
last_health_check: datetime
|
|
last_kelly_calib: datetime | None = None
|
|
config_version: str
|
|
started_at: datetime
|
|
last_audit_hash: str | None = None
|