"""Typed CRUD layer over the SQLite database. All methods take a :class:`sqlite3.Connection` so callers can compose a single transaction across multiple writes (the orchestrator does this when persisting an entry decision + the resulting proposal). The repository never opens its own connection: that responsibility is left to :func:`cerbero_bite.state.db.connect`. Decimals are stored as TEXT to preserve precision (see ``state/models.py``). """ from __future__ import annotations import sqlite3 from datetime import UTC, datetime, timedelta from decimal import Decimal from typing import Any from uuid import UUID from cerbero_bite.state.models import ( DecisionRecord, DvolSnapshot, InstructionRecord, ManualAction, MarketSnapshotRecord, OptionChainQuoteRecord, PositionRecord, PositionStatus, SystemStateRecord, ) __all__ = ["Repository"] # --------------------------------------------------------------------------- # Encoding helpers # --------------------------------------------------------------------------- def _enc_dec(value: Decimal | None) -> str | None: return None if value is None else str(value) def _enc_dt(value: datetime | None) -> str | None: if value is None: return None if value.tzinfo is None: value = value.replace(tzinfo=UTC) return value.astimezone(UTC).isoformat() def _enc_uuid(value: UUID | None) -> str | None: return None if value is None else str(value) def _dec_dec(value: Any) -> Decimal | None: if value is None: return None return Decimal(str(value)) def _dec_dt(value: Any) -> datetime | None: if value is None: return None return datetime.fromisoformat(str(value)) def _dec_uuid(value: Any) -> UUID | None: if value is None: return None return UUID(str(value)) # --------------------------------------------------------------------------- # Repository # --------------------------------------------------------------------------- class Repository: """Typed CRUD wrapper. One instance per process, all calls take a conn.""" # ------------------------------------------------------------------ # positions # ------------------------------------------------------------------ def create_position(self, conn: sqlite3.Connection, record: PositionRecord) -> None: conn.execute( """ INSERT INTO positions ( proposal_id, spread_type, asset, expiry, short_strike, long_strike, short_instrument, long_instrument, n_contracts, spread_width_usd, spread_width_pct, credit_eth, credit_usd, max_loss_usd, spot_at_entry, dvol_at_entry, delta_at_entry, eth_price_at_entry, proposed_at, opened_at, closed_at, close_reason, debit_paid_eth, pnl_eth, pnl_usd, status, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( _enc_uuid(record.proposal_id), record.spread_type, record.asset, _enc_dt(record.expiry), _enc_dec(record.short_strike), _enc_dec(record.long_strike), record.short_instrument, record.long_instrument, record.n_contracts, _enc_dec(record.spread_width_usd), _enc_dec(record.spread_width_pct), _enc_dec(record.credit_eth), _enc_dec(record.credit_usd), _enc_dec(record.max_loss_usd), _enc_dec(record.spot_at_entry), _enc_dec(record.dvol_at_entry), _enc_dec(record.delta_at_entry), _enc_dec(record.eth_price_at_entry), _enc_dt(record.proposed_at), _enc_dt(record.opened_at), _enc_dt(record.closed_at), record.close_reason, _enc_dec(record.debit_paid_eth), _enc_dec(record.pnl_eth), _enc_dec(record.pnl_usd), record.status, _enc_dt(record.created_at), _enc_dt(record.updated_at), ), ) def get_position( self, conn: sqlite3.Connection, proposal_id: UUID ) -> PositionRecord | None: row = conn.execute( "SELECT * FROM positions WHERE proposal_id = ?", (_enc_uuid(proposal_id),), ).fetchone() return None if row is None else _row_to_position(row) def list_positions( self, conn: sqlite3.Connection, *, status: PositionStatus | None = None, ) -> list[PositionRecord]: if status is None: cursor = conn.execute( "SELECT * FROM positions ORDER BY created_at DESC" ) else: cursor = conn.execute( "SELECT * FROM positions WHERE status = ? ORDER BY created_at DESC", (status,), ) return [_row_to_position(r) for r in cursor.fetchall()] def list_open_positions(self, conn: sqlite3.Connection) -> list[PositionRecord]: rows = conn.execute( "SELECT * FROM positions WHERE status IN ('open', 'awaiting_fill', " "'closing') ORDER BY created_at DESC" ).fetchall() return [_row_to_position(r) for r in rows] def update_position_status( self, conn: sqlite3.Connection, proposal_id: UUID, *, status: PositionStatus, 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, now: datetime, ) -> None: sets: list[str] = ["status = ?", "updated_at = ?"] params: list[Any] = [status, _enc_dt(now)] if opened_at is not None: sets.append("opened_at = ?") params.append(_enc_dt(opened_at)) if closed_at is not None: sets.append("closed_at = ?") params.append(_enc_dt(closed_at)) if close_reason is not None: sets.append("close_reason = ?") params.append(close_reason) if debit_paid_eth is not None: sets.append("debit_paid_eth = ?") params.append(_enc_dec(debit_paid_eth)) if pnl_eth is not None: sets.append("pnl_eth = ?") params.append(_enc_dec(pnl_eth)) if pnl_usd is not None: sets.append("pnl_usd = ?") params.append(_enc_dec(pnl_usd)) params.append(_enc_uuid(proposal_id)) conn.execute( f"UPDATE positions SET {', '.join(sets)} WHERE proposal_id = ?", params, ) def count_concurrent_positions(self, conn: sqlite3.Connection) -> int: row = conn.execute( "SELECT COUNT(*) FROM positions WHERE status IN " "('awaiting_fill', 'open', 'closing')" ).fetchone() return int(row[0]) # ------------------------------------------------------------------ # instructions # ------------------------------------------------------------------ def create_instruction( self, conn: sqlite3.Connection, record: InstructionRecord ) -> None: conn.execute( """ INSERT INTO instructions ( instruction_id, proposal_id, kind, payload_json, sent_at, acknowledged_at, filled_at, cancelled_at, actual_fill_eth, actual_fees_eth ) VALUES (?,?,?,?,?,?,?,?,?,?) """, ( _enc_uuid(record.instruction_id), _enc_uuid(record.proposal_id), record.kind, record.payload_json, _enc_dt(record.sent_at), _enc_dt(record.acknowledged_at), _enc_dt(record.filled_at), _enc_dt(record.cancelled_at), _enc_dec(record.actual_fill_eth), _enc_dec(record.actual_fees_eth), ), ) def update_instruction( self, conn: sqlite3.Connection, instruction_id: UUID, *, 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, ) -> None: sets: list[str] = [] params: list[Any] = [] if acknowledged_at is not None: sets.append("acknowledged_at = ?") params.append(_enc_dt(acknowledged_at)) if filled_at is not None: sets.append("filled_at = ?") params.append(_enc_dt(filled_at)) if cancelled_at is not None: sets.append("cancelled_at = ?") params.append(_enc_dt(cancelled_at)) if actual_fill_eth is not None: sets.append("actual_fill_eth = ?") params.append(_enc_dec(actual_fill_eth)) if actual_fees_eth is not None: sets.append("actual_fees_eth = ?") params.append(_enc_dec(actual_fees_eth)) if not sets: return params.append(_enc_uuid(instruction_id)) conn.execute( f"UPDATE instructions SET {', '.join(sets)} " f"WHERE instruction_id = ?", params, ) def list_instructions( self, conn: sqlite3.Connection, proposal_id: UUID ) -> list[InstructionRecord]: rows = conn.execute( "SELECT * FROM instructions WHERE proposal_id = ? ORDER BY sent_at", (_enc_uuid(proposal_id),), ).fetchall() return [_row_to_instruction(r) for r in rows] # ------------------------------------------------------------------ # decisions # ------------------------------------------------------------------ def record_decision( self, conn: sqlite3.Connection, record: DecisionRecord ) -> int: cursor = conn.execute( """ INSERT INTO decisions ( decision_type, proposal_id, timestamp, inputs_json, outputs_json, action_taken, notes ) VALUES (?,?,?,?,?,?,?) """, ( record.decision_type, _enc_uuid(record.proposal_id), _enc_dt(record.timestamp), record.inputs_json, record.outputs_json, record.action_taken, record.notes, ), ) return int(cursor.lastrowid or 0) def list_decisions( self, conn: sqlite3.Connection, *, proposal_id: UUID | None = None, limit: int = 100, ) -> list[DecisionRecord]: if proposal_id is None: rows = conn.execute( "SELECT * FROM decisions ORDER BY timestamp DESC LIMIT ?", (limit,), ).fetchall() else: rows = conn.execute( "SELECT * FROM decisions WHERE proposal_id = ? " "ORDER BY timestamp DESC LIMIT ?", (_enc_uuid(proposal_id), limit), ).fetchall() return [_row_to_decision(r) for r in rows] # ------------------------------------------------------------------ # dvol_history # ------------------------------------------------------------------ def record_dvol_snapshot( self, conn: sqlite3.Connection, snapshot: DvolSnapshot ) -> None: conn.execute( "INSERT OR REPLACE INTO dvol_history(timestamp, asset, dvol, spot) " "VALUES (?,?,?,?)", ( _enc_dt(snapshot.timestamp), snapshot.asset, _enc_dec(snapshot.dvol), _enc_dec(snapshot.spot), ), ) # ------------------------------------------------------------------ # 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] def count_iv_rv_distinct_days( self, conn: sqlite3.Connection, *, asset: str, max_days: int, as_of: datetime | None = None, ) -> int: """Numero di giorni di calendario distinti coperti da IV-RV validi. Esclude righe con ``fetch_ok=0`` o ``iv_minus_rv IS NULL``. Usato dal caller del gate adattivo per decidere la finestra (warmup hard / min_days / target_days). Args: as_of: Reference time for the rolling window. Defaults to ``datetime.now(UTC)``. """ if max_days <= 0: raise ValueError(f"max_days must be positive, got {max_days}") ref = as_of if as_of is not None else datetime.now(UTC) if ref.tzinfo is None: raise ValueError("as_of must be timezone-aware") cutoff = ref - timedelta(days=max_days) row = conn.execute( "SELECT COUNT(DISTINCT substr(timestamp, 1, 10)) AS n " "FROM market_snapshots " "WHERE asset = ? " " AND fetch_ok = 1 " " AND iv_minus_rv IS NOT NULL " " AND timestamp >= ?", (asset, _enc_dt(cutoff)), ).fetchone() return int(row["n"]) if row is not None else 0 def iv_rv_values_for_window( self, conn: sqlite3.Connection, *, asset: str, window_days: int, as_of: datetime | None = None, ) -> list[Decimal]: """Valori IV-RV ordinati ASC su ``[as_of - window_days, as_of]``. Esclude righe con ``fetch_ok=0`` o ``iv_minus_rv IS NULL``. Tutti i record validi della finestra concorrono come singoli contributi alla statistica del percentile, indipendentemente dalla cadenza con cui sono stati raccolti (tick live vs backfill daily). """ if window_days <= 0: raise ValueError(f"window_days must be positive, got {window_days}") ref = as_of if as_of is not None else datetime.now(UTC) if ref.tzinfo is None: raise ValueError("as_of must be timezone-aware") cutoff = ref - timedelta(days=window_days) rows = conn.execute( "SELECT iv_minus_rv FROM market_snapshots " "WHERE asset = ? " " AND fetch_ok = 1 " " AND iv_minus_rv IS NOT NULL " " AND timestamp >= ? " "ORDER BY timestamp ASC", (asset, _enc_dt(cutoff)), ).fetchall() return [Decimal(str(r["iv_minus_rv"])) for r in rows] def dvol_lookback( self, conn: sqlite3.Connection, *, asset: str, reference: datetime, tolerance_minutes: int = 15, ) -> Decimal | None: """DVOL al tick più vicino a `reference`, entro ±tolerance_minutes. Ritorna ``None`` se non esiste un tick valido (``fetch_ok=1``, ``dvol IS NOT NULL``) entro la tolerance. Usato dal Vol-of-Vol guard per stimare DVOL N ore fa. """ if reference.tzinfo is None: raise ValueError("reference must be timezone-aware") ref_lo = (reference - timedelta(minutes=tolerance_minutes)).astimezone(UTC) ref_hi = (reference + timedelta(minutes=tolerance_minutes)).astimezone(UTC) row = conn.execute( "SELECT dvol, timestamp FROM market_snapshots " "WHERE asset = ? " " AND fetch_ok = 1 " " AND dvol IS NOT NULL " " AND timestamp >= ? " " AND timestamp <= ? " "ORDER BY ABS(julianday(timestamp) - julianday(?)) ASC LIMIT 1", ( asset, _enc_dt(ref_lo), _enc_dt(ref_hi), _enc_dt(reference), ), ).fetchone() if row is None: return None return Decimal(str(row["dvol"])) # ------------------------------------------------------------------ # option_chain_snapshots # ------------------------------------------------------------------ def record_option_chain_snapshot( self, conn: sqlite3.Connection, quotes: list[OptionChainQuoteRecord], ) -> int: """Bulk-insert dei quote di un singolo tick. Tutti i quote condividono lo stesso ``timestamp``. Idempotente per (timestamp, instrument_name).""" if not quotes: return 0 rows = [ ( _enc_dt(q.timestamp), q.asset, q.instrument_name, _enc_dec(q.strike), _enc_dt(q.expiry), q.option_type, _enc_dec(q.bid), _enc_dec(q.ask), _enc_dec(q.mid), _enc_dec(q.iv), _enc_dec(q.delta), _enc_dec(q.gamma), _enc_dec(q.theta), _enc_dec(q.vega), q.open_interest, q.volume_24h, q.book_depth_top3, ) for q in quotes ] conn.executemany( "INSERT OR REPLACE INTO option_chain_snapshots(" "timestamp, asset, instrument_name, strike, expiry, option_type, " "bid, ask, mid, iv, delta, gamma, theta, vega, " "open_interest, volume_24h, book_depth_top3) " "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", rows, ) return len(rows) def list_option_chain_snapshots( self, conn: sqlite3.Connection, *, asset: str, start: datetime | None = None, end: datetime | None = None, expiry_from: datetime | None = None, expiry_to: datetime | None = None, limit: int = 50000, ) -> list[OptionChainQuoteRecord]: 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)) if expiry_from is not None: clauses.append("expiry >= ?") params.append(_enc_dt(expiry_from)) if expiry_to is not None: clauses.append("expiry <= ?") params.append(_enc_dt(expiry_to)) params.append(int(limit)) rows = conn.execute( f"SELECT * FROM option_chain_snapshots " f"WHERE {' AND '.join(clauses)} " f"ORDER BY timestamp DESC, instrument_name ASC LIMIT ?", params, ).fetchall() return [_row_to_option_chain_quote(r) for r in rows] def latest_option_chain_timestamp( self, conn: sqlite3.Connection, *, asset: str, ) -> datetime | None: """Timestamp dell'ultimo snapshot raccolto per ``asset``, utile per stimare la freschezza del dato dalla GUI.""" row = conn.execute( "SELECT timestamp FROM option_chain_snapshots " "WHERE asset = ? ORDER BY timestamp DESC LIMIT 1", (asset,), ).fetchone() if row is None: return None return _dec_dt(row["timestamp"]) # ------------------------------------------------------------------ # manual_actions # ------------------------------------------------------------------ def enqueue_manual_action( self, conn: sqlite3.Connection, action: ManualAction ) -> int: cursor = conn.execute( "INSERT INTO manual_actions(kind, proposal_id, payload_json, created_at) " "VALUES (?,?,?,?)", ( action.kind, _enc_uuid(action.proposal_id), action.payload_json, _enc_dt(action.created_at), ), ) return int(cursor.lastrowid or 0) def next_unconsumed_action( self, conn: sqlite3.Connection ) -> ManualAction | None: row = conn.execute( "SELECT * FROM manual_actions WHERE consumed_at IS NULL " "ORDER BY created_at ASC LIMIT 1" ).fetchone() return None if row is None else _row_to_manual(row) def mark_action_consumed( self, conn: sqlite3.Connection, action_id: int, *, consumed_by: str, result: str, now: datetime, ) -> None: conn.execute( "UPDATE manual_actions SET consumed_at = ?, consumed_by = ?, " "result = ? WHERE id = ?", (_enc_dt(now), consumed_by, result, action_id), ) # ------------------------------------------------------------------ # system_state # ------------------------------------------------------------------ def init_system_state( self, conn: sqlite3.Connection, *, config_version: str, now: datetime ) -> None: """Insert the singleton row if it does not already exist.""" existing = conn.execute( "SELECT 1 FROM system_state WHERE id = 1" ).fetchone() if existing is not None: return conn.execute( "INSERT INTO system_state(id, kill_switch, last_health_check, " "config_version, started_at) VALUES (1, 0, ?, ?, ?)", (_enc_dt(now), config_version, _enc_dt(now)), ) def get_system_state( self, conn: sqlite3.Connection ) -> SystemStateRecord | None: row = conn.execute("SELECT * FROM system_state WHERE id = 1").fetchone() if row is None: return None keys = row.keys() return SystemStateRecord( id=int(row["id"]), kill_switch=int(row["kill_switch"]), kill_reason=row["kill_reason"], kill_at=_dec_dt(row["kill_at"]), last_health_check=_dec_dt_required(row["last_health_check"]), last_kelly_calib=_dec_dt(row["last_kelly_calib"]), config_version=row["config_version"], started_at=_dec_dt_required(row["started_at"]), last_audit_hash=( row["last_audit_hash"] if "last_audit_hash" in keys else None ), auto_pause_until=( _dec_dt(row["auto_pause_until"]) if "auto_pause_until" in keys else None ), auto_pause_reason=( row["auto_pause_reason"] if "auto_pause_reason" in keys else None ), ) def set_last_audit_hash( self, conn: sqlite3.Connection, *, hex_hash: str ) -> None: """Store the most recent audit chain hash. Called by AuditLog after append.""" conn.execute( "UPDATE system_state SET last_audit_hash = ? WHERE id = 1", (hex_hash,), ) def set_kill_switch( self, conn: sqlite3.Connection, *, armed: bool, reason: str | None, now: datetime, ) -> None: conn.execute( "UPDATE system_state SET kill_switch = ?, kill_reason = ?, " "kill_at = ?, last_health_check = ? WHERE id = 1", ( 1 if armed else 0, reason, _enc_dt(now) if armed else None, _enc_dt(now), ), ) def touch_health_check( self, conn: sqlite3.Connection, *, now: datetime ) -> None: conn.execute( "UPDATE system_state SET last_health_check = ? WHERE id = 1", (_enc_dt(now),), ) def set_auto_pause( self, conn: sqlite3.Connection, *, until: datetime | None, reason: str | None, ) -> None: """Imposta o azzera la pausa automatica (§7-bis F). ``until = None`` annulla la pausa (l'engine torna attivo). Il setter è idempotente: chiamarlo con un until già nel passato è equivalente a clear. """ conn.execute( "UPDATE system_state SET auto_pause_until = ?, " "auto_pause_reason = ? WHERE id = 1", (_enc_dt(until) if until is not None else None, reason), ) def recent_closed_position_pnls_usd( self, conn: sqlite3.Connection, *, limit: int ) -> list[Decimal]: """Ritorna la lista dei pnl_usd delle ultime ``limit`` posizioni chiuse, ordinate dalla più recente alla più vecchia. Posizioni con ``pnl_usd`` ``NULL`` (es. chiuse di emergenza senza P/L noto) sono saltate. Usato dal circuit breaker §7-bis F. """ if limit <= 0: return [] rows = conn.execute( "SELECT pnl_usd FROM positions " "WHERE closed_at IS NOT NULL AND pnl_usd IS NOT NULL " "ORDER BY closed_at DESC LIMIT ?", (limit,), ).fetchall() return [Decimal(row["pnl_usd"]) for row in rows] # --------------------------------------------------------------------------- # Row → model converters # --------------------------------------------------------------------------- def _dec_dt_required(value: Any) -> datetime: out = _dec_dt(value) if out is None: raise ValueError("expected non-null datetime in row") return out def _row_to_position(row: sqlite3.Row) -> PositionRecord: proposal_id = _dec_uuid(row["proposal_id"]) if proposal_id is None: raise ValueError("positions.proposal_id was NULL") return PositionRecord( proposal_id=proposal_id, spread_type=row["spread_type"], asset=row["asset"], expiry=_dec_dt_required(row["expiry"]), short_strike=_dec_dec_required(row["short_strike"]), long_strike=_dec_dec_required(row["long_strike"]), short_instrument=row["short_instrument"], long_instrument=row["long_instrument"], n_contracts=int(row["n_contracts"]), spread_width_usd=_dec_dec_required(row["spread_width_usd"]), spread_width_pct=_dec_dec_required(row["spread_width_pct"]), credit_eth=_dec_dec_required(row["credit_eth"]), credit_usd=_dec_dec_required(row["credit_usd"]), max_loss_usd=_dec_dec_required(row["max_loss_usd"]), spot_at_entry=_dec_dec_required(row["spot_at_entry"]), dvol_at_entry=_dec_dec_required(row["dvol_at_entry"]), delta_at_entry=_dec_dec_required(row["delta_at_entry"]), eth_price_at_entry=_dec_dec_required(row["eth_price_at_entry"]), proposed_at=_dec_dt_required(row["proposed_at"]), opened_at=_dec_dt(row["opened_at"]), closed_at=_dec_dt(row["closed_at"]), close_reason=row["close_reason"], debit_paid_eth=_dec_dec(row["debit_paid_eth"]), pnl_eth=_dec_dec(row["pnl_eth"]), pnl_usd=_dec_dec(row["pnl_usd"]), status=row["status"], created_at=_dec_dt_required(row["created_at"]), updated_at=_dec_dt_required(row["updated_at"]), ) def _row_to_instruction(row: sqlite3.Row) -> InstructionRecord: instruction_id = _dec_uuid(row["instruction_id"]) proposal_id = _dec_uuid(row["proposal_id"]) if instruction_id is None or proposal_id is None: raise ValueError("instructions row missing required UUID") return InstructionRecord( instruction_id=instruction_id, proposal_id=proposal_id, kind=row["kind"], payload_json=row["payload_json"], sent_at=_dec_dt_required(row["sent_at"]), acknowledged_at=_dec_dt(row["acknowledged_at"]), filled_at=_dec_dt(row["filled_at"]), cancelled_at=_dec_dt(row["cancelled_at"]), actual_fill_eth=_dec_dec(row["actual_fill_eth"]), actual_fees_eth=_dec_dec(row["actual_fees_eth"]), ) def _row_to_decision(row: sqlite3.Row) -> DecisionRecord: return DecisionRecord( id=int(row["id"]), decision_type=row["decision_type"], proposal_id=_dec_uuid(row["proposal_id"]), timestamp=_dec_dt_required(row["timestamp"]), inputs_json=row["inputs_json"], outputs_json=row["outputs_json"], action_taken=row["action_taken"], notes=row["notes"], ) def _row_to_manual(row: sqlite3.Row) -> ManualAction: return ManualAction( id=int(row["id"]), kind=row["kind"], proposal_id=_dec_uuid(row["proposal_id"]), payload_json=row["payload_json"], created_at=_dec_dt_required(row["created_at"]), consumed_at=_dec_dt(row["consumed_at"]), consumed_by=row["consumed_by"], result=row["result"], ) 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 _row_to_option_chain_quote(row: sqlite3.Row) -> OptionChainQuoteRecord: return OptionChainQuoteRecord( timestamp=_dec_dt_required(row["timestamp"]), asset=row["asset"], instrument_name=row["instrument_name"], strike=_dec_dec_required(row["strike"]), expiry=_dec_dt_required(row["expiry"]), option_type=row["option_type"], bid=_dec_dec(row["bid"]), ask=_dec_dec(row["ask"]), mid=_dec_dec(row["mid"]), iv=_dec_dec(row["iv"]), delta=_dec_dec(row["delta"]), gamma=_dec_dec(row["gamma"]), theta=_dec_dec(row["theta"]), vega=_dec_dec(row["vega"]), open_interest=( int(row["open_interest"]) if row["open_interest"] is not None else None ), volume_24h=( int(row["volume_24h"]) if row["volume_24h"] is not None else None ), book_depth_top3=( int(row["book_depth_top3"]) if row["book_depth_top3"] is not None else None ), ) def _dec_dec_required(value: Any) -> Decimal: out = _dec_dec(value) if out is None: raise ValueError("expected non-null Decimal in row") return out