一句话版本
让大模型 Agent 自动挖因子很有吸引力:它能读论文、提出假设、写代码、跑回测,一夜之间尝试上千个想法。但这也把量化研究最古老的问题放大了一千倍:试得越多,越容易在噪声里找到"显著"的结果(见《多重检验与因子动物园》)。 本文用一个模拟实验说明:在 2000 个候选因子中只藏了一个真因子,按常规的 t > 2 标准,会有 60 个候选"通过",其中 59 个是假的。解决办法不是让 Agent 少尝试,而是建立不可绕过的防线:锁起来的样本外"保险箱"、按尝试次数提高的门槛、经济逻辑审查、以及完整的尝试记录。
免责
本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据。
第一层:通俗理解
1.1 一个不知疲倦的研究员
想象你雇了一个研究员,他:
- 不需要睡觉,一晚上能测试 2000 个想法;
- 很会写报告,每个"发现"都配有流畅的解释;
- 不会主动告诉你他一共试了多少次。
第二天早上,他交给你 60 份"显著有效"的因子报告。你该相信多少?
1.2 Agent 挖因子的典型流程
这个循环本身就是一台多重检验机器:那个"否 → 回到提出假设"的箭头,每转一圈,就多一次在同一份数据上的尝试。
第二层:模拟实验
2.1 设置
- 120 个月、每月 300 只股票;
- Agent 生成 2000 个候选因子,其中只有 1 个真正有预测力,其余 1999 个是纯噪声;
- 前 84 个月给 Agent 做研究(样本内),后 36 个月锁进"保险箱"(样本外)。
import numpy as np, pandas as pd
rng = np.random.default_rng(99)
T, N, K = 120, 300, 2000 # 120 个月、300 只股票、Agent 生成 2000 个候选因子
fwd = rng.normal(size=(T, N)) # 未来收益
true_factor = rng.normal(size=(T, N)); fwd += 0.05 * true_factor # 只藏了一个真因子
def mean_ic(F, R):
Fz = (F - F.mean(1, keepdims=True)) / F.std(1, keepdims=True)
Rz = (R - R.mean(1, keepdims=True)) / R.std(1, keepdims=True)
ic = (Fz * Rz).mean(1)
return ic.mean(), ic.mean() / ic.std() * np.sqrt(len(ic)) # IC 均值与 t 值
split = 84 # 前 7 年给 Agent"研究",后 3 年锁进保险箱
res = []
for k in range(K):
F = true_factor if k == 0 else rng.normal(size=(T, N))
ic_is, t_is = mean_ic(F[:split], fwd[:split])
ic_oos, t_oos = mean_ic(F[split:], fwd[split:])
res.append((k, ic_is, t_is, ic_oos, t_oos))
df = pd.DataFrame(res, columns=["k", "ic_is", "t_is", "ic_oos", "t_oos"])
passed = df[df["t_is"] > 2]
print("样本内 t>2 的候选数:", len(passed), "(其中真因子:", int((passed['k'] == 0).sum()), "个)")
print("这些候选的样本外 IC 均值:", round(passed["ic_oos"].mean(), 4))
print("样本外仍 t>2 的:", int((passed["t_oos"] > 2).sum()), "个;真因子样本内 t =",
round(df.loc[0, "t_is"], 1), " 样本外 t =", round(df.loc[0, "t_oos"], 1))
strict = df[df["t_is"] > 3.0]
print("用 t>3 门槛:", len(strict), "个通过,其中真因子", int((strict['k'] == 0).sum()), "个")
输出:
样本内 t>2 的候选数: 61 (其中真因子: 1 个)
这些候选的样本外 IC 均值: 0.0014
样本外仍 t>2 的: 3 个;真因子样本内 t = 7.4 样本外 t = 5.7
用 t>3 门槛: 7 个通过,其中真因子 1 个
2.2 解读
| 环节 | 结果 |
|---|---|
| 常规门槛 t > 2 | 61 个通过,60 个是假的(单侧检验下约 2.3% 的噪声会通过,2000 × 2.3% ≈ 46,加上随机波动) |
| 这 61 个的样本外 IC | 平均约 0.001,基本为零 |
| 保险箱(样本外)检验 | 只剩 3 个,真因子在其中,样本外 t = 5.7 |
| 提高门槛 t > 3 | 7 个通过,假的大幅减少 |
两条防线组合起来(提高门槛 + 样本外保险箱),能把假发现压到很少。但要注意:样本外"t > 2"也有约 2% 的噪声能碰巧通过,所以那 3 个里仍有 2 个是假的。
第三层:防线设计
3.1 样本外保险箱
| 规则 | 说明 |
|---|---|
| 物理隔离 | 保险箱数据不放在 Agent 能访问的环境里,Agent 的工具无法读取 |
| 一次性 | 每个候选因子只能被打开检验一次 |
| 批量开箱 | 不是每个因子单独开箱,而是一批候选一起检验,并对这批的数量做多重检验校正 |
| 定期更换 | 随着时间推移,新的数据进入保险箱;旧的保险箱数据被"用过"之后降级为研究数据 |
如果 Agent 能看到保险箱的结果再回去修改因子,保险箱就变成了样本内。这一点必须靠系统权限而不是靠 Agent 自律来保证(见《Agent交易系统的安全边界》、Agent权限设计)。
3.2 按尝试次数提高门槛
- 系统自动记录 Agent 的每一次回测调用,包括被放弃的;
- 根据总尝试次数,用 Bonferroni、BHY 或 Deflated Sharpe Ratio 动态计算门槛(见《多重检验与因子动物园》);
- Agent 的报告必须包含"本轮共尝试 N 次"这一行,而且这个数字由系统填写,不由 Agent 自报。
3.3 经济逻辑审查
纯粹从数据中挖出来、没有合理解释的因子,更可能是噪声。要求每个候选因子回答:
| 问题 | 说明 |
|---|---|
| 为什么有效? | 风险补偿还是行为偏差?谁在亏钱给你? |
| 为什么还没被套利掉? | 有什么限制让别人无法利用它? |
| 在哪里应该无效? | 如果逻辑成立,在什么市场、什么时期它应该失效?去检验这个预测 |
第三个问题特别有力:一个好的解释能做出可证伪的预测。这和因果分析减少金融幻觉中"负对照"的思路一致。
3.4 其他防线
| 防线 | 说明 |
|---|---|
| 与已知因子的相关性 | 新因子如果只是市值、动量、行业的组合,就不是新发现 |
| 跨市场、跨时期检验 | 在 A 股发现的因子,在美股、港股是否也有效 |
| 参数稳定性 | 参数稍微变一点,效果是否还在(见《回测的七宗罪》) |
| 扣除成本 | 用真实的成本模型重新评估(见《交易成本建模》) |
| 多样性约束 | 防止 Agent 反复生成同一个想法的微小变体(它们本质上是一次尝试的多次计数,但会让门槛估算失真) |
| 人工复核 | 最终入库前由人审阅代码和逻辑 |
3.5 Agent 的真正价值
在这些防线之下,Agent 挖因子仍然很有价值,但价值不在于"找到更多显著的因子",而在于:
- 快速把论文和想法变成可运行的代码;
- 系统性地探索人可能忽略的数据和组合;
- 自动完成繁琐的检验流程;
- 生成可证伪的假设,交给更严格的检验。
总结
| 层次 | 要点 |
|---|---|
| 风险 | Agent 是一台多重检验机器;2000 次尝试中,t > 2 会让约 60 个噪声"通过" |
| 保险箱 | 样本外数据物理隔离、只开一次、批量校正 |
| 门槛 | 系统记录尝试次数,自动提高门槛 |
| 审查 | 经济逻辑、可证伪预测、增量信息、成本、人工复核 |