Files
xiaobaifupan/next/backend/database/migrations/m0007_screener.py
T

140 lines
4.8 KiB
Python

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,
)