from __future__ import annotations import sqlite3 from backend.database.migrations.runner import Migration def upgrade(connection: sqlite3.Connection) -> None: statements = ( """ CREATE TABLE screener_factor_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, trade_date TEXT NOT NULL, version TEXT NOT NULL, observed_at TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('final', 'archive')), source_set_json TEXT NOT NULL, coverage_json TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE (trade_date, version) ) """, """ CREATE TABLE screener_factor_values ( snapshot_id INTEGER NOT NULL REFERENCES screener_factor_snapshots(id) ON DELETE CASCADE, identifier TEXT NOT NULL, code TEXT NOT NULL, name TEXT NOT NULL, sector TEXT, listed_days INTEGER NOT NULL, is_st INTEGER NOT NULL CHECK (is_st IN (0, 1)), payload_json TEXT NOT NULL, PRIMARY KEY (snapshot_id, identifier) ) """, """ CREATE TABLE screener_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, mode TEXT NOT NULL CHECK (mode IN ('stage', 'curated', 'custom')), strategy_id TEXT NOT NULL, strategy_name TEXT NOT NULL, strategy_version INTEGER NOT NULL, selection_date TEXT NOT NULL, factor_snapshot_id INTEGER NOT NULL REFERENCES screener_factor_snapshots(id) ON DELETE RESTRICT, status TEXT NOT NULL CHECK ( status IN ( 'pending', 'running', 'completed', 'no_signal', 'data_incomplete', 'failed' ) ), started_at TEXT, completed_at TEXT, coverage REAL NOT NULL DEFAULT 0 CHECK (coverage >= 0 AND coverage <= 1), missing_fields_json TEXT NOT NULL DEFAULT '[]', result_json TEXT NOT NULL DEFAULT '[]', error_message TEXT NOT NULL DEFAULT '' ) """, """ CREATE UNIQUE INDEX screener_runs_idempotency_idx ON screener_runs ( mode, strategy_id, selection_date, strategy_version, factor_snapshot_id, COALESCE(owner_user_id, 0) ) """, """ CREATE TABLE custom_screener_strategies ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, formula_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE (user_id, name) ) """, """ CREATE TABLE strategy_tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, run_id INTEGER NOT NULL REFERENCES screener_runs(id) ON DELETE CASCADE, identifier TEXT NOT NULL, code TEXT NOT NULL, name TEXT NOT NULL, sector TEXT, selection_date TEXT NOT NULL, strategy_name TEXT NOT NULL, entry_price REAL NOT NULL CHECK (entry_price > 0), added_at TEXT NOT NULL, UNIQUE (user_id, run_id, identifier) ) """, """ CREATE TABLE strategy_track_bars ( track_id INTEGER NOT NULL REFERENCES strategy_tracks(id) ON DELETE CASCADE, trade_date TEXT NOT NULL, open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL, PRIMARY KEY (track_id, trade_date) ) """, """ CREATE TABLE strategy_track_events ( track_id INTEGER NOT NULL REFERENCES strategy_tracks(id) ON DELETE CASCADE, milestone TEXT NOT NULL CHECK (milestone IN ('t1', 't5')), created_at TEXT NOT NULL, PRIMARY KEY (track_id, milestone) ) """, ) for statement in statements: connection.execute(statement) def downgrade(connection: sqlite3.Connection) -> None: for table in ( "strategy_track_events", "strategy_track_bars", "strategy_tracks", "custom_screener_strategies", "screener_runs", "screener_factor_values", "screener_factor_snapshots", ): connection.execute(f"DROP TABLE {table}") MIGRATION = Migration( version=7, name="create_deterministic_screener", signature="screener:v1:factors-runs-custom-tracking", upgrade=upgrade, downgrade=downgrade, )