diff --git a/scripts/factor_research/run_eval.py b/scripts/factor_research/run_eval.py new file mode 100644 index 0000000..20daa6d --- /dev/null +++ b/scripts/factor_research/run_eval.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +"""批量因子评估 CLI(冒烟/全量;NAS docker exec / 本地直跑). + +用法示例: + 冒烟(50只×2024×10因子): + venv310/bin/python scripts/factor_research/run_eval.py \ + --start 2024-01-01 --end 2024-12-31 --limit 50 --label smoke-2024 \ + --factors alpha2,alpha6,alpha12,alpha18,kmid,klen,roc_5,ma_20,std_20,wvma_20 + 全量(Alpha101+158 × 全A × 8.5年): + venv310/bin/python scripts/factor_research/run_eval.py --label batch1-full +""" +import argparse +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0"))) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--start", default="2018-01-01") + ap.add_argument("--end", default="2026-06-30") + ap.add_argument("--categories", nargs="*", default=["alpha101", "alpha158"]) + ap.add_argument("--factors", default="", help="逗号分隔,优先于 --categories") + ap.add_argument("--symbols", default="", help="逗号分隔裸代码;空=全A") + ap.add_argument("--limit", type=int, default=None, help="随机抽样 N 只(种子42)") + ap.add_argument("--label", default="batch1") + ap.add_argument("--db", default=None, help="factor_eval.db 路径;默认 default_eval_db_path()") + ap.add_argument("--list-factors", default="", metavar="CATEGORY", help="列出类目因子后退出") + args = ap.parse_args() + + from sanguo_factor.alpha_datasets import mount_all + mount_all() + from sanguo_factor.registry import list_factors + from sanguo_factor.batch_eval import run_batch_eval + from sanguo_factor.eval_store import default_eval_db_path, get_rows + + if args.list_factors: + for f in list_factors(args.list_factors): + print(f"{f['name']:16s} [{f['category']}] {f['expression'][:80]}") + return 0 + + if args.factors: + factor_names = [s.strip() for s in args.factors.split(",") if s.strip()] + else: + factor_names = [f["name"] for c in args.categories for f in list_factors(c)] + if not factor_names: + print("未找到因子(检查 --categories/--factors)", file=sys.stderr) + return 1 + + db_path = args.db or default_eval_db_path() + symbols = [s.strip() for s in args.symbols.split(",") if s.strip()] or None + print(f"[eval] {len(factor_names)} 因子 × {args.start}~{args.end} → {db_path}") + + def _cb(done: int, total: int, current: str) -> None: + print(f"[eval] {done}/{total} {current}", flush=True) + + out = run_batch_eval(factor_names, args.start, args.end, db_path, label=args.label, + symbols=symbols, limit=args.limit, cfg=None, progress_cb=_cb) + + print(f"[eval] run_id={out['run_id']} done={out['factors_done']} " + f"errors={len(out['errors'])} elapsed={out['elapsed_sec']}s symbols={out['symbols_count']}") + if out["errors"]: + print(f"[eval] 失败因子: {', '.join(out['errors'][:20])}") + + rows = get_rows(db_path, out["run_id"]) + scored = [] + for r in rows: + p1 = (r.get("metrics") or {}).get("1") or {} + if p1.get("icir") is not None: + scored.append((abs(p1["icir"]), r["factor"], p1)) + scored.sort(reverse=True) + print(f"\n{'因子':<14s} {'IC':>8s} {'ICIR':>8s} {'t':>8s} {'胜率':>7s} 结论") + for _, name, p1 in scored[:15]: + print(f"{name:<14s} {p1['ic_mean']:>8.4f} {p1['icir']:>8.3f} " + f"{p1['t_stat']:>8.2f} {p1['win_rate']:>7.1%} {p1['conclusion']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())