From 977cebb2aa32399cdf349886b699b1b1d5822a85 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Tue, 8 Sep 2026 09:27:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(factor):=20=E8=B4=A2=E5=8A=A1=E5=9B=A0?= =?UTF-8?q?=E5=AD=90=E5=89=8D=E7=AB=AF=E5=B9=B6=E5=88=97=E7=B1=BB=E5=88=AB?= =?UTF-8?q?+run=E5=AF=BC=E5=85=A5=E4=B8=BB=E5=BA=93=E8=84=9A=E6=9C=AC=20[n?= =?UTF-8?q?as]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Leaderboard.vue 类别 chips/样式映射加 fundamental(财务基本面),teal 色系 - tokens.css 新增 --teal 类别色变量(#2dd4bf,未占用色) - import_runs_to_main_db.py:独立评估库指定 run_ids 幂等导入主库 (INSERT OR REPLACE,列序按 dst PRAGMA 实取,写前只读预检, 本地临时双库自测:幂等/零沾染/列序不匹配/缺失run rc=1 全过) Co-Authored-By: Claude Code --- frontend/src/styles/tokens.css | 3 + frontend/src/views/factor/Leaderboard.vue | 8 + .../factor_research/import_runs_to_main_db.py | 156 ++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 scripts/factor_research/import_runs_to_main_db.py diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 015cb07..ae00d03 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -23,6 +23,9 @@ /* —— 次强调:紫(alpha158)—— */ --purple-dim: #b48cff; /* alpha158 类别色 */ + /* —— 类别色:teal(fundamental 财务因子)—— */ + --teal: #2dd4bf; /* fundamental 类别色 */ + /* —— 数据高亮:琥珀 —— */ --amber: #ffb000; /* 数据高亮/警告 */ --warn: #ffb000; /* 语义别名 */ diff --git a/frontend/src/views/factor/Leaderboard.vue b/frontend/src/views/factor/Leaderboard.vue index a3ca7e5..b773ba2 100644 --- a/frontend/src/views/factor/Leaderboard.vue +++ b/frontend/src/views/factor/Leaderboard.vue @@ -22,6 +22,7 @@ const CATEGORY_CHIPS = [ { key: 'alpha101', label: 'Alpha101' }, { key: 'alpha158', label: 'Alpha158' }, { key: 'builtin', label: '内置' }, + { key: 'fundamental', label: '财务基本面' }, ] async function load() { @@ -87,6 +88,7 @@ function getCategoryClass(cat: string): string { alpha101: 'cat-alpha101', alpha158: 'cat-alpha158', builtin: 'cat-builtin', + fundamental: 'cat-fundamental', } return map[cat] || 'cat-builtin' } @@ -608,6 +610,12 @@ td .fexpr { border-color: rgba(255, 176, 0, 0.3); } +.cat-fundamental { + color: var(--teal); + background: rgba(45, 212, 191, 0.1); + border-color: rgba(45, 212, 191, 0.3); +} + .pos { color: var(--up); } diff --git a/scripts/factor_research/import_runs_to_main_db.py b/scripts/factor_research/import_runs_to_main_db.py new file mode 100644 index 0000000..bab62e0 --- /dev/null +++ b/scripts/factor_research/import_runs_to_main_db.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +"""把独立因子评估库的指定 run 导入主 factor_eval.db(幂等追加). + +背景: 财务批在 NAS 独立库 /volume1/stock/factor_eval_fundamental.db 产出 +(fund-p0-h1 / fund-p0-h2 / quant62_ref, category=fundamental),前端 +Leaderboard 只读主库(默认 factor_eval.db),导入后即可与量价类别并列展示。 + +用法: + python scripts/factor_research/import_runs_to_main_db.py \ + --src /volume1/stock/factor_eval_fundamental.db \ + --dst /volume1/stock/sanguo_vnpy_v2/data_backup/factor_eval.db \ + --run-ids fund-p0-h1,fund-p0-h2,quant62_ref + (--dst 省略时用 default_eval_db_path(),容器内即主库默认位) + +语义: + - 对 --run-ids 每个 run_id,src 的 eval_runs 行 + 该 run 全部 eval_results + 行 INSERT OR REPLACE 进 dst;列取 dst 实际 PRAGMA table_info(不手写 schema), + src 只需含 dst 的列即可; + - dst 中不属于这些 run_id 的行一律不动(纯追加语义,重复执行幂等); + - 写前只读预检:逐 run 打印 src 将导入/dst 将被替换行数;任一 run_id 在 + src 不存在则报错退出、零写入。 +""" +import argparse +import os +import sqlite3 +import sys + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +TABLES = ("eval_runs", "eval_results") + + +def _connect_ro(path: str) -> sqlite3.Connection: + conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + return conn + + +def _connect_rw(path: str) -> sqlite3.Connection: + conn = sqlite3.connect(path, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + return conn + + +def _require_table(conn: sqlite3.Connection, table: str, db_tag: str) -> None: + row = conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,) + ).fetchone() + if row is None: + raise SystemExit(f"[错误] {db_tag} 缺表 {table}(不是 factor_eval 库?)") + + +def _table_cols(conn: sqlite3.Connection, table: str) -> list[str]: + return [r[1] for r in conn.execute(f"PRAGMA table_info({table})")] + + +def _count(conn: sqlite3.Connection, table: str, run_id: str) -> int: + return conn.execute( + f"SELECT COUNT(*) FROM {table} WHERE run_id=?", (run_id,) + ).fetchone()[0] + + +def main() -> int: + ap = argparse.ArgumentParser( + description="因子评估 run 跨库导入(独立库→主库,幂等追加)") + ap.add_argument("--src", required=True, help="源 factor_eval 库(只读打开)") + ap.add_argument("--dst", default=None, + help="主 factor_eval 库;默认 default_eval_db_path()") + ap.add_argument("--run-ids", required=True, help="逗号分隔 run_id 列表") + args = ap.parse_args() + + run_ids = [s.strip() for s in args.run_ids.split(",") if s.strip()] + if not run_ids: + print("[错误] --run-ids 为空", file=sys.stderr) + return 1 + + dst = args.dst + if dst is None: + sys.path.insert(0, _REPO_ROOT) + from sanguo_factor.eval_store import default_eval_db_path + dst = default_eval_db_path() + + src = _connect_ro(args.src) + dstc = _connect_rw(dst) + try: + for table in TABLES: + _require_table(src, table, f"src={args.src}") + _require_table(dstc, table, f"dst={dst}") + + # —— 只读预检:src 将导入 / dst 将被替换 —— # + print(f"[预检] src={args.src}") + print(f"[预检] dst={dst}") + missing = [] + plan: list[tuple[str, int, int]] = [] # (run_id, src_runs, src_results) + for rid in run_ids: + n_runs = _count(src, "eval_runs", rid) + n_res = _count(src, "eval_results", rid) + if n_runs == 0 and n_res == 0: + missing.append(rid) + continue + plan.append((rid, n_runs, n_res)) + print(f"[预检] {rid}: src runs={n_runs} results={n_res} | " + f"dst 已有 runs={_count(dstc, 'eval_runs', rid)} " + f"results={_count(dstc, 'eval_results', rid)}(同主键将被替换)") + if missing: + print(f"[错误] 以下 run_id 在 src 不存在,零写入退出: {missing}", + file=sys.stderr) + return 1 + + tot_runs = sum(p[1] for p in plan) + tot_res = sum(p[2] for p in plan) + print(f"[预检] 将导入 eval_runs {tot_runs} 行 + eval_results {tot_res} 行 " + f"(INSERT OR REPLACE,dst 其他行不动)") + + # —— 写入:单事务包两表,任一失败整体回滚 —— # + with dstc: + for table in TABLES: + dst_cols = _table_cols(dstc, table) + src_cols = set(_table_cols(src, table)) + absent = [c for c in dst_cols if c not in src_cols] + if absent: + print(f"[错误] src.{table} 缺列 {absent}(src schema 过旧)," + "零写入退出", file=sys.stderr) + return 1 + placeholders = ",".join("?" for _ in dst_cols) + col_list = ",".join(dst_cols) + rows = src.execute( + f"SELECT {col_list} FROM {table} WHERE run_id " + f"IN ({','.join('?' for _ in run_ids)})", + run_ids, + ).fetchall() + dstc.executemany( + f"INSERT OR REPLACE INTO {table}({col_list}) " + f"VALUES({placeholders})", rows) + print(f"[写入] {table}: {len(rows)} 行") + finally: + src.close() + dstc.close() + + # —— 核验(幂等口径:再跑一次行数不变) —— # + check = _connect_ro(dst) + try: + marks = ",".join("?" for _ in run_ids) + n_runs = check.execute( + f"SELECT COUNT(*) FROM eval_runs WHERE run_id IN ({marks})", + run_ids).fetchone()[0] + n_res = check.execute( + f"SELECT COUNT(*) FROM eval_results WHERE run_id IN ({marks})", + run_ids).fetchone()[0] + finally: + check.close() + print(f"[核验] dst 中这些 run 现有: eval_runs={n_runs} eval_results={n_res}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())