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:
@@ -23,6 +23,9 @@
|
||||
/* —— 次强调:紫(alpha158)—— */
|
||||
--purple-dim: #b48cff; /* alpha158 类别色 */
|
||||
|
||||
/* —— 类别色:teal(fundamental 财务因子)—— */
|
||||
--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())
|
||||
Reference in New Issue
Block a user