news 2026/9/18 8:14:39

WorkBuddy 量化投研:10 个 Skill 搭建四级闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WorkBuddy 量化投研:10 个 Skill 搭建四级闭环

量化投研最尴尬的阶段,往往是手里已经有了一堆数据、一堆因子脚本、一堆回测代码,但每天开盘前还是靠人肉在十几个窗口之间来回切换:先在终端拉一遍行情,再翻财报看行业景气,然后打开因子表手工排序,最后凭感觉决定今天调不调仓。这套流程跑一次要两三个小时,跑完之后你自己都不确定中间哪一步被昨天的缓存污染了。WorkBuddy 这类工作台出现之后,真正值得花力气的地方不是"让 AI 帮我选股",而是把上面这条链路拆成职责清晰、可以单独验证、又能串起来的 Skill,让每一步都有明确的输入输出契约。我这次拿金融量化做例子,把 10 个 Skill 串成一个四级的投研闭环——从数据底座、宏观环境,到行业轮动、标的多因子画像,再到组合构建、回测归因和风控执行,每一级都有自己的产物,上一级的输出就是下一级的输入,中间不靠"聊天上下文"传递,全部落盘成 JSON 和 Parquet。这篇文章写给三类人:刚接触 WorkBuddy 想搞明白 Skill 到底怎么用的人、有量化基础但被工程化绊住的人、以及想让团队投研流程可复现的人。文中所有代码和参数都可以直接抄,替换数据源就能跑。

1. 为什么要把量化投研拆成"四级闭环"和 10 个 Skill

1.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 # json,IC/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:12+08: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 1:data-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(limit=3) # 标记长期停牌:连续 20 个交易日无成交 halted = px.isna().rolling(20).sum() >= 20 px = px.mask(halted, np.nan) return px

ffill(limit=3)这个参数是我调了很久才定下来的。不限制的话,一只票停牌半年,价格被一路前向填充,回测里看起来净值平滑得像条直线,实际上根本卖不出去。限制成 3 天之后,超过 3 天的缺失就保留 NaN,下游因子计算会自动剔除,干净很多。

提示:data-hub 的输出一定要版本化。我习惯按数据日期分目录存,比如data/20250114/,永不覆盖历史目录。这样回测某一天的信号时,能精确拿到当天可见的数据,杜绝未来函数。

数据落地格式上,我全部用 Parquet 而不是 CSV。原因有两个:一是体积,同样一份 5000 只标的、10 年日频的价格面板,CSV 大概 300MB,Parquet 压缩后不到 40MB;二是类型保持,CSV 读回来所有列都是 object,每次都要重新解析,Parquet 直接带 schema。

3.2 Skill 2:macro-regime 用打分代替拍脑袋

宏观环境判断最容易变成玄学,我的做法是把它彻底量化成几个可以计算的维度,每个维度打 0 到 100 分,加权得到总分,再映射成风险预算系数。这套方法不追求预测准确,只追求状态识别及时——它能告诉我"现在和三个月前比,环境是变好了还是变差了",这就够了。

我用的四个维度:

维度具体指标打分方向权重
流动性短端利率变化、信用利差利差收窄加分30%
盈利趋势分析师上调/下调比例上调占比高加分25%
风险偏好指数波动率、成交额分位波动率低加分25%
估值水位全市场估值分位低估加分20%

每个指标先算历史分位,注意方向对齐(比如信用利差是越低越好,取1 - 分位),加权求和得到 0 到 100 的总分,然后按固定阈值切成四档:80 分以上为进攻档,风险预算系数 1.0;60 到 80 为中性档,0.8;40 到 60 为防守档,0.6;40 以下为收缩档,0.4。

这个系数会一路传到第四级的 risk-guard,直接乘在最终仓位上。它的作用不是择时,而是控制回撤的尾部风险——环境差的时候主动降仓,虽然会错过一些反弹,但长期看净值曲线的形状会舒服很多。

需要强调的是,阈值和权重必须用滚动窗口校准,不能拍死。我的做法是每年年初用过去 5 年数据重新拟合一次权重的相对大小,看哪个维度对当年回撤的解释力最强,微调但不大改。改成全自动优化很容易过拟合,反而坏事。

3.3 实操:把一级跑成一个可复用的缓存

一级的两个 Skill 有一个共同特点:数据量大但计算简单,跑一次能用很久。所以我把它们做成"按周更新 + 按需触发"的模式。

具体做法是加一个轻量的调度脚本,每周一早上跑一次全量更新,把结果写到以周为单位的目录下,比如regime/2025W03/。后续所有日频任务都读这个周的目录,不重新计算。如果盘中遇到突发情况,手工触发一次增量更新,只重算最新几个交易日。

