Files
Cerbero-Bite/src/cerbero_bite/state/models.py
T
Adriano Dal Pastro 1eeb53650e feat(research): collettore full-chain opzioni con book_depth e colonna source
Aggiunge un secondo collettore opzioni indipendente dal live, pensato per
trasformare il dataset da "skew/premi medi" a backtest opzioni vero
(per-trade e standing put).

- option_chain_research_cycle.py: cattura tutte le scadenze <= expiry_max_days
  (1g..3mesi) ed entrambe le ali (OI>=100, filtro moneyness opzionale),
  popolando book_depth_top3 via orderbook_depth_top3 (concorrenza limitata)
  cosi' lo slippage reale e' modellabile. Best-effort come il collettore live.
- migrazione 0007: colonna `source` su option_chain_snapshots ('live' default
  per le righe storiche, 'research' per il nuovo collettore) + indice
  (asset, source, timestamp). user_version 6 -> 7.
- ResearchCollectorConfig (schema): blocco `research_collector`, enabled=false
  di default (costo API non trascurabile); cron orario, expiry_max_days=95.
- orchestrator: job `option_chain_research` schedulato solo se data-analysis
  attiva E research_collector.enabled.
- repository: insert+mapper estesi con `source`; list_option_chain_snapshots
  accetta un filtro `source` per le query di backtest.

Verificato in isolamento (immagine + interprete reali): import, migrazione,
round-trip repo. Suite: 527 passed, zero regressioni vs baseline pristina.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:44:47 +00:00

224 lines
6.0 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
asset: str # "ETH", "BTC"
dvol: Decimal
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
# 'live' = collettore operativo (finestra DTE strategia, no depth);
# 'research' = collettore full-chain con book_depth popolato.
source: str = "live"
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
auto_pause_until: datetime | None = None
auto_pause_reason: str | None = None