halo 的技术博客

返回

先说结论:遗传编程(GP)能在你没想到的公式空间里自动搜出可交易因子,但它同时是一台高效的过拟合机器——你给它越多算力,它越擅长把噪声雕刻成好看的训练 IC。本文用纯 Python 从零写一个极简 GP 引擎,在埋了真实非线性结构的合成面板上进化 25 代,最优因子的验证集 IC 达到 0.098,超过最强单因子(0.078)、逼近理论上限(0.105)。然后我会用 500 个随机公式在纯噪声上搜索,向你演示为什么”搜得越多,训练集越好看”是 GP 最危险的性质。

为什么要让公式自己进化#

手工构造因子的流程是:你先有一个金融直觉(“动量好但高波动时不可靠”),翻译成公式(mom * exp(-|vol|)),再拿数据验证。瓶颈在第一步——人的直觉枚举不了公式空间。三个特征、四个算子、深度 3 的表达式树就有上万种组合,其中可能藏着你永远想不到的形态。

遗传编程把这个搜索交给进化:

  1. 编码:每个候选因子是一棵表达式树。叶子节点是特征(动量、波动率、换手率、反转)或常数,内部节点是算子(加减乘除、取负、绝对值、exp(-|x|)、符号函数)
  2. 适应度:把树求值成因子值,算训练集上的 Rank IC
  3. 选择:锦标赛——随机抽 5 个个体,最强者获得繁殖权
  4. 交叉:从父母各随机剪一棵子树互换,产生新公式
  5. 变异:随机挑一个节点,替换成一棵随机新子树
  6. 重复若干代,适应度差的公式被淘汰,好的结构片段在种群里扩散

关键洞察:交叉操作天然保护”有用的子结构”。如果 (mom - rev) 是个好片段,它会作为整体在种群里被复制传播,而不是每次都要从头拼出来。这是 GP 相对随机搜索的本质优势。

极简 GP 引擎:不到 100 行的核心#

表达式树用嵌套元组表示,求值是递归下降:

两个工程细节值得单独说:

  • 保护性除法a / (|b| + 1) 而不是 a / b。裸除法会在 b 接近零时产生 inf,一个 inf 污染整个截面排名,让适应度计算崩掉。GP 文献里这叫 protected operator,所有可能产生 NaN/inf 的算子都要包一层。
  • 常数叶子:允许树里出现 0.37 这种常数,让进化能微调权重(比如 mom - 0.5*rev 里的 0.5)。没有常数叶子,GP 只能表达等权组合。

交叉和变异都建立在”随机取一个节点、替换整棵子树”之上:

def crossover(a, b):
    """从 a 随机选一个位置,用 b 的随机子树替换"""
    pa, _  = random.choice(nodes_list(a))   # a 中随机位置
    _, nb  = random.choice(nodes_list(b))   # b 中随机子树
    return replace(a, pa, nb)

def mutate(a):
    p, _ = random.choice(nodes_list(a))
    return replace(a, p, rand_tree(2))      # 换成随机新子树
python

实验设置:埋一个 GP 该找到的真结构#

合成面板 600 天 × 100 只股票,四个标准化特征:动量 mom、波动率 vol、换手率 turn、短期反转 rev。未来收益由一个非线性真实结构驱动:

true_alpha = mom * np.exp(-np.abs(vol)) - 0.5 * rev
fwd_ret    = 0.15 * true_alpha + noise     # 信噪比故意压低
python

这个结构的金融语义很自然:动量有效但被高波动打折(exp(-|vol|) 是打折函数),反转贡献负向。前 400 天做训练(适应度只看这里),后 200 天做验证(进化过程完全不接触)。

进化参数:种群 150、代数 25、精英保留 5、锦标赛规模 5、交叉率 0.7、变异率 0.2。适应度是训练集 Rank IC 减去简约压力

fitness = train_ic - 0.0015 * tree_size    # 每多一个节点罚 0.0015
python

结果:进化找到了 (mom - rev)#

25 代之后,最优个体是 (mom - rev)——只有 3 个节点。看四组数字:

验证集 IC 对比

因子验证集 Rank IC
动量 mom(最强单因子)0.078
反转 rev-0.065
波动率 / 换手率≈0
GP 进化因子 (mom - rev)0.098
真实结构(理论上限)0.105

三个观察:

  1. GP 因子比最强单因子高 26%(0.098 vs 0.078)。它发现了”反转的负 IC 可以翻过来用”——mom - rev 等价于做多动量同时做多”反转因子的负向”,把两份信息合成一份。
  2. 离理论上限只差 7%(0.098 vs 0.105)。没找到 exp(-|vol|) 打折项,因为在这个信噪比下,波动率打折带来的边际 IC 增益小于简约压力的惩罚——进化选择了”性价比最高”的结构而不是”最完整”的结构。这不是失败,这正是简约压力在正确工作
  3. 训练 IC 0.0984、验证 IC 0.0978,几乎无衰减。简单的表达式泛化好,这是下文所有陷阱讨论的正面参照。

进化曲线

进化曲线显示第 1 代最优就有 0.076(种群 150 里随机撞上了裸 mom),之后逐步爬到 0.098。训练与验证两条线基本贴合——当它们分叉时,就是过拟合的信号,应该早停

膨胀(bloat):GP 的癌症#