#!/bin/bash set -euo pipefail WEEK=$(date +%Y"W"%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(ddof=1) p = (pros - pros.mean()) / pros.std(ddof=1) 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 4:chain-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 5:valuation-band 估值分位要分场景看

估值分位这个东西,直接看 PE 历史分位是最容易出错的做法。原因很简单:不同行业的估值中枢会随时间迁移,一家公司从成长期进入成熟期,PE 中枢下移是正常的,你把它当成"低估"去买,就是掉进了价值陷阱。

所以 valuation-band 里我做了三件事。第一是滚动窗口,用过去 5 年而不是全部历史,避免把十年前的估值体系带进来。第二是行业内相对分位,先看这家公司在同细分环节里排第几,再看绝对分位。第三是结合盈利趋势做交叉判断:如果估值分位低于 30% 但同时盈利预期在下调,标记为"低估值陷阱";如果估值分位在 60% 到 80% 之间但盈利在上调,标记为"合理偏贵但趋势向好"。

这套交叉判断输出的不是一个数字,而是一个带标签的结构。这些标签在三级做因子合成时能当哑变量用,效果比单纯的估值分位好不少。

注意:估值分位对金融类、周期类公司的解释力差别很大。周期股在盈利高点时 PE 往往很低,看着便宜其实最贵。我在 valuation-band 里对周期行业单独用了 PB 分位加产能利用率来做,不走 PE 通道。

5. 三级实现:标的多因子画像与信号合成

5.1 Skill 6:factor-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, axis=1, method="spearman") return ic.dropna() def ic_summary(ic: pd.Series) -> dict: ir = ic.mean() / ic.std(ddof=1) return { "ic_mean": round(float(ic.mean()), 4), "ic_std": round(float(ic.std(ddof=1)), 4), "ir": round(float(ir), 4), "ic_win_rate": round(float((ic > 0).mean()), 4), "samples": int(ic.shape[0]), }

我这边因子入池的门槛是:IC 均值绝对值大于 0.02,IR 大于 0.3,IC 胜率大于 0.52,样本数至少 500 个交易日。这三个条件同时满足的因子其实不多,但一旦满足,在组合里的稳定性明显更好。

分层回测是第二道关。把因子值排序分成 5 组,看第 1 组和第 5 组的净值差。这里最容易犯的错是只看多空收益,不看单调性。有些因子多空收益很漂亮,但中间三组乱序跳,这种因子本质上是靠极端组撑起来的,实盘里很容易失效。我会额外算一个单调性指标,用组序和组均收益的秩相关来表示,低于 0.7 就标黄。

第三道关是相关性检查。新因子和已有因子的相关系数超过 0.85,就基本没有增量价值,直接拒绝;0.7 到 0.85 之间标为"弱增量",只在已有因子失效时启用。

实操心得:因子计算一定要做行业和市值中性化,但中性化的方式有讲究。用回归取残差最干净但慢,用行业内做 z-score 快但会损失市值信息。我的折中做法是:先按市值分组(大中小三组)做组内 z-score,再按细分环节做去均值。这套组合下来,单次因子计算从 6 分钟压到 90 秒,效果几乎无损。

5.2 Skill 7:signal-composer 加权还是等权

因子池有了之后,合成方式直接决定最终信号质量。我试过三种:等权、IC 加权、最大化 ICIR 优化。

等权最稳,最大问题是没办法体现因子之间的强弱差异。IC 加权简单直接,用滚动 60 日的 IC 均值做权重,效果比等权好一些。优化法理论上最优,但实测下来最容易过拟合,样本内漂亮样本外崩塌。

我最后用的是IC 加权 + 权重上限的方案:权重按滚动 IC 均值分配,但单个因子权重上限 25%,下限 5%,同时限制同一大类因子(比如动量类、估值类)合计不超过 40%。上限约束牺牲了一点样本内表现,换来的是样本外的稳定性。

def compose_weights(ic_hist: pd.DataFrame, caps: dict, floor=0.05, cap=0.25) -> pd.Series: """ic_hist: 日期 x 因子 的 IC 历史,caps: 大类因子映射""" raw = ic_hist.tail(60).mean().abs() w = raw / raw.sum() w = w.clip(lower=floor) 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.055,IR 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 8:backtest-lab 回测要防止自欺欺人

回测最容易自欺欺人的地方有三个:未来函数、幸存者偏差、成本低估。backtest-lab 里我把这三个都做成了硬校验。

