717 lines
31 KiB
Python
717 lines
31 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime
|
|
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_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
|