4、因子计算引擎:向量化计算、多因子并行计算、因子计算框架设计

好,咱们今天聊点硬核的——因子计算引擎。

说实话,我见过太多团队在因子计算上栽跟头。有的用纯Python循环算,一个因子跑半小时;有的把几百个因子串在一起算,改一个就得重跑全部。说白了,就是没把计算引擎当回事。

我个人习惯,做因子库之前,先把计算引擎的架构想清楚。这就像盖楼前先打地基,地基稳了,后面怎么折腾都行。

4.1 向量化计算:告别for循环

先说说向量化。你想想看,我们做量化最常处理的是什么?是时间序列数据。几千只股票,每只几百个交易日,数据量轻松上百万行。

如果用for循环去算,比如计算过去20天的均值:

# 千万别这么写
for i in range(len(close_prices)):
    if i >= 20:
        ma20[i] = np.mean(close_prices[i-20:i])

这种写法,数据量一上来,慢得你想砸电脑。我在项目中遇到过,用纯Python循环算50个因子,跑了整整一个晚上。

向量化计算,说白了就是利用NumPy、Pandas这些库的底层C语言优化,一次性处理整个数组。比如:

import pandas as pd
import numpy as np

# 向量化计算20日均线
close = pd.Series(close_prices)
ma20 = close.rolling(window=20).mean()

这一行代码,底层用的是C语言循环,速度比Python快几十倍。嗯,这里要注意:向量化不是银弹。有些复杂的逻辑,比如条件判断、分组计算,还是得用apply或者groupby。但核心原则是——能用向量化就别用手动循环。

我的小技巧:写因子公式时,先想能不能用rolling、expanding、shift这些向量化函数。实在不行,再用apply。我一般把apply控制在总计算量的10%以内。

4.2 多因子并行计算:别让CPU闲着

单个因子算得快还不够。一个因子库可能有几百个因子,如果串行计算,一个接一个跑,时间成本太高。

我曾经接手过一个项目,300多个因子,串行跑一次要40分钟。每次调参都要等这么久,人都等傻了。

后来我改成了并行计算。思路很简单:把因子分成多个批次,同时计算。Python里常用的工具有concurrent.futures、joblib、或者Dask。

from concurrent.futures import ProcessPoolExecutor
import pandas as pd

def calculate_factor(factor_name, data):
    # 假设每个因子有自己的计算函数
    return factor_name, factor_func_dict[factor_name](data)

# 并行计算所有因子
with ProcessPoolExecutor(max_workers=8) as executor:
    futures = [executor.submit(calculate_factor, name, data) 
               for name in factor_list]
    results = [f.result() for f in futures]

这里有个坑,我踩过好几次——内存爆炸。并行计算时,每个进程都会复制一份数据。如果数据量太大,比如全市场日线数据,8个进程同时跑,内存直接爆掉。

避坑指南:我曾经一次性提交了所有因子到进程池,结果内存直接飙到64GB。后来我改用分批提交,每批10个因子,跑完一批再提交下一批。这样内存占用可控,速度也不慢。

另外,并行计算不是核数越多越好。我测试过,8核和16核的加速比大概在1.5倍左右,因为Python的GIL锁和进程间通信有开销。建议根据实际数据量,选4-8个worker就够了。

4.3 因子计算框架设计:让系统可扩展

好了,向量化和并行都搞定了,接下来是重头戏——因子计算框架

我见过最糟糕的设计,是把所有因子计算逻辑写在一个几千行的文件里。新增一个因子,得从头到尾看一遍,生怕改错什么。这种代码,维护成本极高。

一个好的因子计算框架,应该满足三个要求:

  • 可扩展:新增因子只需写一个函数,不用改框架代码
  • 可配置:因子参数、计算频率、数据源都能通过配置文件调整
  • 可复用:公共计算逻辑(比如去极值、标准化)能统一调用

我个人习惯用注册器模式来设计。每个因子是一个独立的类或函数,通过装饰器注册到框架中。

# 因子注册器
class FactorRegistry:
    _factors = {}
    
    @classmethod
    def register(cls, name):
        def wrapper(func):
            cls._factors[name] = func
            return func
        return wrapper
    
    @classmethod
    def get_factor(cls, name):
        return cls._factors.get(name)

# 注册一个因子
@FactorRegistry.register('ma5_ratio')
def ma5_ratio(data):
    close = data['close']
    ma5 = close.rolling(5).mean()
    return close / ma5 - 1

这样设计的好处是:新增因子时,只需要写一个函数,加个装饰器就行。框架自动识别、自动调度。我曾经用这个模式管理过500多个因子,新增一个因子不超过5分钟。

下面是我常用的因子计算框架流程图,你可以参考一下:

因子计算框架流程图 原始数据输入 数据预处理(去极值、填充) 因子注册器(装饰器模式) 并行计算引擎(多进程) 因子矩阵输出 数据层 预处理层 注册层 计算层 输出层

这个框架的核心思想是分层解耦。数据层只管数据清洗,注册层只管因子定义,计算层只管调度。每一层各司其职,互不干扰。

核心要点:因子计算框架的设计,决定了你的因子库能长多大、能跑多快。我建议一开始就花时间把框架搭好,后面会省很多事。别像我早期那样,先凑合用,结果后面重构了三次。

4.4 实战中的几个细节

最后,分享几个我在实战中总结的细节:

  1. 缓存中间结果:很多因子会用到公共计算,比如过去20天的波动率。把这些中间结果缓存起来,能省不少时间。我一般用lru_cache或者自定义缓存字典。
  2. 因子依赖管理:有些因子依赖其他因子。比如Alpha因子依赖基础因子。这时候需要拓扑排序,确保依赖的因子先算。我踩过这个坑,后来加了个依赖声明机制。
  3. 错误隔离:一个因子算崩了,不能影响其他因子。我习惯用try-except包裹每个因子的计算,失败时记录日志,继续算下一个。

嗯,因子计算引擎这块,说白了就是三个字——快、稳、省。向量化让你快,并行让你稳,框架设计让你省心。把这三点做好,你的因子库就能支撑起后续的回测和实盘了。

一个小建议:刚开始做因子库时,别追求一次性把所有因子都算出来。先搭好框架,跑通10个核心因子,验证流程没问题了,再逐步扩展。我见过太多人一上来就搞几百个因子,结果框架没搭好,后面改得痛不欲生。

好了,这一章的内容就到这里。记住,计算引擎是因子库的心脏,值得你花时间好好打磨。


公众号:蓝海资料掘金营,微信deep3321