mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-11 05:07:25 -04:00
feat: add trading_days schema migration
This commit is contained in:
111
api/migrations/001_trading_days_schema.py
Normal file
111
api/migrations/001_trading_days_schema.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""Migration: Create trading_days, holdings, and actions tables."""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from api.database import Database
|
||||||
|
|
||||||
|
|
||||||
|
def create_trading_days_schema(db: "Database") -> None:
|
||||||
|
"""Create new schema for day-centric trading results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database instance to apply migration to
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create trading_days table
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS trading_days (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- Starting position (cash only, holdings from previous day)
|
||||||
|
starting_cash REAL NOT NULL,
|
||||||
|
starting_portfolio_value REAL NOT NULL,
|
||||||
|
|
||||||
|
-- Daily performance metrics
|
||||||
|
daily_profit REAL NOT NULL,
|
||||||
|
daily_return_pct REAL NOT NULL,
|
||||||
|
|
||||||
|
-- Ending state (cash only, holdings in separate table)
|
||||||
|
ending_cash REAL NOT NULL,
|
||||||
|
ending_portfolio_value REAL NOT NULL,
|
||||||
|
|
||||||
|
-- Reasoning
|
||||||
|
reasoning_summary TEXT,
|
||||||
|
reasoning_full TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
total_actions INTEGER DEFAULT 0,
|
||||||
|
session_duration_seconds REAL,
|
||||||
|
days_since_last_trading INTEGER DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(job_id, model, date),
|
||||||
|
FOREIGN KEY (job_id) REFERENCES jobs(job_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create index for lookups
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_trading_days_lookup
|
||||||
|
ON trading_days(job_id, model, date)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create holdings table (ending positions only)
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS holdings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
trading_day_id INTEGER NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (trading_day_id) REFERENCES trading_days(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(trading_day_id, symbol)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create index for holdings lookups
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_holdings_day
|
||||||
|
ON holdings(trading_day_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create actions table (trade ledger)
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS actions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
trading_day_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
action_type TEXT NOT NULL,
|
||||||
|
symbol TEXT,
|
||||||
|
quantity INTEGER,
|
||||||
|
price REAL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (trading_day_id) REFERENCES trading_days(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create index for actions lookups
|
||||||
|
db.connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_actions_day
|
||||||
|
ON actions(trading_day_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
db.connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def drop_old_positions_table(db: "Database") -> None:
|
||||||
|
"""Drop deprecated positions table after migration complete.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database instance
|
||||||
|
"""
|
||||||
|
db.connection.execute("DROP TABLE IF EXISTS positions")
|
||||||
|
db.connection.commit()
|
||||||
1
api/migrations/__init__.py
Normal file
1
api/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Database schema migrations."""
|
||||||
84
tests/unit/test_trading_days_schema.py
Normal file
84
tests/unit/test_trading_days_schema.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import pytest
|
||||||
|
import sqlite3
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Import migration module with numeric prefix
|
||||||
|
migration_path = os.path.join(os.path.dirname(__file__), '../../api/migrations/001_trading_days_schema.py')
|
||||||
|
spec = importlib.util.spec_from_file_location("migration_001", migration_path)
|
||||||
|
migration_001 = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["migration_001"] = migration_001
|
||||||
|
spec.loader.exec_module(migration_001)
|
||||||
|
create_trading_days_schema = migration_001.create_trading_days_schema
|
||||||
|
|
||||||
|
|
||||||
|
class MockDatabase:
|
||||||
|
"""Simple mock database for testing migrations."""
|
||||||
|
def __init__(self, connection):
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
|
||||||
|
class TestTradingDaysSchema:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def db(self, tmp_path):
|
||||||
|
"""Create temporary test database."""
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
connection = sqlite3.connect(str(db_path))
|
||||||
|
return MockDatabase(connection)
|
||||||
|
|
||||||
|
def test_create_trading_days_table(self, db):
|
||||||
|
"""Test trading_days table is created with correct schema."""
|
||||||
|
create_trading_days_schema(db)
|
||||||
|
|
||||||
|
# Query schema
|
||||||
|
cursor = db.connection.execute(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='trading_days'"
|
||||||
|
)
|
||||||
|
schema = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Verify required columns
|
||||||
|
assert "job_id TEXT NOT NULL" in schema
|
||||||
|
assert "model TEXT NOT NULL" in schema
|
||||||
|
assert "date TEXT NOT NULL" in schema
|
||||||
|
assert "starting_cash REAL NOT NULL" in schema
|
||||||
|
assert "starting_portfolio_value REAL NOT NULL" in schema
|
||||||
|
assert "daily_profit REAL NOT NULL" in schema
|
||||||
|
assert "daily_return_pct REAL NOT NULL" in schema
|
||||||
|
assert "ending_cash REAL NOT NULL" in schema
|
||||||
|
assert "ending_portfolio_value REAL NOT NULL" in schema
|
||||||
|
assert "reasoning_summary TEXT" in schema
|
||||||
|
assert "reasoning_full TEXT" in schema
|
||||||
|
assert "UNIQUE(job_id, model, date)" in schema
|
||||||
|
|
||||||
|
def test_create_holdings_table(self, db):
|
||||||
|
"""Test holdings table is created with correct schema."""
|
||||||
|
create_trading_days_schema(db)
|
||||||
|
|
||||||
|
cursor = db.connection.execute(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='holdings'"
|
||||||
|
)
|
||||||
|
schema = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
assert "trading_day_id INTEGER NOT NULL" in schema
|
||||||
|
assert "symbol TEXT NOT NULL" in schema
|
||||||
|
assert "quantity INTEGER NOT NULL" in schema
|
||||||
|
assert "FOREIGN KEY (trading_day_id) REFERENCES trading_days(id)" in schema
|
||||||
|
assert "UNIQUE(trading_day_id, symbol)" in schema
|
||||||
|
|
||||||
|
def test_create_actions_table(self, db):
|
||||||
|
"""Test actions table is created with correct schema."""
|
||||||
|
create_trading_days_schema(db)
|
||||||
|
|
||||||
|
cursor = db.connection.execute(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='actions'"
|
||||||
|
)
|
||||||
|
schema = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
assert "trading_day_id INTEGER NOT NULL" in schema
|
||||||
|
assert "action_type TEXT NOT NULL" in schema
|
||||||
|
assert "symbol TEXT" in schema
|
||||||
|
assert "quantity INTEGER" in schema
|
||||||
|
assert "price REAL" in schema
|
||||||
|
assert "FOREIGN KEY (trading_day_id) REFERENCES trading_days(id)" in schema
|
||||||
Reference in New Issue
Block a user