feat(factor): 财务因子前端并列类别+run导入主库脚本 [nas]

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 09:27:40 +08:00
parent 67fae2fef2
commit 977cebb2aa
3 changed files with 167 additions and 0 deletions
+3
View File
@@ -23,6 +23,9 @@
/* —— 次强调:紫(alpha158)—— */
--purple-dim: #b48cff; /* alpha158 类别色 */
/* —— 类别色:tealfundamental 财务因子)—— */
--teal: #2dd4bf; /* fundamental 类别色 */
/* —— 数据高亮:琥珀 —— */
--amber: #ffb000; /* 数据高亮/警告 */
--warn: #ffb000; /* 语义别名 */
@@ -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);
}
@@ -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())