|
| 1 | +""" |
| 2 | +SQLAlchemy table definitions for the Council trading bot. |
| 3 | +
|
| 4 | +Tables |
| 5 | +------ |
| 6 | +cycles One row per council run (context snapshot + agent outputs + signal). |
| 7 | +trades One row per order placed (entry → exit lifecycle). |
| 8 | +portfolio_state Singleton row tracking cash and peak portfolio value. |
| 9 | +reflections One row per post-trade reflection summary. |
| 10 | +
|
| 11 | +Usage |
| 12 | +----- |
| 13 | + from src.db.schema import get_engine, create_all |
| 14 | +
|
| 15 | + engine = get_engine() # default: ./council.db |
| 16 | + create_all(engine) # idempotent — safe to call every startup |
| 17 | +""" |
| 18 | + |
| 19 | +import os |
| 20 | + |
| 21 | +from sqlalchemy import ( |
| 22 | + Boolean, |
| 23 | + Column, |
| 24 | + DateTime, |
| 25 | + Float, |
| 26 | + Integer, |
| 27 | + String, |
| 28 | + Text, |
| 29 | + create_engine, |
| 30 | +) |
| 31 | +from sqlalchemy import MetaData |
| 32 | + |
| 33 | +metadata = MetaData() |
| 34 | + |
| 35 | +from sqlalchemy import Table |
| 36 | + |
| 37 | +cycles = Table( |
| 38 | + "cycles", |
| 39 | + metadata, |
| 40 | + Column("id", Integer, primary_key=True, autoincrement=True), |
| 41 | + Column("timestamp", DateTime, nullable=False), |
| 42 | + Column("asset", String(16), nullable=False), |
| 43 | + Column("signal", String(8), nullable=False), # BUY | SELL | HOLD |
| 44 | + Column("conviction", String(8), nullable=False), # high | medium | low |
| 45 | + Column("vetoed", Boolean, nullable=False), |
| 46 | + Column("context_json", Text, nullable=False), |
| 47 | + Column("council_outputs_json", Text, nullable=False), |
| 48 | + Column("deliberation_json", Text, nullable=False), |
| 49 | +) |
| 50 | + |
| 51 | +trades = Table( |
| 52 | + "trades", |
| 53 | + metadata, |
| 54 | + Column("id", Integer, primary_key=True, autoincrement=True), |
| 55 | + Column("cycle_id", Integer, nullable=False), # FK → cycles.id |
| 56 | + Column("symbol", String(16), nullable=False), |
| 57 | + Column("side", String(4), nullable=False), # BUY | SELL |
| 58 | + Column("entry_price", Float, nullable=False), |
| 59 | + Column("entry_time", DateTime, nullable=False), |
| 60 | + Column("position_size_usd", Float, nullable=False), |
| 61 | + Column("position_size_btc", Float, nullable=False), |
| 62 | + Column("stop_loss", Float, nullable=False), |
| 63 | + Column("take_profit", Float, nullable=True), |
| 64 | + Column("exchange_order_id", String(64), nullable=True), |
| 65 | + Column("sl_order_id", String(64), nullable=True), |
| 66 | + # Filled in when the position is closed |
| 67 | + Column("exit_price", Float, nullable=True), |
| 68 | + Column("exit_time", DateTime, nullable=True), |
| 69 | + Column("pnl_usd", Float, nullable=True), |
| 70 | + Column("pnl_pct", Float, nullable=True), |
| 71 | + Column("status", String(8), nullable=False, default="open"), # open | closed | stopped |
| 72 | +) |
| 73 | + |
| 74 | +portfolio_state = Table( |
| 75 | + "portfolio_state", |
| 76 | + metadata, |
| 77 | + # Always a single row with id=1 (upsert pattern). |
| 78 | + Column("id", Integer, primary_key=True), |
| 79 | + Column("cash_usd", Float, nullable=False), |
| 80 | + Column("peak_value", Float, nullable=False), |
| 81 | + Column("updated_at", DateTime, nullable=False), |
| 82 | +) |
| 83 | + |
| 84 | +reflections = Table( |
| 85 | + "reflections", |
| 86 | + metadata, |
| 87 | + Column("id", Integer, primary_key=True, autoincrement=True), |
| 88 | + Column("trade_id", Integer, nullable=False), # FK → trades.id |
| 89 | + Column("summary", Text, nullable=False), |
| 90 | + Column("created_at", DateTime, nullable=False), |
| 91 | +) |
| 92 | + |
| 93 | +_DEFAULT_DB_PATH = os.getenv("COUNCIL_DB_PATH", "council.db") |
| 94 | + |
| 95 | + |
| 96 | +def get_engine(db_path: str = _DEFAULT_DB_PATH): |
| 97 | + """Return a SQLAlchemy engine. Use ``db_path=':memory:'`` for tests.""" |
| 98 | + return create_engine(f"sqlite:///{db_path}", future=True) |
| 99 | + |
| 100 | + |
| 101 | +def create_all(engine) -> None: |
| 102 | + """Create all tables if they do not exist (idempotent).""" |
| 103 | + metadata.create_all(engine) |
0 commit comments