不加约束的 GP 有个臭名昭著的病:表达式树无限膨胀。交叉倾向于产生更大的树(大树里能剪出更多子树),而更大的树里充斥着 (x - x)sign(sign(x)) 这种中性垃圾——它们不改变适应度,却让公式膨胀到几百个节点,完全不可读、不可信、不可交易。

膨胀控制

我们的防御是两层:简约压力(每节点罚 0.0015 IC)+ 硬上限(超过 25 节点直接重新生成)。效果:种群平均节点数从初始 6.1 降到 3.8,最优个体稳定在 3 节点。如果你去掉简约压力重跑,平均尺寸会在 25 代内涨到 20+,而验证 IC 不会更高——多出来的节点全是拟合噪声的自由度

经验法则:简约系数选多大?让”多 10 个节点”的惩罚大约等于你认为的 IC 噪声水平(本例 0.015)。太小控不住膨胀,太大会把真实的复杂结构也罚没了。

最危险的性质:GP 是数据挖掘偏差的放大器#

现在到全文最重要的部分。GP 的本质是在巨大的公式空间里挑训练集上最好看的那个——这个动作本身就有毒,哪怕特征和收益毫无关系。

实验:4 个纯噪声特征(与收益零相关),随机生成 500 个表达式,每次记录”迄今最优训练 IC”:

数据挖掘偏差

红线(迄今最优训练 IC)单调上涨:10 个公式后 0.007,100 个后 0.011,500 个后 0.018。特征是纯噪声,但”最优公式”的训练 IC 看起来越来越像真信号——这就是数据挖掘偏差(data-mining bias / multiple testing):从 K 个随机变量里挑最大值,期望值随 K 单调上升。蓝线(同一公式的验证 IC)在低位震荡,随时变号——噪声不会跨样本复制。

对照另一张图:在有真信号的特征上生成 300 个随机表达式,训练 IC 与验证 IC 的相关系数高达 0.965——因为大部分表达式的 IC 由它包含的真实特征贡献,这部分能跨期复制。

过拟合散点

两张图合起来是一个判据:你的 GP 挖出的因子属于哪张图? 判别方法只有一个——严格隔离的样本外。

三层防御:把过拟合机器变成研究工具#

第一层:验证集早停 + 从不回看。 适应度只在训练段计算,验证段只用于监控。一旦最优个体的验证 IC 连续多代下滑而训练 IC 还在涨,立即停止进化。最阴险的失误是”看了验证集表现后调整算子集/参数再重跑”——这让验证集悄悄变成了训练集,你需要第三段从未接触的数据才能说话。

第二层:简约压力是过拟合税。 节点数惩罚不只是为了可读性——每个额外节点都是一个额外的自由度,而自由度就是拟合噪声的容量。3 节点的 (mom - rev) 泛化好不是偶然,是结构性的。

第三层:多种子稳定性检验。 换 5 个随机种子重跑进化。如果 5 次跑出的最优表达式在结构上高度相似(都含 mom 正向、rev 负向),说明进化收敛到的是数据里的真结构;如果 5 次结果五花八门、共同点只有”训练 IC 都不错”,那你挖到的是 5 份不同形状的噪声。表达式的稳定性比单次 IC 的高低更有信息量。

实践中还会撞上的三件事#

算子集是最强的先验。 本文给了 exp(-|x|) 算子,真实结构才有机会被表达。实践中算子集的选择(是否给时序算子 ts_rankdecay_linear?是否给横截面算子 cs_rank?)直接决定 GP 能探索的空间——这一步依然需要金融直觉,GP 只是把直觉从”具体公式”上移到了”算子词表”层面。WorldQuant 的 Alpha101 里大量使用的正是这类时序/截面算子组合。

适应度不该只有 IC。 生产级实现会用多目标:IC + IC 的 t 统计量 + 换手率惩罚 + 与已有因子库的相关性惩罚。最后一项尤其重要——挖出一个与你库里动量因子相关 0.95 的”新”因子毫无价值。

求值是瓶颈,缓存是刚需。 进化中大量个体是重复或近似重复的(精英保留 + 交叉都会复制结构)。对 repr(tree) 做记忆化缓存,本文实验的求值次数从 150×25=3750 次降到约 1400 次实际计算。面板更大时这决定了你能否把种群和代数开到有统计意义的规模。

收尾#

遗传编程把因子研究从”人想公式”变成”人设计搜索空间、机器搜公式”。本文的实验给出一个诚实的两面:它确实找到了超越所有单因子、逼近理论上限的组合(验证 IC 0.098 vs 上限 0.105),且简约压力让结果保持在 3 个节点的可读尺寸;但纯噪声实验同样清楚地演示了——搜索能力本身就是偏差来源,500 次随机尝试能把纯噪声的训练 IC 推到 0.018 并且还在涨。

用一句话总结使用纪律:GP 给你的每一个因子都应该被当作”嫌疑人”而不是”发现”——直到它通过了从未参与进化的样本外检验、通过了多种子稳定性检验、并且你能为它的结构讲出一个进化之前就成立的金融故事。

(本文实验为合成数据教学演示,IC 量级与真实市场不可直接类比;实盘因子挖掘还需处理生存偏差、交易成本与容量约束。不构成投资建议。)

遗传编程因子挖掘:让表达式树自己进化出可交易 alpha
https://blog.halo26812.eu.org/blog/genetic-programming-factor
Author halo
Published at 2026年7月25日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