"""SQLite state management for paper trading sessions.""" from __future__ import annotations import datetime as dt import sqlite3 import uuid from dataclasses import dataclass from pathlib import Path from typing import Any from apps.paper_trader.models import create_schema @dataclass class SessionRow: session_id: str session_name: str config_path: str initial_equity: float created_at: str status: str parking_preset: str | None = None idle_alpha_preset: str | None = None form4_sleeve_preset: str | None = None ownership_sleeve_preset: str | None = None risk_off_alpha_sleeve_preset: str | None = None @dataclass class StrategyStateRow: session_id: str symbol: str event_id: str engine_id: str entry_date: str stop_price: float target_price: float current_stop: float peak_price: float days_held: int trade_direction: str candidate_json: str plan_json: str status: str id: int | None = None order_id: str | None = None @dataclass class SessionStateRow: session_id: str consecutive_losses: int = 0 cooldown_remaining: int = 0 kill_switch_triggered: bool = False daily_new_risk_used: float = 0.0 last_processed_date: str | None = None @dataclass class DailySnapshotRow: session_id: str date: str equity: float cash: float market_value: float daily_pnl: float | None = None total_pnl: float | None = None drawdown_pct: float | None = None open_position_count: int | None = None class StateManager: """SQLite CRUD for paper trading state.""" def __init__(self, db_path: str | Path) -> None: self.db_path = Path(db_path) create_schema(self.db_path) def _connect(self) -> sqlite3.Connection: conn = sqlite3.connect(str(self.db_path)) conn.row_factory = sqlite3.Row return conn # ------------------------------------------------------------------ # # Sessions # ------------------------------------------------------------------ # def create_session( self, session_name: str, config_path: str, initial_equity: float, parking_preset: str | None = None, idle_alpha_preset: str | None = None, form4_sleeve_preset: str | None = None, ownership_sleeve_preset: str | None = None, risk_off_alpha_sleeve_preset: str | None = None, ) -> str: session_id = str(uuid.uuid4())[:8] created_at = dt.datetime.now(tz=dt.timezone.utc).isoformat() with self._connect() as conn: conn.execute( "INSERT INTO sessions (session_id, session_name, config_path, initial_equity, created_at, status, parking_preset, idle_alpha_preset, form4_sleeve_preset, ownership_sleeve_preset, risk_off_alpha_sleeve_preset) " "VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)", ( session_id, session_name, config_path, initial_equity, created_at, parking_preset, idle_alpha_preset, form4_sleeve_preset, ownership_sleeve_preset, risk_off_alpha_sleeve_preset, ), ) conn.execute( "INSERT INTO session_state (session_id) VALUES (?)", (session_id,), ) return session_id def get_session(self, session_name_or_id: str) -> SessionRow | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM sessions WHERE session_id = ? OR session_name = ? LIMIT 1", (session_name_or_id, session_name_or_id), ).fetchone() if row is None: return None return SessionRow(**dict(row)) def list_sessions(self) -> list[SessionRow]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM sessions ORDER BY created_at" ).fetchall() return [SessionRow(**dict(r)) for r in rows] def set_session_status(self, session_id: str, status: str) -> None: with self._connect() as conn: conn.execute( "UPDATE sessions SET status = ? WHERE session_id = ?", (status, session_id), ) def delete_session(self, session_id: str) -> None: """Delete a session and all related data.""" with self._connect() as conn: for table in ( "processed_phases", "processed_dates", "daily_snapshots", "trades", "processed_events", "strategy_states", "session_state", ): conn.execute(f"DELETE FROM {table} WHERE session_id = ?", (session_id,)) conn.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,)) # ------------------------------------------------------------------ # # Strategy states # ------------------------------------------------------------------ # def get_open_strategy_states(self, session_id: str) -> list[StrategyStateRow]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM strategy_states WHERE session_id = ? AND status IN ('open', 'partial')", (session_id,), ).fetchall() return [StrategyStateRow(**dict(r)) for r in rows] def get_strategy_state_by_symbol( self, session_id: str, symbol: str ) -> StrategyStateRow | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM strategy_states WHERE session_id = ? AND symbol = ? AND status IN ('open', 'partial') LIMIT 1", (session_id, symbol), ).fetchone() if row is None: return None return StrategyStateRow(**dict(row)) def save_strategy_state(self, session_id: str, state: StrategyStateRow) -> None: with self._connect() as conn: conn.execute( """INSERT INTO strategy_states (session_id, symbol, event_id, engine_id, order_id, entry_date, stop_price, target_price, current_stop, peak_price, days_held, trade_direction, candidate_json, plan_json, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, symbol, status) DO UPDATE SET current_stop=excluded.current_stop, peak_price=excluded.peak_price, days_held=excluded.days_held, order_id=excluded.order_id""", ( session_id, state.symbol, state.event_id, state.engine_id, state.order_id, state.entry_date, state.stop_price, state.target_price, state.current_stop, state.peak_price, state.days_held, state.trade_direction, state.candidate_json, state.plan_json, state.status, ), ) def update_strategy_state( self, session_id: str, symbol: str, *, days_held: int | None = None, current_stop: float | None = None, peak_price: float | None = None, status: str | None = None, ) -> None: updates: list[str] = [] values: list[Any] = [] if days_held is not None: updates.append("days_held = ?") values.append(days_held) if current_stop is not None: updates.append("current_stop = ?") values.append(current_stop) if peak_price is not None: updates.append("peak_price = ?") values.append(peak_price) if status is not None: updates.append("status = ?") values.append(status) if not updates: return values.extend([session_id, symbol]) with self._connect() as conn: conn.execute( f"UPDATE strategy_states SET {', '.join(updates)} " "WHERE session_id = ? AND symbol = ? AND status IN ('open', 'partial')", values, ) def close_strategy_state(self, session_id: str, symbol: str) -> None: with self._connect() as conn: conn.execute( "DELETE FROM strategy_states " "WHERE session_id = ? AND symbol = ? AND status IN ('open', 'partial')", (session_id, symbol), ) # ------------------------------------------------------------------ # # Session-level state # ------------------------------------------------------------------ # def get_session_state(self, session_id: str) -> SessionStateRow: with self._connect() as conn: row = conn.execute( "SELECT * FROM session_state WHERE session_id = ?", (session_id,), ).fetchone() if row is None: return SessionStateRow(session_id=session_id) d = dict(row) d["kill_switch_triggered"] = bool(d["kill_switch_triggered"]) return SessionStateRow(**d) def update_session_state(self, state: SessionStateRow) -> None: with self._connect() as conn: conn.execute( """INSERT INTO session_state (session_id, consecutive_losses, cooldown_remaining, kill_switch_triggered, daily_new_risk_used, last_processed_date) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET consecutive_losses=excluded.consecutive_losses, cooldown_remaining=excluded.cooldown_remaining, kill_switch_triggered=excluded.kill_switch_triggered, daily_new_risk_used=excluded.daily_new_risk_used, last_processed_date=excluded.last_processed_date""", ( state.session_id, state.consecutive_losses, state.cooldown_remaining, int(state.kill_switch_triggered), state.daily_new_risk_used, state.last_processed_date, ), ) # ------------------------------------------------------------------ # # Processed events # ------------------------------------------------------------------ # def has_processed_event(self, session_id: str, event_id: str) -> bool: with self._connect() as conn: row = conn.execute( "SELECT 1 FROM processed_events WHERE session_id = ? AND event_id = ?", (session_id, event_id), ).fetchone() return row is not None def record_processed_event( self, session_id: str, event_id: str, processed_date: str, action: str, skip_reason: str | None = None, ) -> None: with self._connect() as conn: conn.execute( "INSERT OR IGNORE INTO processed_events " "(session_id, event_id, processed_date, action, skip_reason) " "VALUES (?, ?, ?, ?, ?)", (session_id, event_id, processed_date, action, skip_reason), ) # ------------------------------------------------------------------ # # Trades # ------------------------------------------------------------------ # def record_trade( self, session_id: str, symbol: str, engine_id: str | None, capital_bucket_id: str | None, entry_date: str | None, exit_date: str, entry_price: float | None, exit_price: float, exit_reason: str, shares: int, net_pnl: float, r_multiple: float, holding_days: int, ) -> str: trade_id = str(uuid.uuid4()) with self._connect() as conn: conn.execute( "INSERT INTO trades (trade_id, session_id, symbol, engine_id, capital_bucket_id, entry_date, exit_date, " "entry_price, exit_price, exit_reason, shares, net_pnl, r_multiple, holding_days) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( trade_id, session_id, symbol, engine_id, capital_bucket_id, entry_date, exit_date, entry_price, exit_price, exit_reason, shares, net_pnl, r_multiple, holding_days, ), ) return trade_id def list_trades(self, session_id: str, limit: int | None = None) -> list[dict]: with self._connect() as conn: if limit: rows = conn.execute( "SELECT * FROM trades WHERE session_id = ? ORDER BY exit_date DESC LIMIT ?", (session_id, limit), ).fetchall() else: rows = conn.execute( "SELECT * FROM trades WHERE session_id = ? ORDER BY exit_date", (session_id,), ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # # Daily snapshots # ------------------------------------------------------------------ # def save_daily_snapshot(self, row: DailySnapshotRow) -> None: with self._connect() as conn: conn.execute( """INSERT INTO daily_snapshots (session_id, date, equity, cash, market_value, daily_pnl, total_pnl, drawdown_pct, open_position_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, date) DO UPDATE SET equity=excluded.equity, cash=excluded.cash, market_value=excluded.market_value, daily_pnl=excluded.daily_pnl, total_pnl=excluded.total_pnl, drawdown_pct=excluded.drawdown_pct, open_position_count=excluded.open_position_count""", ( row.session_id, row.date, row.equity, row.cash, row.market_value, row.daily_pnl, row.total_pnl, row.drawdown_pct, row.open_position_count, ), ) def list_snapshots(self, session_id: str) -> list[dict]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM daily_snapshots WHERE session_id = ? ORDER BY date", (session_id,), ).fetchall() return [dict(r) for r in rows] def get_peak_equity(self, session_id: str, initial_equity: float) -> float: with self._connect() as conn: row = conn.execute( "SELECT MAX(equity) FROM daily_snapshots WHERE session_id = ?", (session_id,), ).fetchone() if row and row[0] is not None: return max(float(row[0]), initial_equity) return initial_equity # ------------------------------------------------------------------ # # Idempotency: processed dates # ------------------------------------------------------------------ # def is_phase_processed(self, session_id: str, date: dt.date, phase: str) -> bool: with self._connect() as conn: row = conn.execute( "SELECT 1 FROM processed_phases WHERE session_id = ? AND date = ? AND phase = ?", (session_id, date.isoformat(), phase), ).fetchone() return row is not None def mark_phase_processed(self, session_id: str, date: dt.date, phase: str) -> None: with self._connect() as conn: conn.execute( "INSERT OR IGNORE INTO processed_phases (session_id, date, phase) VALUES (?, ?, ?)", (session_id, date.isoformat(), phase), ) def is_date_processed(self, session_id: str, date: dt.date) -> bool: with self._connect() as conn: row = conn.execute( "SELECT 1 FROM processed_dates WHERE session_id = ? AND date = ?", (session_id, date.isoformat()), ).fetchone() return row is not None def mark_date_processed(self, session_id: str, date: dt.date) -> None: with self._connect() as conn: conn.execute( "INSERT OR IGNORE INTO processed_dates (session_id, date) VALUES (?, ?)", (session_id, date.isoformat()), ) # ------------------------------------------------------------------ # # Cash Parking # ------------------------------------------------------------------ # def get_parking_state(self, session_id: str) -> dict[str, Any] | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM parking_state WHERE session_id = ? AND status = 'active'", (session_id,), ).fetchone() if row is None: return None return dict(row) def save_parking_state( self, session_id: str, symbol: str, entry_date: dt.date, qty: int, avg_price: float, entry_value: float, peak_price: float = 0, gate_in_sgov: int = 0, committed_target: str = "", sgov_entry_value: float = 0, ) -> None: with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO parking_state (session_id, symbol, entry_date, qty, avg_price, entry_value, status, peak_price, gate_in_sgov, committed_target, pending_target, pending_days, sgov_entry_value, sold_today) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, '', 0, ?, 0)""", (session_id, symbol, entry_date.isoformat(), qty, avg_price, entry_value, peak_price or avg_price, gate_in_sgov, committed_target or symbol.lower(), sgov_entry_value or entry_value), ) def list_parking_entries(self, session_id: str) -> list[dict]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM parking_state WHERE session_id = ? ORDER BY entry_date", (session_id,), ).fetchall() return [dict(r) for r in rows] def close_parking_state(self, session_id: str) -> None: with self._connect() as conn: conn.execute( "UPDATE parking_state SET status = 'closed' WHERE session_id = ? AND status = 'active'", (session_id,), ) def update_parking_peak(self, session_id: str, peak_price: float) -> None: with self._connect() as conn: conn.execute( "UPDATE parking_state SET peak_price = ? WHERE session_id = ? AND status = 'active'", (peak_price, session_id), ) def update_parking_gate_state( self, session_id: str, **kwargs: Any, ) -> None: """Update gate hysteresis fields. Accepts: gate_in_sgov, committed_target, pending_target, pending_days, sold_today, overlay_brake_cooldown, overlay_hold_days.""" allowed = {"gate_in_sgov", "committed_target", "pending_target", "pending_days", "sold_today", "overlay_brake_cooldown", "overlay_hold_days"} updates, params = [], [] for k, v in kwargs.items(): if k in allowed: updates.append(f"{k} = ?") params.append(v) if not updates: return params.append(session_id) with self._connect() as conn: conn.execute( f"UPDATE parking_state SET {', '.join(updates)} WHERE session_id = ? AND status = 'active'", params, ) def update_parking_sgov_value(self, session_id: str, new_value: float) -> None: with self._connect() as conn: conn.execute( "UPDATE parking_state SET entry_value = ? WHERE session_id = ? AND status = 'active'", (new_value, session_id), )