未来函数校验的方式是在每次取数据时强制加一个as_of时间戳,所有查询只能看到该时间戳之前的数据。这个约束写在 Skill 的入口处,不允许绕过。财务数据尤其要注意,报告期和公告日不是一回事,我统一用公告日对齐,而不是报告期末。

幸存者偏差靠保留退市样本来解决。标的池是"每个时点当时存在的全部标的",而不是"今天的全部标的"。这一条听起来简单,但很多现成的回测框架默认就是后者,跑出来的收益会虚高一大截。

成本模型我用的是分段计算:固定佣金、印花税、按成交额万分之几的冲击成本,再加一个滑点估计。冲击成本这块,小资金可以直接忽略,但资金量上去之后必须建模,我用的是一个简化的平方根模型,成交额占比每提高 1%,冲击成本增加约 0.3 个基点。

def trade_cost(delta_w: pd.Series, adv: pd.Series, nav: float, fee=0.0003, tax=0.001, impact_k=0.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 9:risk-guard 仓位不是拍出来的

risk-guard 接收的是三级打分表加二级的行业上限,输出的是最终下单权重。它要过四道检查,任何一道不通过都会强制调整。

第一道是风险预算折算。把一级给的宏观系数直接乘在目标仓位上。环境好时满仓,环境差时四成仓。

第二道是波动率目标。这是我个人认为最重要的一条。先算候选组合的预测波动率,如果超过目标波动率,就按比例缩减总仓位。目标波动率我定的是年化 15%,超过就先减总仓,而不是去砍个股,因为砍个股容易破坏分散度。

def vol_target_scale(w: pd.Series, vol: pd.Series, target=0.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 10:report-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_output=True, text=True, timeout=1800) if r.returncode != 0: print(f"[fail] {name}\n{r.stderr[-2000:]}") raise SystemExit(1) print(f"[ok] {name}") if __name__ == "__main__": run_pipeline(f"runs/{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 渲染加 PDF,14 秒。总计大约 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;二是历史数据修正,财务数据经常被追溯调整,导致昨天的回测今天跑不出同样结果;三是限流,短时间内大量请求被限速,返回的数据不完整但格式正确。

对应的防御措施我在>

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 8:14:30

机器学习在乳腺癌诊断中的应用与优化

1. 项目背景与核心价值乳腺癌是全球女性最常见的恶性肿瘤之一&#xff0c;早期准确诊断对治疗方案选择和预后改善至关重要。传统病理诊断依赖医生经验&#xff0c;存在主观性强、效率低下的痛点。这个项目通过机器学习方法构建自动化分类模型&#xff0c;将乳腺肿瘤影像特征转化…

作者头像 李华
网站建设 2026/9/18 8:09:27

Ubuntu U盘挂载与卸载实战:设备识别、权限与fstab避坑

1. 先把设备认明白&#xff1a;Ubuntu眼里的U盘到底是谁在Ubuntu下折腾U盘&#xff0c;十次里有八次卡在第一步——不知道U盘是哪个设备。这个坎看着小&#xff0c;实际是后面所有挂载、卸载、写fstab操作的地基。地基歪了&#xff0c;轻则报个错&#xff0c;重则把系统盘上的分…

作者头像 李华
网站建设 2026/9/18 8:07:18

QDPSK通信系统MATLAB仿真:差分相位调制与误码率分析

简介&#xff1a;一份基于MATLAB的QDPSK通信系统仿真文档&#xff0c;主要面向电子信息类、通信工程等专业的学生与工程师&#xff0c;用于理解四相相对移相键控&#xff08;QDPSK&#xff09;的原理及Simulink建模仿真方法。文档基于“信号与系统”课程教学场景&#xff0c;阐…

作者头像 李华
网站建设 2026/9/18 8:03:55

SpringBoot+Vue构建适老化在线教育平台实践

1. 项目背景与核心价值随着人口老龄化进程加速&#xff0c;老年教育需求呈现爆发式增长。传统线下老年大学普遍面临场地有限、课程单一、时间固定等问题&#xff0c;而多数在线教育平台又存在操作复杂、界面不友好等适老化不足的缺陷。我在实际调研中发现&#xff0c;65岁以上老…

作者头像 李华
网站建设 2026/9/18 8:03:28

前端必学Docker:从镜像容器到云服务器部署实战

1. 先想清楚&#xff1a;前端为什么要碰 Docker说句实话&#xff0c;大多数前端开发者听到 Docker 的第一反应都是“这是运维的事&#xff0c;跟我没关系”&#xff0c;我当年也是这么想的。直到有一次&#xff0c;我把一个 Vue 项目部署到测试服务器&#xff0c;本地跑得好好的…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.