Files
xiaobai-review/database.py
T

747 lines
32 KiB
Python

from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from backend.database import MIGRATIONS, MigrationRunner, SQLiteConnectionFactory
from backend.features.accounts.repository import AccountRepositoryMixin
from backend.features.alerts.repository import AlertRepositoryMixin
from backend.features.auction.repository import AuctionRepositoryMixin
from backend.features.dragon_tiger.repository import DragonTigerRepositoryMixin
from backend.features.heaven.repository import HeavenRepositoryMixin
from backend.features.market.repository import MarketRepositoryMixin
from backend.features.mentor.repository import MentorRepositoryMixin
from backend.features.pools.repository import PoolRepositoryMixin
from backend.features.popularity.repository import PopularityRepositoryMixin
from backend.features.review.repository import ReviewRepositoryMixin
from backend.features.screener.repository import ScreenerRepositoryMixin
from backend.features.system.repository import SystemSettingsRepositoryMixin
from backend.llm.repository import LLMAuditRepositoryMixin
class ReviewDatabase(
AccountRepositoryMixin,
AlertRepositoryMixin,
AuctionRepositoryMixin,
DragonTigerRepositoryMixin,
HeavenRepositoryMixin,
MarketRepositoryMixin,
MentorRepositoryMixin,
PoolRepositoryMixin,
PopularityRepositoryMixin,
ReviewRepositoryMixin,
ScreenerRepositoryMixin,
SystemSettingsRepositoryMixin,
LLMAuditRepositoryMixin,
):
def __init__(self, path: Path) -> None:
self.path = path
self.path.parent.mkdir(parents=True, exist_ok=True)
self.connection_factory = SQLiteConnectionFactory(self.path)
self._initialize()
def connect(self) -> sqlite3.Connection:
return self.connection_factory.connect()
def _initialize(self) -> None:
with self.connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_salt TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_sessions (
token_hash TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
csrf_token TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_user_sessions_user
ON user_sessions(user_id, expires_at);
CREATE TABLE IF NOT EXISTS user_credentials (
user_id INTEGER PRIMARY KEY,
encrypted_payload TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_birth_profiles (
user_id INTEGER PRIMARY KEY,
encrypted_payload TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS system_settings (
setting_key TEXT PRIMARY KEY,
encrypted_payload TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS llm_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
feature TEXT NOT NULL,
source TEXT NOT NULL,
model TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
latency_ms INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_llm_usage_user_time
ON llm_usage(user_id, created_at DESC);
CREATE TABLE IF NOT EXISTS dashboard_snapshots (
trade_date TEXT PRIMARY KEY,
source TEXT NOT NULL,
payload TEXT NOT NULL,
record_count INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT NOT NULL,
source TEXT NOT NULL,
status TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT,
record_count INTEGER NOT NULL DEFAULT 0,
message TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sync_runs_trade_date
ON sync_runs(trade_date, id DESC);
CREATE TABLE IF NOT EXISTS data_snapshots (
kind TEXT NOT NULL,
cache_key TEXT NOT NULL,
source TEXT NOT NULL,
payload TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (kind, cache_key)
);
CREATE TABLE IF NOT EXISTS watchlist (
user_id INTEGER NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
sector TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT 'red',
remark TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, code),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS review_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code TEXT NOT NULL DEFAULT '',
stock_name TEXT NOT NULL DEFAULT '',
trade_date TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
plan TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_review_notes_code_date
ON review_notes(code, trade_date DESC, id DESC);
CREATE TABLE IF NOT EXISTS reason_overrides (
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
reason TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (trade_date, code)
);
CREATE TABLE IF NOT EXISTS seat_aliases (
seat_name TEXT PRIMARY KEY,
alias TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sector_phase_overrides (
name TEXT PRIMARY KEY,
element TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS stock_master (
ts_code TEXT PRIMARY KEY,
code TEXT NOT NULL,
name TEXT NOT NULL,
industry TEXT NOT NULL DEFAULT '',
market TEXT NOT NULL DEFAULT '',
list_date TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_stock_master_code ON stock_master(code);
CREATE TABLE IF NOT EXISTS daily_bars (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
open REAL NOT NULL DEFAULT 0,
high REAL NOT NULL DEFAULT 0,
low REAL NOT NULL DEFAULT 0,
close REAL NOT NULL DEFAULT 0,
pct_chg REAL NOT NULL DEFAULT 0,
vol REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_daily_bars_code_date
ON daily_bars(ts_code, trade_date DESC);
CREATE TABLE IF NOT EXISTS benchmark_bars (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL NOT NULL DEFAULT 0,
pct_chg REAL NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_benchmark_bars_code_date
ON benchmark_bars(ts_code, trade_date DESC);
CREATE TABLE IF NOT EXISTS daily_indicators (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
turnover_rate REAL NOT NULL DEFAULT 0,
volume_ratio REAL NOT NULL DEFAULT 0,
total_mv REAL NOT NULL DEFAULT 0,
circ_mv REAL NOT NULL DEFAULT 0,
pe_ttm REAL,
pb REAL,
ps_ttm REAL,
dv_ttm REAL,
PRIMARY KEY (trade_date, ts_code)
);
CREATE TABLE IF NOT EXISTS fundamental_indicators (
end_date TEXT NOT NULL,
ann_date TEXT NOT NULL DEFAULT '',
ts_code TEXT NOT NULL,
roe REAL,
roa REAL,
roic REAL,
grossprofit_margin REAL,
netprofit_yoy REAL,
or_yoy REAL,
ocf_to_opincome REAL,
PRIMARY KEY (end_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_fundamental_indicators_code_date
ON fundamental_indicators(ts_code, ann_date DESC, end_date DESC);
CREATE TABLE IF NOT EXISTS moneyflow_daily (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
net_mf_amount REAL NOT NULL DEFAULT 0,
large_net_amount REAL NOT NULL DEFAULT 0,
medium_net_amount REAL NOT NULL DEFAULT 0,
small_net_amount REAL NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE TABLE IF NOT EXISTS auction_factors (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
price REAL NOT NULL DEFAULT 0,
pre_close REAL NOT NULL DEFAULT 0,
change REAL NOT NULL DEFAULT 0,
vol REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
turnover_rate REAL NOT NULL DEFAULT 0,
volume_ratio REAL NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_auction_factors_code_date
ON auction_factors(ts_code, trade_date DESC);
CREATE TABLE IF NOT EXISTS earnings_events (
end_date TEXT NOT NULL,
ann_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
forecast_profit REAL,
actual_profit REAL,
surprise_pct REAL,
revenue_yoy REAL,
netprofit_yoy REAL,
source TEXT NOT NULL DEFAULT '',
PRIMARY KEY (end_date, ann_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_earnings_events_code_announcement
ON earnings_events(ts_code, ann_date DESC, end_date DESC);
CREATE TABLE IF NOT EXISTS popularity_factors (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
ths_rank INTEGER,
dc_rank INTEGER,
combined_score REAL NOT NULL DEFAULT 0,
rank_change INTEGER,
dual_source INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_popularity_factors_code_date
ON popularity_factors(ts_code, trade_date DESC);
CREATE TABLE IF NOT EXISTS lhb_institution_daily (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
net_buy_amount REAL NOT NULL DEFAULT 0,
buy_amount REAL NOT NULL DEFAULT 0,
sell_amount REAL NOT NULL DEFAULT 0,
seat_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (trade_date, ts_code)
);
CREATE INDEX IF NOT EXISTS idx_lhb_institution_code_date
ON lhb_institution_daily(ts_code, trade_date DESC);
CREATE TABLE IF NOT EXISTS screener_strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
regimes TEXT NOT NULL,
formula TEXT NOT NULL,
builtin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS screener_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
trade_date TEXT NOT NULL,
regime TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'smart',
strategy_name TEXT NOT NULL,
formula TEXT NOT NULL,
result TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS mentor_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
mentor_id TEXT NOT NULL,
trade_date TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
meta TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_mentor_messages_conversation
ON mentor_messages(user_id, mentor_id, trade_date, id DESC);
CREATE TABLE IF NOT EXISTS mentor_preferences (
user_id INTEGER NOT NULL,
mentor_id TEXT NOT NULL,
pinned INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, mentor_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_mentor_preferences_user_order
ON mentor_preferences(user_id, pinned DESC, sort_order, mentor_id);
CREATE TABLE IF NOT EXISTS wencai_saved_queries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
query TEXT NOT NULL,
search_type TEXT NOT NULL DEFAULT 'stock',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, query, search_type),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wencai_saved_queries_user
ON wencai_saved_queries(user_id, updated_at DESC, id DESC);
CREATE TABLE IF NOT EXISTS strategy_tracks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
run_id INTEGER NOT NULL,
selection_date TEXT NOT NULL,
strategy_name TEXT NOT NULL,
ts_code TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
sector TEXT NOT NULL DEFAULT '',
entry_price REAL NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, run_id, ts_code),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (run_id) REFERENCES screener_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_strategy_tracks_user_run
ON strategy_tracks(user_id, run_id DESC, id);
CREATE TABLE IF NOT EXISTS alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
kind TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
available_date TEXT NOT NULL,
code TEXT NOT NULL DEFAULT '',
dedupe_key TEXT NOT NULL,
is_read INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
read_at TEXT,
UNIQUE(user_id, dedupe_key),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_alerts_user_due
ON alerts(user_id, available_date, is_read, id DESC);
CREATE TABLE IF NOT EXISTS trade_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
action TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
position_pct REAL NOT NULL DEFAULT 0,
pnl_amount REAL,
pnl_pct REAL,
thesis TEXT NOT NULL DEFAULT '',
execution TEXT NOT NULL DEFAULT '',
emotion TEXT NOT NULL DEFAULT 'calm',
tags TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_trade_entries_user_date
ON trade_entries(user_id, trade_date DESC, id DESC);
CREATE TABLE IF NOT EXISTS assistant_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
context_date TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_assistant_messages_user
ON assistant_messages(user_id, id DESC);
CREATE TABLE IF NOT EXISTS heaven_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
mode TEXT NOT NULL,
context_date TEXT NOT NULL,
subject TEXT NOT NULL,
subject_detail TEXT NOT NULL DEFAULT '',
answer TEXT NOT NULL,
context_snapshot TEXT NOT NULL DEFAULT '{}',
dedupe_key TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(user_id, dedupe_key),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_heaven_readings_user_mode
ON heaven_readings(user_id, mode, context_date DESC, id DESC);
"""
)
user_columns = {
str(row["name"]) for row in connection.execute("PRAGMA table_info(users)")
}
migrations = {
"role": "ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'",
"llm_mode": "ALTER TABLE users ADD COLUMN llm_mode TEXT NOT NULL DEFAULT 'auto'",
"membership_status": "ALTER TABLE users ADD COLUMN membership_status TEXT NOT NULL DEFAULT 'inactive'",
"membership_plan": "ALTER TABLE users ADD COLUMN membership_plan TEXT NOT NULL DEFAULT ''",
"membership_starts_at": "ALTER TABLE users ADD COLUMN membership_starts_at TEXT",
"membership_expires_at": "ALTER TABLE users ADD COLUMN membership_expires_at TEXT",
}
for column, statement in migrations.items():
if column not in user_columns:
connection.execute(statement)
indicator_columns = {
str(row["name"])
for row in connection.execute("PRAGMA table_info(daily_indicators)")
}
indicator_migrations = {
"pe_ttm": "ALTER TABLE daily_indicators ADD COLUMN pe_ttm REAL",
"pb": "ALTER TABLE daily_indicators ADD COLUMN pb REAL",
"ps_ttm": "ALTER TABLE daily_indicators ADD COLUMN ps_ttm REAL",
"dv_ttm": "ALTER TABLE daily_indicators ADD COLUMN dv_ttm REAL",
}
for column, statement in indicator_migrations.items():
if column not in indicator_columns:
connection.execute(statement)
connection.execute(
"""
UPDATE users SET role = 'admin'
WHERE id = (SELECT MIN(id) FROM users)
AND NOT EXISTS (SELECT 1 FROM users WHERE role = 'admin')
"""
)
watchlist_columns = {
str(row["name"]) for row in connection.execute("PRAGMA table_info(watchlist)")
}
if "user_id" not in watchlist_columns:
connection.execute("ALTER TABLE watchlist RENAME TO watchlist_legacy")
connection.execute(
"""
CREATE TABLE watchlist (
user_id INTEGER NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
sector TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT 'red',
remark TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, code),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
"""
)
first_user = connection.execute("SELECT MIN(id) AS id FROM users").fetchone()
if first_user and first_user["id"]:
connection.execute(
"""
INSERT INTO watchlist
(user_id, code, name, sector, color, created_at, updated_at)
SELECT ?, code, name, sector, color, created_at, updated_at
FROM watchlist_legacy
""",
(int(first_user["id"]),),
)
connection.execute("DROP TABLE watchlist_legacy")
watchlist_columns.add("remark")
if "remark" not in watchlist_columns:
connection.execute(
"ALTER TABLE watchlist ADD COLUMN remark TEXT NOT NULL DEFAULT ''"
)
note_columns = {
str(row["name"]) for row in connection.execute("PRAGMA table_info(review_notes)")
}
if "user_id" not in note_columns:
connection.execute("ALTER TABLE review_notes ADD COLUMN user_id INTEGER")
if "summary" not in note_columns:
connection.execute(
"ALTER TABLE review_notes ADD COLUMN summary TEXT NOT NULL DEFAULT ''"
)
first_user = connection.execute("SELECT MIN(id) AS id FROM users").fetchone()
if first_user and first_user["id"]:
connection.execute(
"UPDATE review_notes SET user_id = ? WHERE user_id IS NULL",
(int(first_user["id"]),),
)
connection.execute(
"""
CREATE INDEX IF NOT EXISTS idx_review_notes_user_date
ON review_notes(user_id, trade_date DESC, id DESC)
"""
)
strategy_columns = {
str(row["name"]) for row in connection.execute("PRAGMA table_info(screener_strategies)")
}
if "user_id" not in strategy_columns:
connection.execute("ALTER TABLE screener_strategies ADD COLUMN user_id INTEGER")
run_columns = {
str(row["name"]) for row in connection.execute("PRAGMA table_info(screener_runs)")
}
legacy_run_ownership = "user_id" not in run_columns
if "user_id" not in run_columns:
connection.execute("ALTER TABLE screener_runs ADD COLUMN user_id INTEGER")
if "mode" not in run_columns:
connection.execute(
"ALTER TABLE screener_runs ADD COLUMN mode TEXT NOT NULL DEFAULT 'smart'"
)
legacy_runs = connection.execute(
"SELECT id, strategy_name, formula FROM screener_runs"
).fetchall()
for run in legacy_runs:
try:
formula = json.loads(run["formula"])
except (TypeError, json.JSONDecodeError):
formula = {}
meta = formula.get("meta") if isinstance(formula, dict) else {}
library = str((meta or {}).get("library") or "")
category = str((meta or {}).get("category") or "")
if library == "curated":
mode = "curated"
elif library == "quant" or (
library == "custom" and category == "量化公式"
) or str(run["strategy_name"] or "") == "自定义量化公式":
mode = "quant"
else:
mode = "smart"
connection.execute(
"UPDATE screener_runs SET mode = ? WHERE id = ?",
(mode, int(run["id"])),
)
connection.execute(
"UPDATE screener_runs SET user_id = NULL WHERE user_id = 0"
)
if first_user and first_user["id"]:
first_user_id = int(first_user["id"])
connection.execute(
"UPDATE screener_strategies SET user_id = ? WHERE builtin = 0 AND user_id IS NULL",
(first_user_id,),
)
if legacy_run_ownership:
connection.execute(
"UPDATE screener_runs SET user_id = ? WHERE user_id IS NULL",
(first_user_id,),
)
else:
connection.execute(
"""
UPDATE screener_runs SET user_id = ?
WHERE user_id IS NULL AND mode = 'quant'
""",
(first_user_id,),
)
connection.execute(
"""
CREATE INDEX IF NOT EXISTS idx_screener_strategies_user
ON screener_strategies(user_id, builtin, updated_at DESC)
"""
)
connection.execute(
"""
CREATE INDEX IF NOT EXISTS idx_screener_runs_user_date
ON screener_runs(user_id, trade_date DESC, id DESC)
"""
)
connection.execute(
"""
CREATE INDEX IF NOT EXISTS idx_screener_runs_user_mode_date
ON screener_runs(user_id, mode, trade_date DESC, id DESC)
"""
)
MigrationRunner().apply(connection, MIGRATIONS)
def list_sector_phase_overrides(self) -> dict[str, str]:
with self.connect() as connection:
rows = connection.execute(
"SELECT name, element FROM sector_phase_overrides ORDER BY updated_at DESC, name"
).fetchall()
return {row["name"]: row["element"] for row in rows}
def save_sector_phase_override(self, name: str, element: str) -> None:
now = datetime.now().astimezone().isoformat(timespec="seconds")
with self.connect() as connection:
connection.execute(
"""
INSERT INTO sector_phase_overrides (name, element, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
element = excluded.element,
updated_at = excluded.updated_at
""",
(name, element, now),
)
def delete_sector_phase_override(self, name: str) -> bool:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM sector_phase_overrides WHERE name = ?",
(name,),
)
return cursor.rowcount > 0
def list_wencai_saved_queries(
self, user_id: int, limit: int = 30
) -> list[dict[str, Any]]:
with self.connect() as connection:
rows = connection.execute(
"""
SELECT id, title, query, search_type, created_at, updated_at
FROM wencai_saved_queries
WHERE user_id = ?
ORDER BY updated_at DESC, id DESC LIMIT ?
""",
(int(user_id), max(1, min(100, int(limit)))),
).fetchall()
return [dict(row) for row in rows]
def save_wencai_query(
self, user_id: int, title: str, query: str, search_type: str = "stock"
) -> int:
now = datetime.now().astimezone().isoformat(timespec="seconds")
with self.connect() as connection:
connection.execute(
"""
INSERT INTO wencai_saved_queries
(user_id, title, query, search_type, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, query, search_type) DO UPDATE SET
title = excluded.title,
updated_at = excluded.updated_at
""",
(int(user_id), title, query, search_type, now, now),
)
row = connection.execute(
"""
SELECT id FROM wencai_saved_queries
WHERE user_id = ? AND query = ? AND search_type = ?
""",
(int(user_id), query, search_type),
).fetchone()
if not row:
raise ValueError("问财条件保存失败。")
return int(row["id"])
def delete_wencai_saved_query(self, user_id: int, query_id: int) -> bool:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM wencai_saved_queries WHERE id = ? AND user_id = ?",
(int(query_id), int(user_id)),
)
return cursor.rowcount > 0