
量化投研最尴尬的阶段往往是手里已经有了一堆数据、一堆因子脚本、一堆回测代码但每天开盘前还是靠人肉在十几个窗口之间来回切换先在终端拉一遍行情再翻财报看行业景气然后打开因子表手工排序最后凭感觉决定今天调不调仓。这套流程跑一次要两三个小时跑完之后你自己都不确定中间哪一步被昨天的缓存污染了。WorkBuddy 这类工作台出现之后真正值得花力气的地方不是让 AI 帮我选股而是把上面这条链路拆成职责清晰、可以单独验证、又能串起来的 Skill让每一步都有明确的输入输出契约。我这次拿金融量化做例子把 10 个 Skill 串成一个四级的投研闭环——从数据底座、宏观环境到行业轮动、标的多因子画像再到组合构建、回测归因和风控执行每一级都有自己的产物上一级的输出就是下一级的输入中间不靠聊天上下文传递全部落盘成 JSON 和 Parquet。这篇文章写给三类人刚接触 WorkBuddy 想搞明白 Skill 到底怎么用的人、有量化基础但被工程化绊住的人、以及想让团队投研流程可复现的人。文中所有代码和参数都可以直接抄替换数据源就能跑。1. 为什么要把量化投研拆成四级闭环和 10 个 Skill1.1 从一个大而全的提示词到分工明确的 Skill 流水线很多人第一次用 AI 做投研习惯写一个超长的提示词把帮我分析一下现在的市场、挑几个行业、选几只票、给个仓位建议全塞进去。我试过结果基本是灾难。原因不复杂单次对话里的上下文窗口有限模型在长链路推理中会逐渐丢失前面的约束条件到了最后一步给仓位建议时它早就忘了第二步里你自己定义的行业上限是 25%。更要命的是这种流程不可复现今天跑出来一个结果明天同样的提示词跑出来完全不一样你没法归因到底是数据变了还是模型抽风了。Skill 的思路完全不同。一个 Skill 就是一段被固化的能力它有明确的名字、明确的触发词、明确的输入参数、明确的输出格式还有明确的失败边界。模型在这个 Skill 内部的自由度被限制在怎么完成任务而不是要不要做这个任务。把 10 个这样的单元串起来整条链路的确定性就上来了——中间任何一环出错你能定位到具体的 Skill而不是对着一整段输出发懵。打个生活化的比方一整个大提示词像是让一个厨师从买菜、洗菜、切配到炒菜全包中途他要是忘了放盐你根本不知道是哪一步出的问题Skill 流水线更像是中央厨房切配间只负责切配出品必须是一盒标准化的净菜炒锅那边拿到净菜就能直接下锅谁的问题一目了然。1.2 四级闭环的分层逻辑宏观、行业、标的、组合我把这套体系分成四级不是为了好看而是因为这四个层级的决策频率和数据更新节奏完全不同混在一起做就一定会互相污染。一级是环境层回答现在这个市场该不该重仓。它依赖的是利率、汇率、信用利差、指数波动率、成交额结构这类周频甚至月频想起来看一次的数据更新慢、噪声大但决定了后面所有仓位系数的上限。二级是行业层回答钱该往哪些赛道倾斜。它依赖行业指数动量、景气度指标、估值分位、资金流向通常是周频调仓。三级是标的层回答在这个赛道里买哪几个。这部分是日频甚至更高频的因子有效性衰减快需要持续滚动检验。四级是组合与执行层回答具体买多少、什么时候调、跌到哪止损。这四级如果揉在一起最典型的后果就是你用一个日频因子去解释一个季度才变一次的宏观判断逻辑上根本对不齐。分开之后一级输出的风险预算系数会作为参数传给四级四级再乘上自己的仓位结果这样整条链路是可解释的。1.3 Skill 和 Agent 的边界什么时候该用哪个热词里经常有人问 skill 和 agent 的区别我自己的划分标准很粗暴流程固定的用 Skill需要临场判断的用 Agent。数据清洗、因子计算、IC 检验、回测、风控检查、报告渲染这些步骤的顺序和参数基本不会变属于典型的 Skill 场景写成脚本固化的收益最大。而今天这个异动到底是不是消息面驱动的要不要临时降仓这种问题需要模型去读新闻、读公告、做判断属于 Agent 场景。我的实际做法是Agent 做决策点Skill 做执行段。整条链路里只有两个地方留给 Agent一是在二级结束、三级开始之间让 Agent 看一眼有没有突发事件需要打断二是在四级输出之后让 Agent 审一遍报告有没有明显矛盾。其余八个环节全部是纯 Skill不接受协商。2. 10 个 Skill 的职责清单与输入输出契约2.1 十个 Skill 一览表先把全景摆出来后面每一级再展开细节。这张表是我自己工作台里真实在用的版本编号顺序就是执行顺序。编号Skill 名称所属层级核心职责关键输入关键输出1>--- name: factor-forge version: 1.2.0 level: 3 triggers: [挖因子, 因子检验, IC分析, 分层回测] inputs: - panel_path # 必需一级产出的面板数据路径 - universe # 必需标的池文件 - factor_spec # 必需因子定义 yaml - horizon # 可选默认 5预测期(交易日) outputs: - factor_panel # parquet日期 x 标的 x 因子 - ic_report # jsonIC/IR/胜率 - layered_curve # parquet分层回测净值 timeout: 900 --- # factor-forge ## 用途 根据 factor_spec 从面板数据中构造因子做横截面标准化与去极值 计算 Rank IC 与分层回测输出可直接进入 signal-composer 的因子面板。 ## 执行约定 1. 所有因子一律做行业中性化与市值中性化后再算 IC。 2. 缺失值处理采用行业内中位数填充不允许全表填充。 3. 若 IC 样本数少于 250 个交易日直接抛错退出不做静默降级。 ## 失败处理 - 面板数据缺失率 30%报错并提示重新运行>{ skill: factor-forge, version: 1.2.0, run_id: 20250114-093012-fg, inputs_hash: a3f5c9..., started_at: 2025-01-14T09:30:1208:00, elapsed_sec: 214.6, status: success, artifacts: { factor_panel: s3://local/factors/20250114/panel.parquet, ic_report: s3://local/factors/20250114/ic.json }, metrics: { ic_mean: 0.0432, ir: 0.61, ic_win_rate: 0.573 } }inputs_hash是个小细节但特别管用。把输入文件的内容摘要拼起来算个哈希下次运行前先比一下哈希没变就直接复用上次产物跳过计算。我这边跑日频链路的时候光这一条能省掉一半以上的算力。3. 一级实现数据底座与宏观环境识别3.1 Skill 1data-hub 把脏数据挡在门外>import pandas as pd import numpy as np def align_panel(price: pd.DataFrame, calendar: pd.DatetimeIndex) - pd.DataFrame: 把价格宽表对齐到统一交易日历停牌置 NaN 而非前向填充 px price.reindex(calendar) # 只做有限次数的前向填充用于处理数据源偶发缺失最多补 3 天 px px.ffill(limit3) # 标记长期停牌连续 20 个交易日无成交 halted px.isna().rolling(20).sum() 20 px px.mask(halted, np.nan) return pxffill(limit3)这个参数是我调了很久才定下来的。不限制的话一只票停牌半年价格被一路前向填充回测里看起来净值平滑得像条直线实际上根本卖不出去。限制成 3 天之后超过 3 天的缺失就保留 NaN下游因子计算会自动剔除干净很多。提示data-hub 的输出一定要版本化。我习惯按数据日期分目录存比如data/20250114/永不覆盖历史目录。这样回测某一天的信号时能精确拿到当天可见的数据杜绝未来函数。数据落地格式上我全部用 Parquet 而不是 CSV。原因有两个一是体积同样一份 5000 只标的、10 年日频的价格面板CSV 大概 300MBParquet 压缩后不到 40MB二是类型保持CSV 读回来所有列都是 object每次都要重新解析Parquet 直接带 schema。3.2 Skill 2macro-regime 用打分代替拍脑袋宏观环境判断最容易变成玄学我的做法是把它彻底量化成几个可以计算的维度每个维度打 0 到 100 分加权得到总分再映射成风险预算系数。这套方法不追求预测准确只追求状态识别及时——它能告诉我现在和三个月前比环境是变好了还是变差了这就够了。我用的四个维度维度具体指标打分方向权重流动性短端利率变化、信用利差利差收窄加分30%盈利趋势分析师上调/下调比例上调占比高加分25%风险偏好指数波动率、成交额分位波动率低加分25%估值水位全市场估值分位低估加分20%每个指标先算历史分位注意方向对齐比如信用利差是越低越好取1 - 分位加权求和得到 0 到 100 的总分然后按固定阈值切成四档80 分以上为进攻档风险预算系数 1.060 到 80 为中性档0.840 到 60 为防守档0.640 以下为收缩档0.4。这个系数会一路传到第四级的 risk-guard直接乘在最终仓位上。它的作用不是择时而是控制回撤的尾部风险——环境差的时候主动降仓虽然会错过一些反弹但长期看净值曲线的形状会舒服很多。需要强调的是阈值和权重必须用滚动窗口校准不能拍死。我的做法是每年年初用过去 5 年数据重新拟合一次权重的相对大小看哪个维度对当年回撤的解释力最强微调但不大改。改成全自动优化很容易过拟合反而坏事。3.3 实操把一级跑成一个可复用的缓存一级的两个 Skill 有一个共同特点数据量大但计算简单跑一次能用很久。所以我把它们做成按周更新 按需触发的模式。具体做法是加一个轻量的调度脚本每周一早上跑一次全量更新把结果写到以周为单位的目录下比如regime/2025W03/。后续所有日频任务都读这个周的目录不重新计算。如果盘中遇到突发情况手工触发一次增量更新只重算最新几个交易日。#!/bin/bash set -euo pipefail WEEK$(date %YW%V) OUT/data/regime/${WEEK} mkdir -p ${OUT} workbuddy run>def sector_score(mom: pd.Series, pros: pd.Series, freshness: pd.Series) - pd.Series: mom: 风险调整动量, pros: 景气变化, freshness: 数据新鲜度(天) m (mom - mom.mean()) / mom.std(ddof1) p (pros - pros.mean()) / pros.std(ddof1) raw 0.6 * m 0.4 * p # 数据超过 30 天未更新景气权重腰斩 stale freshness 30 p_adj p * 0.5 adj 0.6 * m 0.4 * p_adj return raw.where(~stale, adj)最后输出的是一张行业得分表配合一个硬约束单一行业权重上限 25%前五大行业合计不超过 70%。这个约束不是风控拍的是回测跑出来的——放宽到 35% 之后组合波动率上升 40% 但收益只多了 8%性价比明显不划算。4.2 Skill 4chain-map 让赛道和标的真正挂钩产业链映射听起来虚但它解决一个很实际的问题二级选出来的是行业三级要选的是个股中间需要一个可靠的桥。我的做法是维护一份三层结构的映射表一级行业、二级细分、三级关键环节每个环节下面挂具体的标的池。这份表不是自动生成的是人工维护加定期校验。自动生成的问题在于很多公司的业务标签和实际收入结构严重不符——一家做传统制造的公司因为参股了一家芯片企业就被打上半导体标签这种噪声会直接毁掉下游的因子检验。维护流程大概是每季度跑一次收入结构校验把主营业务收入占比低于 50% 的标签标为弱关联在下游打分时降低权重同时监控新上市标的人工确认归属。这份表更新完之后chain-map 会输出一份行业到标的的映射 JSON供三级使用。{ sector: 电力设备, sub_sector: 储能, segments: [ {name: 电芯, weight: 0.45, symbols: [...]}, {name: PCS, weight: 0.30, symbols: [...]}, {name: 系统集成, weight: 0.25, symbols: [...]} ], updated_at: 2025-01-10 }有了这张表三级在做因子中性化的时候就能按细分环节而不是按大行业来做中性化的精度会明显提高。4.3 Skill 5valuation-band 估值分位要分场景看估值分位这个东西直接看 PE 历史分位是最容易出错的做法。原因很简单不同行业的估值中枢会随时间迁移一家公司从成长期进入成熟期PE 中枢下移是正常的你把它当成低估去买就是掉进了价值陷阱。所以 valuation-band 里我做了三件事。第一是滚动窗口用过去 5 年而不是全部历史避免把十年前的估值体系带进来。第二是行业内相对分位先看这家公司在同细分环节里排第几再看绝对分位。第三是结合盈利趋势做交叉判断如果估值分位低于 30% 但同时盈利预期在下调标记为低估值陷阱如果估值分位在 60% 到 80% 之间但盈利在上调标记为合理偏贵但趋势向好。这套交叉判断输出的不是一个数字而是一个带标签的结构。这些标签在三级做因子合成时能当哑变量用效果比单纯的估值分位好不少。注意估值分位对金融类、周期类公司的解释力差别很大。周期股在盈利高点时 PE 往往很低看着便宜其实最贵。我在 valuation-band 里对周期行业单独用了 PB 分位加产能利用率来做不走 PE 通道。5. 三级实现标的多因子画像与信号合成5.1 Skill 6factor-forge 因子有效性必须自己验到了三级才真正进入因子的世界。我给 factor-forge 定了一条铁律任何新因子必须通过三项检验才能进池子——IC 检验、分层回测、相关性检查。IC 检验用的是 Rank IC也就是横截面上的秩相关。为什么用秩相关而不是皮尔逊相关因为因子的绝对数值往往有极端值皮尔逊相关会被少数异常点带偏秩相关看的是排序更符合实际选股逻辑。import numpy as np import pandas as pd def rank_ic(factor: pd.DataFrame, fwd_ret: pd.DataFrame) - pd.Series: factor 与 fwd_ret 均为 日期 x 标的 的宽表返回逐日 Rank IC ic factor.corrwith(fwd_ret, axis1, methodspearman) return ic.dropna() def ic_summary(ic: pd.Series) - dict: ir ic.mean() / ic.std(ddof1) return { ic_mean: round(float(ic.mean()), 4), ic_std: round(float(ic.std(ddof1)), 4), ir: round(float(ir), 4), ic_win_rate: round(float((ic 0).mean()), 4), samples: int(ic.shape[0]), }我这边因子入池的门槛是IC 均值绝对值大于 0.02IR 大于 0.3IC 胜率大于 0.52样本数至少 500 个交易日。这三个条件同时满足的因子其实不多但一旦满足在组合里的稳定性明显更好。分层回测是第二道关。把因子值排序分成 5 组看第 1 组和第 5 组的净值差。这里最容易犯的错是只看多空收益不看单调性。有些因子多空收益很漂亮但中间三组乱序跳这种因子本质上是靠极端组撑起来的实盘里很容易失效。我会额外算一个单调性指标用组序和组均收益的秩相关来表示低于 0.7 就标黄。第三道关是相关性检查。新因子和已有因子的相关系数超过 0.85就基本没有增量价值直接拒绝0.7 到 0.85 之间标为弱增量只在已有因子失效时启用。实操心得因子计算一定要做行业和市值中性化但中性化的方式有讲究。用回归取残差最干净但慢用行业内做 z-score 快但会损失市值信息。我的折中做法是先按市值分组大中小三组做组内 z-score再按细分环节做去均值。这套组合下来单次因子计算从 6 分钟压到 90 秒效果几乎无损。5.2 Skill 7signal-composer 加权还是等权因子池有了之后合成方式直接决定最终信号质量。我试过三种等权、IC 加权、最大化 ICIR 优化。等权最稳最大问题是没办法体现因子之间的强弱差异。IC 加权简单直接用滚动 60 日的 IC 均值做权重效果比等权好一些。优化法理论上最优但实测下来最容易过拟合样本内漂亮样本外崩塌。我最后用的是IC 加权 权重上限的方案权重按滚动 IC 均值分配但单个因子权重上限 25%下限 5%同时限制同一大类因子比如动量类、估值类合计不超过 40%。上限约束牺牲了一点样本内表现换来的是样本外的稳定性。def compose_weights(ic_hist: pd.DataFrame, caps: dict, floor0.05, cap0.25) - pd.Series: ic_hist: 日期 x 因子 的 IC 历史caps: 大类因子映射 raw ic_hist.tail(60).mean().abs() w raw / raw.sum() w w.clip(lowerfloor) w w / w.sum() # 大类约束超额部分按比例回撤 for _, members in caps.items(): sub w[members] if sub.sum() 0.40: w[members] sub * (0.40 / sub.sum()) w w / w.sum() return w合成之后的打分表会落到三级输出目录字段包括标的代码、综合打分、分项得分、所属细分环节、估值标签。这张表就是第四级唯一的输入非常干净。5.3 参数计算IC、IR、换手率怎么联动看单看 IC 是不够的必须把 IC、IR 和换手率放在一起看。这三个指标构成了一个三角约束任何两个好了第三个一定会变差。举个我实际遇到的例子。有一个反转类因子IC 均值 0.055IR 0.72看着非常优秀。但一算换手率在每个调仓周期都是 80% 以上。按双边千分之三的成本估一年 50 次调仓光成本就吃掉 24% 的收益再高的 IC 也白搭。后来我的做法是在 factor-forge 里强制输出一个成本后 IC用 IC 均值乘以预测期收益的平均绝对幅度再减掉预期换手成本。这个指标不那么好看但更接近实盘。因子类型IC 均值IR单期换手成本后评价中期动量0.0410.5825%可用主力因子短期反转0.0550.7282%降权使用盈利质量0.0320.4512%可用低换手底仓估值分位0.0190.218%仅作辅助这张表是我自己因子池里的真实分布可以看到估值类因子的 IC 其实很弱但换手极低所以它的角色不是贡献收益而是稳定组合、压低回撤。理解每个因子的性格比一味追求高 IC 重要得多。6. 四级实现组合、回测与风控执行6.1 Skill 8backtest-lab 回测要防止自欺欺人回测最容易自欺欺人的地方有三个未来函数、幸存者偏差、成本低估。backtest-lab 里我把这三个都做成了硬校验。未来函数校验的方式是在每次取数据时强制加一个as_of时间戳所有查询只能看到该时间戳之前的数据。这个约束写在 Skill 的入口处不允许绕过。财务数据尤其要注意报告期和公告日不是一回事我统一用公告日对齐而不是报告期末。幸存者偏差靠保留退市样本来解决。标的池是每个时点当时存在的全部标的而不是今天的全部标的。这一条听起来简单但很多现成的回测框架默认就是后者跑出来的收益会虚高一大截。成本模型我用的是分段计算固定佣金、印花税、按成交额万分之几的冲击成本再加一个滑点估计。冲击成本这块小资金可以直接忽略但资金量上去之后必须建模我用的是一个简化的平方根模型成交额占比每提高 1%冲击成本增加约 0.3 个基点。def trade_cost(delta_w: pd.Series, adv: pd.Series, nav: float, fee0.0003, tax0.001, impact_k0.003) - float: delta_w: 权重变化, adv: 日均成交额, nav: 组合规模 turnover delta_w.abs() notional turnover * nav participation (notional / adv.replace(0, np.nan)).fillna(0) impact impact_k * np.sqrt(participation) cost notional * (fee impact) # 卖出部分加征印花税按实际规则调整 cost notional[delta_w 0].sum() * tax return float(cost.sum())归因部分我拆成三层资产配置贡献、行业选择贡献、个股选择贡献。这样每次回测完能看清楚收益到底来自哪一层如果收益主要来自行业配置那说明三级选股环节还有很大优化空间。6.2 Skill 9risk-guard 仓位不是拍出来的risk-guard 接收的是三级打分表加二级的行业上限输出的是最终下单权重。它要过四道检查任何一道不通过都会强制调整。第一道是风险预算折算。把一级给的宏观系数直接乘在目标仓位上。环境好时满仓环境差时四成仓。第二道是波动率目标。这是我个人认为最重要的一条。先算候选组合的预测波动率如果超过目标波动率就按比例缩减总仓位。目标波动率我定的是年化 15%超过就先减总仓而不是去砍个股因为砍个股容易破坏分散度。def vol_target_scale(w: pd.Series, vol: pd.Series, target0.15) - float: w: 相对权重, vol: 预测年化波动率, 返回总仓位缩放系数 port_vol float(np.sqrt((w.values ** 2 * vol.reindex(w.index).values ** 2).sum() 0.0)) # 简化处理忽略相关性项 if port_vol target: return 1.0 return float(target / port_vol)这段代码里我忽略了相关性项看起来是偷懒实际上是有意的。用历史相关矩阵算出来的组合波动率在极端行情下严重低估风险因为危机时相关性会飙到接近 1。忽略相关性等于做了一个保守假设宁可低估仓位也不要高估分散效果。第三道是单票与行业暴露检查。单票上限 5%单一细分环节上限 15%单一行业上限 25%。超限部分按比例分摊到未超限标的上。第四道是流动性检查。单日买入金额不超过该标的 20 日平均成交额的 10%超了就按比例缩减。这条对小资金没影响但一旦规模上去不加上这条会直接把回测收益打回原形。提示四道检查的先后顺序不能乱。风险预算必须在波动率目标之前否则会出现宏观环境很差但波动率低所以满仓这种荒谬结果。我的顺序永远是宏观系数 → 行业约束 → 个股权重 → 波动率目标 → 流动性检查。6.3 Skill 10report-weaver 让报告自己生成最后一个是输出环节。报告这东西看起来是体力活但它恰恰是验证整条链路有没有跑通的最佳方式——如果报告里某个数字是空的或者明显异常说明前面某一级出了问题。report-weaver 的输入是整条链路的所有_meta.json和关键产物路径。它生成的内容包括当日宏观状态与系数、行业得分前五与后五、组合持仓明细与变动、回测的滚动绩效指标、风控检查结果。格式是 Markdown同时输出一份 PDF 存档。我特意在报告里加了一个异常提示区块。规则大概是如果某个 Skill 的运行时间超过历史中位数的 3 倍、或者输出文件大小变化超过 50%、或者关键指标超过 3 个标准差就在报告顶部标红。这个设计帮我抓到过好几次数据源的静默故障——数据接口改了字段名返回的全是 null但流程一路跑到底只有这个指标突变提示暴露了问题。报告的渲染我用的模板加变量替换不依赖模型生成。原因很直接数字类的内容模型有概率把小数位搞错或者张冠李戴用模板更安全。模型只负责写最后的今日观察那一段自然语言而且要求它只能引用报告里已经存在的数字。7. 串联实操一条指令跑通全链路7.1 编排器怎么写10 个 Skill 串起来的方式有两种一种是用工作流引擎按依赖关系编排一种是写一个简单的编排脚本顺序调用。我选后者因为依赖关系足够清晰引入引擎反而增加调试成本。编排脚本的核心是依赖声明和产物检查。每个步骤执行前先检查产物是否存在且新鲜存在就跳过。import subprocess, json, pathlib from datetime import datetime STEPS [ (data-hub, [--config, configs/sources.yaml]), (macro-regime, []), (sector-rotation,[]), (chain-map, []), (valuation-band, []), (factor-forge, []), (signal-composer,[]), (backtest-lab, [--rebalance, 5]), (risk-guard, [--target-vol, 0.15]), (report-weaver, []), ] def fresh(p: pathlib.Path, max_age_h: int 20) - bool: if not p.exists(): return False age (datetime.now() - datetime.fromtimestamp(p.stat().st_mtime)).total_seconds() / 3600 return age max_age_h def run_pipeline(run_dir: str): base pathlib.Path(run_dir) for name, extra in STEPS: marker base / name / _meta.json if fresh(marker): print(f[skip] {name} artifact is fresh) continue cmd [workbuddy, run, name, --out, str(base / name)] extra r subprocess.run(cmd, capture_outputTrue, textTrue, timeout1800) if r.returncode ! 0: print(f[fail] {name}\n{r.stderr[-2000:]}) raise SystemExit(1) print(f[ok] {name}) if __name__ __main__: run_pipeline(fruns/{datetime.now():%Y%m%d})这个脚本才 40 行但它覆盖了整条链路。fresh函数的时间窗我设的是 20 小时因为日频数据一天更新一次20 小时内复用是安全的超过就应该重算。7.2 中断恢复与幂等设计长链路跑一半挂掉是常态。我的应对方式是三步第一步是产物目录隔离每次运行一个run_id所有产物写在自己的目录下互不干扰。第二步是原子写入产物先写临时文件再os.replace重命名避免出现半截文件被下游读走。第三步是断点续跑因为每个步骤都有自己的_meta.json重跑时直接从失败的步骤开始前面的自动跳过。幂等性上有个细节值得说factor-forge 这类计算密集型 Skill我会把随机种子固定下来写进_meta.json。同样的输入加同样的种子必须得到完全一样的输出。有一次我发现两次跑同一个因子结果差了一点点查了半天是中性化那步用了np.random做采样种子没固定修好之后这类问题就再没出现过。7.3 运行一次的真实记录说点具体的。有一次我跑了一个完整链路时间线大致是这样data-hub 拉取 5000 只标的、10 年日频行情和财务数据耗时 6 分 40 秒macro-regime 计算四个维度打分8 秒sector-rotation 处理 30 个行业12 秒chain-map 走的是查表3 秒valuation-band 需要计算滚动分位55 秒factor-forge 最重5 个因子加 IC 加分层回测3 分 48 秒signal-composer 18 秒backtest-lab 跑 5 日调仓的 10 年回测1 分 52 秒risk-guard 因为要模拟流动性约束26 秒report-weaver 渲染加 PDF14 秒。总计大约 13 分钟出头。这个时间比手工操作快多少我手工跑一遍同样的流程光是数据对齐和因子表整理就得一个半小时还不算中间出错的返工。而且手工跑完之后你手里只有一份结果要改参数得从头再来串成链路之后改一个参数只重跑受影响的那一段比如只换因子权重直接从 signal-composer 开始跑40 秒就出结果。8. 常见问题与排查速查表8.1 问题速查表这套东西我用了小半年踩过的坑基本都在下面这张表里。现象最可能的原因排查方式解决方式下游读到空数据上游写出半截文件检查产物目录有无.tmp残留改成原子写入os.replace因子 IC 忽然归零数据源字段变更返回 null对比_meta.json的输出行数加字段校验行数波动超 10% 直接报错回测收益异常高存在未来函数检查是否用报告期而非公告日全链路强制as_of时间戳链路卡住不报错缺超时设置看进程状态和 CPU 占用每个 Skill 设timeout超时抛错两次结果不一致随机种子未固定比对_meta.json的输入哈希固定种子并写入元数据报告数字对不上模板变量名拼写错误用 schema 校验渲染前数据模板加必填字段断言组合波动超预期忽略相关性项的低估检查持仓集中度加行业和环节上限约束8.2 数据源与算力的坑数据源的坑最隐蔽也最值得提前防。我总结下来有三类一是接口静默变更字段名改了但接口不报错返回一堆 null二是历史数据修正财务数据经常被追溯调整导致昨天的回测今天跑不出同样结果三是限流短时间内大量请求被限速返回的数据不完整但格式正确。对应的防御措施我在>