Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
109 lines
4.2 KiB
Python
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()
|