diff --git a/api/migrations/001_trading_days_schema.py b/api/migrations/001_trading_days_schema.py new file mode 100644 index 0000000..630f188 --- /dev/null +++ b/api/migrations/001_trading_days_schema.py @@ -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() diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..67415b7 --- /dev/null +++ b/api/migrations/__init__.py @@ -0,0 +1 @@ +"""Database schema migrations.""" diff --git a/tests/unit/test_trading_days_schema.py b/tests/unit/test_trading_days_schema.py new file mode 100644 index 0000000..6b17b00 --- /dev/null +++ b/tests/unit/test_trading_days_schema.py @@ -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