
把一套投研流程从每次开 Jupyter 重新写一遍、口径每次都不太一样变成点一下就跑完、结果可追溯这件事我惦记了差不多两年。WorkBuddy 这类支持 Skill 的工作台出来之后我把手上那套从数据采集到报告输出的流程拆成了 10 个 Skill串成一条四级闭环——采集、清洗、研究、决策最后再回流到研究。刚开始我也怀疑金融量化这种对数字精度近乎偏执的行当交给一套带着大模型的工具链到底靠不靠谱。跑了大半年结论是只要把算数和解释这两件事彻底分开Skill 化的量化流水线比手写脚本稳得多也省得多。这篇文章写给三类人一类是已经在做量化、手上有一堆散装脚本想整理成体系的一类是刚接触 WorkBuddy 或者类似工作台、想找一个完整场景练手的还有一类是纯粹好奇Skill 到底能干嘛的。我会把 10 个 Skill 的职责边界、数据契约、关键参数怎么定、坑在哪一次讲清楚。文里的目录结构、配置写法、参数取值都是我自己在跑的那套字段命名各个平台不完全一样按你手上工具的规范改一下就能用。1. 先搞明白 WorkBuddy 里的 Skill 到底是什么东西很多人第一次打开 WorkBuddy看到 Skill、Agent、指令、工作流一堆概念堆在一起第一反应是懵的。我在社区里被问得最多的一个问题就是Skill 和 Agent 到底差在哪这里先把这层窗户纸捅破后面讲编排才顺。1.1 Skill、Agent、Prompt 三者的分工边界我的理解是Agent 是干活的人Skill 是干活的手册Prompt 是这次具体要干什么的交代。Agent 负责的是决定做什么、什么时候做、做砸了怎么改它有工具调用循环、有上下文、有记忆。Skill 负责的是这件事的标准做法是什么它是一组打包好的资产——一份说明文档、几个确定性脚本、若干模板和参考数据。Prompt 就是你临时拍给它的任务描述。打个厨房的比方。Agent 是厨师本人会看冰箱、会尝味道、会临时改菜Skill 是挂在墙上的菜谱加上提前配好的酱料包规定了这道菜该放几克盐、火候多少、几分钟出锅Prompt 是今天客人说少放辣。厨师可以没有菜谱凭手感做菜但一家餐厅要稳定出品靠的必须是菜谱而不是每天都换一个厨师凭天赋发挥。这个边界划清楚之后你会发现一个特别重要的推论凡是涉及计算、涉及口径、涉及可复现的环节全部塞进 Skill 的确定性脚本里Agent 只负责调度和解释。我在前面说过要把算数和解释分开说的就是这件事。模型去算 Sharpe 比率它可能给你编一个看起来很合理的数脚本去算它只会给你一个确定的数。这条线守住了整套系统就稳了一大半。1.2 为什么金融量化特别适合拆成 Skill不是所有领域都值得 Skill 化。写公众号、做头脑风暴Agent 自由发挥反而更好。但量化投研有几个特征几乎是为 Skill 量身定做的。第一个特征是流程高度重复且形状固定。无论是日频还是分钟频投研链路永远是采集、清洗、因子、检验、回测、归因、组合、输出这几步每一步的输入输出形态是稳定的。稳定的东西就该被固化固化了才能批量化。第二个特征是口径漂移的代价极高。我踩过最难受的一个坑两个脚本里一个把成交额当万元、一个当元中间差了四个数量级回测跑出来收益率曲线漂亮得离谱找了两天才发现是单位问题。Skill 的价值就在于把口径写死在文件里谁调用都走同一套转换而不是靠脑子记。第三个特征是复核需求强。任何一份投研结论别人都会问这个数怎么来的。Skill 化之后每个中间产物都落地成 Parquet 或者 CSV带上_manifest.json记录行数、时间戳、参数哈希被问的时候直接把文件甩过去比翻聊天记录体面得多。第四个特征是计算密集但解释部分轻。一整套流水线跑下来真正需要大模型参与的其实只有最后写解读那一段前面的计算用 pandas、numpy、cvxpy 就是最优解没必要也不该让模型插手。这意味着 token 成本非常可控一套流水线跑一天模型费用可能就几毛钱。1.3 上手前的环境准备与目录约定平台本身的安装按官方说明走就行不同版本路径不太一样我不在这里复述。真正值得花时间的是目录约定这个定错了后面 10 个 Skill 之间的接口会乱成一锅粥。我用的结构长这样workbuddy-quant/ ├── skills/ │ ├── 01-market-fetch/ │ │ ├── SKILL.md │ │ ├── config.yaml │ │ └── scripts/fetch.py │ ├── 02-filing-parser/ │ ├── 03-data-doctor/ │ └── ...共 10 个 ├── data/ │ ├── raw/ # 原始落盘只追加不修改 │ ├── clean/ # 清洗对齐后的标准宽表 │ ├── factor/ # 因子值 │ └── result/ # 回测、归因、组合结果 ├── config/ │ └── global.yaml # 交易日历、股票池、费率 ├── logs/ └── run_all.py三个原则raw 只追加不修改任何时候都能从原始数据重跑每一层只依赖上一层clean 不会去读 raw 以外的目录factor 不会去读 result所有路径写相对路径整个目录可以打包搬走换机器直接跑。Python 侧我用的依赖很朴素pip install pandas numpy pyarrow scipy statsmodels cvxpy jinja2 pyyaml tqdmpyarrow是必须的Parquet 读写全靠它比 CSV 快一个数量级还自带类型。cvxpy留给组合优化那个 Skill。jinja2留给报告模板。其余都是常规配置。数据源 SDK 按你实际用的装这块各家不一样。注意不要在第一版就上数据库。我一开始热血上头搭了套本地 PostgreSQL结果 90% 的时间花在维护连接和迁移脚本上数据量其实只有几百万行Parquet 分区存储完全够用还不用起服务。2. 四级投研闭环的整体设计与 10 个 Skill 的编排思路骨架搭完接下来是最关键的一步怎么切。切得太细Skill 之间来回传参数传到怀疑人生切得太粗一个 Skill 干五件事出错了根本不知道是哪一段崩的。我前后重构过两轮最终定在 10 个每一层两到四个这个颗粒度是我实测下来最好维护的。2.1 四级闭环采集、清洗、研究、决策我把整条链路分成四级划分依据不是功能相似而是失败模式相似。采集层的问题永远是外部接口不稳超时、限流、字段改名、返回格式变化全是外部因素重试就能解决大半。清洗层的问题永远是数据本身脏缺失、错位、单位混乱、复权断裂全是内部因素要靠校验规则兜住。研究层的问题永远是逻辑对不对因子有没有未来函数、回测有没有偷看数据、成本假设是不是太乐观要靠纪律和交叉验证。决策层的问题永远是表达准不准结论有没有超出数据支持的范围、口径有没有对齐、风险有没有漏报。按失败模式切分的好处是排查的时候你只需要知道是哪一层挂了就能立刻定位到那一层的几个 Skill不用从头上翻。这是从无数次半夜爬起来修数据之后悟出来的。四级之间不是单向的最后有一个回流箭头。决策层输出报告之后风险哨兵会把异常点抛回研究层比如某个因子最近两个月 IC 突然转负这个信息要触发因子检验 Skill 重新跑一遍看看是失效了还是只是短期波动。这个回流机制是整套东西从自动化脚本升级成闭环的关键缺了它就只是个批处理。2.2 10 个 Skill 的职责切分与数据契约具体到每个 Skill我习惯在SKILL.md里写清楚四件事干什么、吃什么、吐什么、什么时候不要用它。最后一条最容易被忽略但恰恰是防止 Agent 乱调用的关键。编号Skill 名称所属层级输入输出01market-fetch采集股票池、日期区间raw/market/*.parquet02filing-parser采集公告/财报原文raw/filing/*.parquet03>name: factor-test description: 对因子值批量计算 IC、ICIR、分层收益、换手率与自相关 输出标准体检表。当用户要求检验因子有效性、或流水线进入研究层时调用。 inputs: - data/factor/*.parquet # 索引 date, 列 symbol, 值因子暴露 outputs: - data/result/factor_test/summary.csv - data/result/factor_test/ic_series.parquet do_not_use_when: 因子值尚未完成截面标准化或样本区间不足 12 个月description里的那段话是给 Agent 看的触发条件写得越具体它越不容易误触发。do_not_use_when是我自己加的约定字段平台未必认但 Agent 读文档时能看到实际效果不错。2.3 串联方式文件总线加显式入参别让 Agent 自由编排这里有个分歧点值得单独说。很多人第一反应是让 Agent 自己决定调用顺序智能编排听起来很美。我试过结论是在量化场景下不要这么做。原因有三。第一投研链路的顺序是硬约束清洗没跑完就跑因子结果一定是错的这不是智能能解决的问题是逻辑问题。第二自由编排意味着每次运行的路径可能不同出了问题没法复现而投研最需要的就是可复现。第三调试成本会爆炸你永远不知道是哪一步的上下文影响了模型的选择。我的做法是文件总线加显式入参。每个 Skill 从磁盘约定位置读、往约定位置写中间不通过 Agent 的上下文传递数据。主控脚本run_all.py按固定顺序调度每一步跑完检查退出码和_manifest.json不通过就停下来报警。Agent 在这里的角色是调度员加翻译官它能听懂帮我看看上周新能源板块的因子表现这种自然语言把它翻译成具体的参数去调用对应的 Skill跑完之后读结果文件写成人话。它不参与数据怎么算只参与算完了怎么讲。这么设计之后整套系统的稳定性跟传统脚本几乎没差别但易用性上了一个台阶。3. 采集层与清洗层前 3 个 Skill 把数据入口焊死数据入口是最容易被轻视的部分。我见过太多人在因子研究上花三个月结果发现底层数据从第一天就是错的。前三个 Skill 的核心任务只有一个让下游永远不必怀疑数据的正确性。3.1 行情采集 Skill增量拉取与幂等设计01-market-fetch是整条流水线的水龙头。它的关键设计点是增量和幂等这两点没做好后面全是麻烦。增量指的是每次只拉缺失的部分。做法是先读本地 Parquet 的最后日期再决定请求区间# skills/01-market-fetch/scripts/fetch.py from pathlib import Path import pandas as pd RAW Path(data/raw/market) def last_date(symbol: str): f RAW / f{symbol}.parquet if not f.exists(): return None return pd.read_parquet(f, columns[date])[date].max() def fetch_incremental(symbol: str, start: str, end: str, source): have last_date(symbol) # 关键从已有日期回退 5 个自然日重拉防止增量边界错位 begin start if have is None else (have - pd.Timedelta(days5)).strftime(%Y-%m-%d) if begin end: return {symbol: symbol, status: skip} df source.query(symbol, begin, end) df[symbol] symbol merge_write(RAW / f{symbol}.parquet, df, keys[date, symbol]) return {symbol: symbol, status: ok, rows: len(df)}注意那个回退 5 个自然日的处理。看着多余实际救命。数据源偶尔会补发修正数据如果你严格只拉新日期修正后的历史值永远进不来等到做归因的时候才发现对不上。回退几天重拉然后按主键去重覆盖成本几乎为零收益极大。幂等指的是同一天跑十次和跑一次结果完全一样。实现方式就是上面的merge_write按date symbol去重后覆盖写而不是无脑append。append是万恶之源我早期所有的数据重复问题都源于它。每个分区写完顺手落一个_manifest.jsonimport json, hashlib, datetime def dump_manifest(path, df, params): meta { rows: len(df), cols: list(df.columns), start: str(df[date].min().date()), end: str(df[date].max().date()), params_hash: hashlib.md5(json.dumps(params, sort_keysTrue).encode()).hexdigest()[:10], built_at: datetime.datetime.now().isoformat(timespecseconds), } path.with_suffix(.manifest.json).write_text(json.dumps(meta, ensure_asciiFalse, indent2))后面排查这个数是什么时候算的、用的什么参数全靠它。注意采集 Skill 里绝对不要做任何计算。不做复权、不算收益率、不做填充原样落盘。原始层一旦掺了加工逻辑你就永远回不到干净状态了。3.2 公告与财报解析 Skill把非结构化文本变成结构化表02-filing-parser是唯一一个真正需要大模型深度参与的采集类 Skill因为财报和公告是纯文本规则解析覆盖率撑死七成。但这个 Skill 的设计有讲究模型只负责抽不负责算。具体做法是两段式。第一段用模型把文本里的关键字段抽成 JSON比如营业收入归母净利润同比增速报告期第二段用脚本做单位归一和校验比如模型抽出来的是1,234,567.89 万元脚本统一转成元并且跟上一期做交叉验证如果单季度营收环比暴涨 500% 就打个可疑标记。# 抽取结果的结构化契约 { symbol: 600XXX, report_period: 2024-12-31, revenue_yuan: 12345678900.0, net_profit_yuan: 1234567890.0, unit_source: 万元, confidence: high, raw_span: 本报告期实现营业收入1,234,567.89万元 }保留raw_span是必须的。出了争议直接定位到原文那一句比什么解释都有力。confidence字段也让下游可以做筛选低置信度的数据不进因子计算。这个 Skill 我踩过最大的坑是报告期口径。有的公告写2024年度有的写2024年第四季度有的写2024年1-12月指的是同一个区间。如果不做统一映射同一个字段会出现三种 period聚合的时候直接算重。我的处理是建一张口径映射表所有输入的 report_period 强制过一遍映射再入库。3.3 数据体检 Skill让它替你做那个最烦的检查03-data-doctor是我最喜欢的一个 Skill因为它把我最讨厌的重复劳动彻底消灭了。它做的事情就一件把你能想到的所有数据质量问题全部写成自动化断言。我实际在用的检查项检查项判定规则处理动作主键重复datesymbol 唯一性直接报错阻断交易日缺失与全局交易日历比对警告并列出日期价格异常close0 或日涨跌幅绝对值11%标记待人工确认复权断裂相邻两日收益跳变30%标记疑似未复权字段缺失率单列缺失5%报警停牌未标记成交量0 但价格变动补充停牌标记时间戳错位数据日期当前交易日阻断单位漂移成交额中位数突变10倍报警第三条和第四条是精华。日涨跌幅绝对值超过 11% 且不是 ST 股几乎百分百是复权没对齐这个规则帮我抓到过三次数据源切换导致的复权基准变化。第四条更狠相邻两日收益跳变超过 30%基本可以确定是复权断裂或者除权日处理错误。体检的输出是一份 Markdown 加一份 CSV前者给人看后者给机器读。CSV 里每条问题带severity字段error直接阻断后续流水线warn只记录不阻断。这个分级很重要全部阻断会让你寸步难行全部放行等于没检查。提示体检规则要随着踩坑不断增加。我给自己定的规矩是每出现一次数据事故就往># 支持的算子ts_mean, ts_std, ts_delay, ts_delta, rank, zscore, corr OPS { ts_mean: lambda s, n: s.rolling(n).mean(), ts_std: lambda s, n: s.rolling(n).std(), ts_delay: lambda s, n: s.shift(n), ts_delta: lambda s, n: s.diff(n), rank: lambda s: s.rank(pctTrue), zscore: lambda s: (s - s.mean()) / s.std(), }一个动量反转因子的表达式就是-1 * rank(ts_delta(close, 20) / ts_delay(close, 20))。这里有个必须强调的纪律所有涉及当前 bar的操作一律强制shift(1)。我在引擎层做了一个开关默认开启auto_shift任何以close、volume结尾的原始字段在进入表达式前先整体后退一天。这一步拦住的是未来函数量化里最致命的错误。你可能觉得我肯定会注意但实测下来一百个因子里至少有三个会在某个算子的边界上不小心用到当日数据靠人眼查是查不出来的。标准化这块也有坑。截面 rank 和截面 zscore 结果差别很大rank 对极值不敏感、更稳健zscore 保留了数值信息但对离群值敏感。我的默认是 rank因为它在不同市场环境下表现更一致。另外标准化必须在截面上做不能在时间序列上做早期我把 zscore 写成时序的结果整个因子变成了另一种东西IC 看着还行但逻辑完全错了。4.2 因子检验 Skill一次跑完该看的全部指标05-factor-test是批量化的典范。输入一堆因子文件输出一张体检表。核心指标和我的判断阈值指标计算方式我的参考阈值说明RankIC 均值因子值与未来收益的截面秩相关绝对值 0.03低于这个基本没用ICIRIC 均值 / IC 标准差 0.3考察稳定性IC 胜率IC 同号占比 55%单边预测能力分层单调性十分组年化收益排序单调或近似单调非线性可接受多空年化首尾组收益差 8%未计成本组内换手率十分组平均换手 30%/月太高吃不掉成本因子自相关相邻期因子值相关 0.95过高说明换手低但信息冗余计算 IC 的时候有几个细节值得说。第一用 Spearman 秩相关而不是 Pearson因为因子值分布往往有厚尾Pearson 会被极值带偏。第二未来收益一定要用可交易价格用当日收盘算收益率等于偷看必须是close.shift(-1)到close.shift(-21)这种错位后的区间收益。第三每个截面的有效样本数要过滤某个交易日只有 20 只股票有数据算出来的 IC 没有意义我的阈值是单截面至少 100 只。换手率的计算很容易写错。正确的是先算组合持仓权重再算权重变化带来的成交额最后除以平均持仓市值。很多人直接用因子值的自相关当换手率代理两者在因子标准化方式不同的时候差异很大会导致成本估算严重失真。4.3 回测 Skill参数怎么定才算不骗自己06-backtest-runner是整套系统里最需要对自己诚实的地方。回测跑出来漂亮的曲线太容易了难的是让它接近真实。我把回测参数单独抽到config/global.yaml强制显式声明backtest: commission_rate: 0.00025 # 双边佣金示意值按你券商的真实费率填 stamp_tax_sell: 0.001 # 卖出印花税按当期规则核对后填写 transfer_fee: 0.00002 # 过户费 slippage_bps: 5 # 滑点按成交额万分之五 trade_price: vwap # 成交价用当日均价比收盘价保守 execution_delay: 1 # T1 执行 filter_limit_up: true # 涨停不买 filter_limit_down: true # 跌停不卖 filter_suspended: true # 停牌不交易 min_listing_days: 60 # 上市不足 60 日剔除 universe: all_a_ex_st # 股票池口径这些参数里滑点是弹性最大的一个。我最早设 2bp回测年化多出来四个点换成 5bp 之后曲线立刻难看但真实。判断标准是小市值、低流动性股票的成交冲击更大如果你做的是全市场选股5bp 是相对保守的起点可以再往上压力测试到 10bp 看策略还能不能活。execution_delay: 1这个设置拦住的是当天收盘信号当天收盘成交这种不现实的假设。实际执行至少要到下一个交易日尤其是指数成分调整或者月末调仓的时候。涨停过滤看起来是小事实际影响巨大。A 股涨停当天基本买不进去如果不做过滤动量类策略的回测收益会被夸大一倍以上。我见过有人把这个问题归结为策略很牛其实是回测的锅。回测的输出不只是净值曲线还包括逐日持仓、逐笔交易、换手率序列、分年度收益、最大回撤区间。特别是逐笔交易明细它是排查异常的唯一依据。看到某一天收益率突然跳了 5%翻成交记录大概率能发现是数据错误或者逻辑 bug。4.4 归因 Skill把收益拆到能解释为止07-pnl-attribution解决的问题是这个策略赚的钱到底从哪来。回测净值曲线好看不代表策略有效可能是踩中了某年的小盘风格也可能是某个行业的 beta。我的做法是两层归因叠加。第一层是因子暴露归因用 Barra 风格的因子做截面回归看组合在各风格因子上的暴露。具体做法是每个交易日把组合持仓对风格因子做加权回归得到暴露序列再乘以因子收益得到归因结果。这一步能回答我的超额收益里有多少来自市值暴露、多少来自动量暴露。第二层是Brinson 行业归因把收益拆成配置效应和选择效应。配置效应是我在哪些行业超配了选择效应是我在行业内选股准不准。这两个数一出来策略的性质立刻就清楚了——如果 90% 的收益来自配置效应那这大概率是个行业轮动策略跟选股能力关系不大。归因结果我用一个表呈现每个分组一行包含权重偏离、收益贡献、占比。这张表是后续报告里最有说服力的一块因为它能明确告诉你超额收益的来源是否可持续。注意归因要用同一套股票池和同一套收益率口径跟回测严格对齐。我早期归因用的是等权基准回测用的是市值加权基准两边的超额收益差了两个点白折腾了一周才发现是基准不一致。5. 决策层后 3 个 Skill 输出能看懂的结论决策层是这套系统最轻但最贵的一层。说它轻是因为计算量小说它贵是因为这里的错误代价最大——一个错误的组合权重或者一份口径混乱的报告会直接影响判断。5.1 组合优化 Skill约束条件别打架08-portfolio-opt用 cvxpy 做均值方差优化加交易成本惩罚。核心难点不在目标函数在约束条件。import cvxpy as cp w cp.Variable(n) ret mu w risk cp.quad_form(w, cov) turnover cp.norm1(w - w_prev) cost tc_rate * turnover objective cp.Maximize(ret - risk_aversion * risk - cost * cost_weight) constraints [ cp.sum(w) 1, w 0, # 不做空 w 0.05, # 单票上限 5% cp.norm1(w - w_prev) 0.30, # 单期换手上限 30% A_industry w industry_cap, # 行业偏离上限 ]实际用的时候最容易出问题的是约束不可行。单票上限 5%、持仓 30 只理论上限是 150%没问题但如果你同时要求某些行业必须满配、单票又限得很死可能就无解了。我的处理是给约束加优先级硬约束权重和、非负不可违反软约束行业偏离、换手失败时自动放宽 20% 并打日志。risk_aversion这个参数不要拍脑袋定。我的做法是跑一组网格看不同取值下的年化、波动、换手选一个换手在可接受范围内、夏普相对平稳的区间而不是选夏普最高的那个点。优化问题的最优点往往对参数极度敏感实际上不可交易这一点做组合优化的人基本都踩过。5.2 报告生成 Skill数字由脚本注入模型只写解读09-report-writer是我觉得最能体现算数和解释分离这个原则的地方。模板用 Jinja2所有数字都是变量从磁盘上的结果文件里读# skills/09-report-writer/scripts/render.py from jinja2 import Template import pandas as pd summary pd.read_csv(data/result/factor_test/summary.csv) bt pd.read_parquet(data/result/backtest/nav.parquet) ctx { date: bt.index[-1].strftime(%Y-%m-%d), ann_ret: f{bt[nav].pct_change().mean() * 252:.2%}, max_dd: f{compute_max_dd(bt[nav]):.2%}, top_factors: summary.nlargest(5, icir)[[name, rank_ic, icir]].to_dict(records), } md Template(open(templates/daily.md.j2).read()).render(**ctx)这样做的好处是报告里的每一个数字都能追溯到具体文件而且模型完全没法篡改。模型在这个 Skill 里的唯一作用是把top_factors这个表翻译成一段人话解读比如本期 ICIR 最高的三个因子分别是……其中 A 因子的分层单调性最好但换手率偏高实盘需注意成本。我给自己定了一条硬规矩报告里出现的任何数字必须能在结果文件里找到对应字段。哪怕是一句较上期提升 0.3 个百分点这个 0.3 也得是算出来的。这条规矩执行下来报告的信任度完全不一样了。图表部分我直接从 matplotlib 出图存 PNG模板里引用相对路径。不要指望模型画图它画出来的图要么数据对不上要么样式惨不忍睹。5.3 风险哨兵 Skill阈值监控加回流触发10-risk-sentinel是闭环的最后一环也是把批处理变成闭环的关键。它做的事情很朴素读结果比阈值超了就告警并触发回流。我的阈值表监控项阈值触发动作组合累计回撤 8%告警并生成归因任务单日波动率分位 95 分位告警因子 IC 连续 20 日转负累计 15 日以上触发 factor-test 重跑单票权重 8%阻断强制重新优化行业偏离 配置上限的 1.5 倍告警数据新鲜度最新日期落后 1 交易日阻断下游注意第四条和第一条的差别有些问题是阻断级有些是告警级。单票权重超限属于逻辑违规必须阻断回撤超阈值属于市场正常波动只需要提醒。这个分级如果做错要么天天被误报骚扰到麻木要么真出问题的时候毫无反应。数据新鲜度这一条特别容易被忽略但特别重要。如果上游采集挂了下游用的是三天前的数据回测和组合全是错的但你从结果上看不出来。加一条断言成本几行代码价值极高。回流触发我是这么实现的哨兵检测到因子 IC 异常后往data/queue/目录写一个任务文件run_all.py启动时会扫描这个队列自动补跑对应的 Skill。这样整个系统就有了自我修复的能力——不完美但足以覆盖 80% 的常见异常。6. 调度与串联让 10 个 Skill 真的跑成一条流水线单个 Skill 跑通不难难的是让它们稳定地串起来尤其是当你要让它每天自动跑的时候。6.1 主控脚本的写法与状态管理run_all.py我写得非常朴素核心就是一个步骤列表加循环STEPS [ (01-market-fetch, [--mode, incremental]), (02-filing-parser, [--lookback, 7]), (03-data-doctor, []), (04-factor-lab, [--expr-file, config/factors.yaml]), (05-factor-test, [--window, 252]), (06-backtest-runner, []), (07-pnl-attribution, []), (08-portfolio-opt, []), (09-report-writer, [--template, daily]), (10-risk-sentinel, []), ] def run(): for name, args in STEPS: rc subprocess.run([python, fskills/{name}/scripts/main.py, *args]).returncode if rc ! 0: alert(f{name} 失败退出码 {rc}) if BLOCKING.get(name, True): break else: log_warn(f{name} 非阻断失败继续)两个设计点用子进程而不是函数调用好处是每个 Skill 完全隔离一个崩了不影响进程状态出问题可以直接单独执行同一条命令复现。区分阻断和非阻断采集失败必须停归因失败可以继续出报告报告里标注归因缺失。6.2 定时、日志与幂等重跑定时我用系统自带的任务调度每天早上七点跑一次跑完把报告推到该去的地方。不引入复杂的调度框架因为需求真的太简单了Airflow 那套东西在单人项目里纯属负担。日志按天切分每个 Skill 一段格式统一成时间戳 | 等级 | Skill 名 | 消息。这个格式看着土但 grep 起来极其方便出问题时grep ERROR logs/2025-*.log一秒定位。幂等重跑是必须保证的。某天早上发现采集挂了修好之后重跑结果必须和正常情况一致不能出现重复数据或者双份计算。这靠的是前面说的merge_write去重覆盖。我给自己定了个测试习惯新加一个 Skill 之后连跑三次比对三次输出的哈希不一致就说明有幂等问题。6.3 参数化配置把可变的部分全部抽出来最后一条经验也是被无数人低估的一条凡是会变的东西一律不要写在代码里。我的config/global.yaml里放了交易日历来源、股票池定义、费率参数、因子列表和表达式、回测窗口、优化约束、告警阈值。改参数只改这一个文件不动任何一行代码。这么做的直接好处是可复现性。前面每个 Skill 落盘的_manifest.json里都记了params_hash想复现半年前的某次回测把当时的 global.yaml 从版本控制里取出来一键重跑结果分毫不差。没有参数外置这一切都做不到。7. 常见问题与排查技巧实录跑了这么久积累的问题清单比代码本身还长。挑几个最有代表性的说。7.1 问题速查表现象大概率原因排查入口回测收益异常高未来函数 / 成本假设过松检查 shift、滑点参数IC 突然归零因子值与收益错位比对索引对齐情况净值曲线出现跳变数据缺失或复权断裂跑>