Files
trading-agent/state/db.py

109 lines
4.2 KiB
Python

"""SQLite state: audit log + context cache, dimensioned by account (persona).
Alpaca remains the source of truth for live positions; this DB records what each agent
proposed and did, and caches each account's latest snapshot so a nightly agent can see its
own holdings (tools/portfolio.py) and the dashboard can show all three books.
"""
from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
SCHEMA = """
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY, trade_date TEXT, started_at TEXT, ended_at TEXT, status TEXT
);
CREATE TABLE IF NOT EXISTS theses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT, agent TEXT, ticker TEXT, direction TEXT, conviction REAL,
thesis TEXT, requested_weight_pct REAL, stop_pct REAL, take_pct REAL, horizon TEXT,
json_blob TEXT
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT, agent TEXT, ticker TEXT, side TEXT, qty INTEGER, entry_type TEXT,
est_price REAL, stop_price REAL, take_price REAL,
alpaca_order_id TEXT, status TEXT, avg_fill_price REAL, submitted_at TEXT
);
CREATE TABLE IF NOT EXISTS snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT, account TEXT, equity REAL, cash REAL, buying_power REAL, positions_json TEXT
);
CREATE TABLE IF NOT EXISTS positions (
symbol TEXT, account TEXT, qty REAL, avg_entry REAL, market_value REAL,
unrealized_pl REAL, as_of TEXT, PRIMARY KEY (symbol, account)
);
"""
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def connect(path: str | Path) -> sqlite3.Connection:
con = sqlite3.connect(str(path))
con.row_factory = sqlite3.Row
return con
def init_db(path: str | Path) -> None:
con = connect(path)
con.executescript(SCHEMA)
con.commit()
con.close()
def record_thesis(con, trade_date, agent, pick_dict: dict) -> None:
con.execute(
"INSERT INTO theses (trade_date, agent, ticker, direction, conviction, thesis, "
"requested_weight_pct, stop_pct, take_pct, horizon, json_blob) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(
trade_date, agent, pick_dict.get("ticker"), pick_dict.get("direction"),
pick_dict.get("conviction"), pick_dict.get("thesis"),
pick_dict.get("requested_weight_pct"), pick_dict.get("stop_loss_pct"),
pick_dict.get("take_profit_pct"), pick_dict.get("time_horizon"),
json.dumps(pick_dict),
),
)
def record_order(con, *, trade_date, agent, ticker, side, qty, entry_type, est_price,
stop_price, take_price, alpaca_order_id, status, avg_fill_price=None) -> None:
con.execute(
"INSERT INTO orders (trade_date, agent, ticker, side, qty, entry_type, est_price, "
"stop_price, take_price, alpaca_order_id, status, avg_fill_price, submitted_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(trade_date, agent, ticker, side, qty, entry_type, est_price, stop_price, take_price,
alpaca_order_id, status, avg_fill_price, _now()),
)
def record_snapshot(con, *, account, equity, cash, buying_power, positions: list[dict]) -> None:
con.execute(
"INSERT INTO snapshots (ts, account, equity, cash, buying_power, positions_json) "
"VALUES (?,?,?,?,?,?)",
(_now(), account, equity, cash, buying_power, json.dumps(positions)),
)
def upsert_position(con, *, account, symbol, qty, avg_entry, market_value, unrealized_pl) -> None:
con.execute(
"INSERT INTO positions (symbol, account, qty, avg_entry, market_value, unrealized_pl, "
"as_of) VALUES (?,?,?,?,?,?,?) ON CONFLICT(symbol, account) DO UPDATE SET "
"qty=excluded.qty, avg_entry=excluded.avg_entry, market_value=excluded.market_value, "
"unrealized_pl=excluded.unrealized_pl, as_of=excluded.as_of",
(symbol, account, qty, avg_entry, market_value, unrealized_pl, _now()),
)
def latest_snapshot(con, account: str | None = None) -> sqlite3.Row | None:
if account:
return con.execute(
"SELECT * FROM snapshots WHERE account=? ORDER BY ts DESC LIMIT 1", (account,)
).fetchone()
return con.execute("SELECT * FROM snapshots ORDER BY ts DESC LIMIT 1").fetchone()