"""SQLite state management for ORB paper trading.""" from __future__ import annotations import os import sqlite3 import uuid from datetime import datetime, timezone from pathlib import Path from typing import Any from apps.orb_trader.models import ( CREATE_TABLES_SQL, ORBCandidateRow, ORBDailySnapshotRow, ORBDailyStateRow, ORBPositionRow, ORBSessionRow, ORBTradeRow, ) _DEFAULT_DB = "data/paper/orb.db" class ORBStateManager: """SQLite CRUD layer for ORB paper trading state.""" def __init__(self, db_path: str | None = None) -> None: self._db_path = db_path or os.environ.get("ORB_TRADER_DB", _DEFAULT_DB) Path(self._db_path).parent.mkdir(parents=True, exist_ok=True) self._init_db() def _connect(self) -> sqlite3.Connection: conn = sqlite3.connect(self._db_path) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") return conn def _init_db(self) -> None: with self._connect() as conn: conn.executescript(CREATE_TABLES_SQL) # ── Sessions ────────────────────────────────────────────────────────────── def create_session( self, name: str, config_path: str, initial_equity: float, ) -> str: session_id = str(uuid.uuid4())[:8] now = datetime.now(timezone.utc).isoformat() with self._connect() as conn: conn.execute( """INSERT INTO sessions (session_id, session_name, config_path, initial_equity, created_at) VALUES (?, ?, ?, ?, ?)""", (session_id, name, config_path, initial_equity, now), ) return session_id def get_session(self, name_or_id: str) -> ORBSessionRow | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM sessions WHERE session_id=? OR session_name=?", (name_or_id, name_or_id), ).fetchone() if row is None: return None return ORBSessionRow(**dict(row)) def list_sessions(self) -> list[ORBSessionRow]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM sessions ORDER BY created_at DESC" ).fetchall() return [ORBSessionRow(**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: with self._connect() as conn: conn.execute("DELETE FROM orb_candidates WHERE session_id=?", (session_id,)) conn.execute("DELETE FROM orb_positions WHERE session_id=?", (session_id,)) conn.execute("DELETE FROM trades WHERE session_id=?", (session_id,)) conn.execute("DELETE FROM daily_snapshots WHERE session_id=?", (session_id,)) conn.execute("DELETE FROM daily_state WHERE session_id=?", (session_id,)) conn.execute("DELETE FROM sessions WHERE session_id=?", (session_id,)) # ── Positions ───────────────────────────────────────────────────────────── def save_position(self, pos: ORBPositionRow) -> None: with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO orb_positions (session_id, date, ticker, direction, entry_price, entry_time, shares, orb_high, orb_low, atr_at_entry, stop_distance, current_stop, peak_price, trailing_active, rvol, composite_score, order_id, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( pos.session_id, pos.date, pos.ticker, pos.direction, pos.entry_price, pos.entry_time, pos.shares, pos.orb_high, pos.orb_low, pos.atr_at_entry, pos.stop_distance, pos.current_stop, pos.peak_price, int(pos.trailing_active), pos.rvol, pos.composite_score, pos.order_id, pos.status, ), ) def get_open_positions(self, session_id: str, date: str) -> list[ORBPositionRow]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM orb_positions WHERE session_id=? AND date=? AND status='open'", (session_id, date), ).fetchall() return [_row_to_position(r) for r in rows] def get_all_open_positions(self, session_id: str) -> list[ORBPositionRow]: """Get all open positions across all dates (for reconciliation).""" with self._connect() as conn: rows = conn.execute( "SELECT * FROM orb_positions WHERE session_id=? AND status='open'", (session_id,), ).fetchall() return [_row_to_position(r) for r in rows] def update_position_stop( self, session_id: str, date: str, ticker: str, current_stop: float, peak_price: float, trailing_active: bool, ) -> None: with self._connect() as conn: conn.execute( """UPDATE orb_positions SET current_stop=?, peak_price=?, trailing_active=? WHERE session_id=? AND date=? AND ticker=? AND status='open'""", (current_stop, peak_price, int(trailing_active), session_id, date, ticker), ) def close_position_record(self, session_id: str, date: str, ticker: str) -> None: with self._connect() as conn: conn.execute( """UPDATE orb_positions SET status='closed' WHERE session_id=? AND date=? AND ticker=?""", (session_id, date, ticker), ) # ── Trades ──────────────────────────────────────────────────────────────── def save_trade(self, trade: ORBTradeRow) -> None: with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO trades (trade_id, session_id, date, ticker, direction, entry_price, exit_price, entry_time, exit_time, shares, pnl, r_multiple, exit_reason, atr_at_entry, rvol, composite_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( trade.trade_id, trade.session_id, trade.date, trade.ticker, trade.direction, trade.entry_price, trade.exit_price, trade.entry_time, trade.exit_time, trade.shares, trade.pnl, trade.r_multiple, trade.exit_reason, trade.atr_at_entry, trade.rvol, trade.composite_score, ), ) def list_trades( self, session_id: str, limit: int | None = None, ) -> list[dict[str, Any]]: with self._connect() as conn: if limit: rows = conn.execute( "SELECT * FROM trades WHERE session_id=? ORDER BY date DESC, exit_time DESC LIMIT ?", (session_id, limit), ).fetchall() else: rows = conn.execute( "SELECT * FROM trades WHERE session_id=? ORDER BY date DESC, exit_time DESC", (session_id,), ).fetchall() return [dict(r) for r in rows] # ── Daily Snapshots ─────────────────────────────────────────────────────── def save_daily_snapshot(self, snap: ORBDailySnapshotRow) -> None: with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO daily_snapshots (session_id, date, equity, daily_pnl, total_pnl, trades_taken, stops_hit, drawdown_pct) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", ( snap.session_id, snap.date, snap.equity, snap.daily_pnl, snap.total_pnl, snap.trades_taken, snap.stops_hit, snap.drawdown_pct, ), ) def list_snapshots(self, session_id: str) -> list[dict[str, Any]]: 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_equity(self, session_id: str) -> float | None: """Return the equity from the latest daily snapshot, or None if no snapshots exist.""" with self._connect() as conn: row = conn.execute( "SELECT equity FROM daily_snapshots WHERE session_id=? ORDER BY date DESC LIMIT 1", (session_id,), ).fetchone() return float(row[0]) if row else None def get_peak_equity(self, session_id: str, initial_equity: float) -> float: """Return the peak equity across all snapshots (for drawdown calculation).""" with self._connect() as conn: row = conn.execute( "SELECT MAX(equity) FROM daily_snapshots WHERE session_id=?", (session_id,), ).fetchone() return float(row[0]) if row and row[0] is not None else initial_equity # ── Daily State ─────────────────────────────────────────────────────────── def get_daily_state(self, session_id: str, date: str) -> ORBDailyStateRow: with self._connect() as conn: row = conn.execute( "SELECT * FROM daily_state WHERE session_id=? AND date=?", (session_id, date), ).fetchone() if row is None: return ORBDailyStateRow(session_id=session_id, date=date) return ORBDailyStateRow( session_id=row["session_id"], date=row["date"], cumulative_loss=row["cumulative_loss"], stops_hit=row["stops_hit"], kill_switch=bool(row["kill_switch"]), phase=row["phase"], ) def update_daily_state(self, session_id: str, date: str, **kwargs: Any) -> None: state = self.get_daily_state(session_id, date) for k, v in kwargs.items(): setattr(state, k, v) with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO daily_state (session_id, date, cumulative_loss, stops_hit, kill_switch, phase) VALUES (?, ?, ?, ?, ?, ?)""", ( state.session_id, state.date, state.cumulative_loss, state.stops_hit, int(state.kill_switch), state.phase, ), ) # ── Candidates ──────────────────────────────────────────────────────────── def save_candidate(self, cand: ORBCandidateRow) -> None: with self._connect() as conn: conn.execute( """INSERT OR REPLACE INTO orb_candidates (session_id, date, ticker, direction, orb_high, orb_low, breakout_level, atr, rvol, gap_pct, composite_score, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( cand.session_id, cand.date, cand.ticker, cand.direction, cand.orb_high, cand.orb_low, cand.breakout_level, cand.atr, cand.rvol, cand.gap_pct, cand.composite_score, cand.status, ), ) def update_candidate_status( self, session_id: str, date: str, ticker: str, status: str ) -> None: with self._connect() as conn: conn.execute( """UPDATE orb_candidates SET status=? WHERE session_id=? AND date=? AND ticker=?""", (status, session_id, date, ticker), ) def list_candidates(self, session_id: str, date: str) -> list[dict[str, Any]]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM orb_candidates WHERE session_id=? AND date=? ORDER BY composite_score DESC", (session_id, date), ).fetchall() return [dict(r) for r in rows] # ── Helpers ─────────────────────────────────────────────────────────────────── def _row_to_position(row: sqlite3.Row) -> ORBPositionRow: d = dict(row) d["trailing_active"] = bool(d.get("trailing_active", 0)) return ORBPositionRow(**d)