4、因子计算引擎:单因子计算、多因子并行计算、分布式计算框架选型(Dask/Ray)

因子计算引擎,说白了就是整个因子库的「心脏」。你因子设计得再好,逻辑再严密,跑不出来或者跑得太慢,一切都是白搭。我见过太多团队,因子研究做得风生水起,一到实盘回测就卡在计算环节,三天三夜跑不完一个月的因子数据。嗯,今天我们就来聊聊怎么把这个引擎造得又快又稳。

4.1 单因子计算:基本功要扎实

单因子计算是所有复杂计算的基础。你想想看,如果连一个因子的计算都搞不定,谈什么并行和分布式?

我个人习惯把单因子计算拆成三个步骤:数据准备 → 逻辑运算 → 结果校验

  • 数据准备:确保你拿到的行情数据、财务数据是干净的。我曾经因为一个除权除息没处理好,导致整个因子序列出现跳变,排查了整整两天。
  • 逻辑运算:用向量化操作代替循环。这是铁律。在Python里,能用pandas/numpy的向量化函数,就别写for循环。速度差距是几十倍甚至上百倍。
  • 结果校验:跑完因子后,一定要看一眼分布、相关性、覆盖率。别等到回测阶段才发现因子是坏的。
核心原则:单因子计算要「原子化」。每个因子只做一件事,做好一件事。这样后续的并行和分布式才能拆得开、合得拢。

举个简单的例子,计算一个「过去5日收益率」因子:

import pandas as pd
import numpy as np

def calc_ret_5d(close: pd.DataFrame) -> pd.DataFrame:
    """
    计算过去5个交易日的收益率
    """
    # 向量化操作,一行搞定
    ret_5d = close.pct_change(periods=5)
    # 去掉前5天的NaN值
    ret_5d.iloc[:5] = np.nan
    return ret_5d

你看,就这么几行。但这里有个坑——pct_change默认是向后填充的,如果你数据里有停牌日,收益率会算错。我建议你提前把停牌日的数据填充为前值,或者用ffill处理一下。

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

单因子算完了,接下来就是批量生产。假设你有100个因子要同时计算,一个一个跑?太慢了。这时候就要上并行。

多因子并行计算,我把它分成两种场景:

  • 因子间并行:不同因子之间没有依赖关系,可以同时算。比如「动量因子」和「波动率因子」互不干扰。
  • 因子内并行:同一个因子,但数据量太大(比如全市场5000只股票,10年日频数据),需要把数据切块并行处理。

在Python里,最简单的并行工具就是concurrent.futures。但说实话,对于量化场景,它有点「不够用」。为什么?因为因子计算往往涉及大量内存操作,而concurrent.futures的进程间通信开销很大。

我的经验:如果因子数量在50个以内,数据量在10GB以下,用multiprocessing.Pool就够了。超过这个量级,建议直接上分布式框架。

这里给一个简单的因子间并行示例:

from concurrent.futures import ProcessPoolExecutor
import pandas as pd

def compute_factor(factor_name: str, data: pd.DataFrame) -> pd.Series:
    """
    根据因子名称调用对应的计算函数
    """
    if factor_name == 'ret_5d':
        return calc_ret_5d(data)
    elif factor_name == 'volatility':
        return calc_volatility(data)
    # ... 其他因子

# 并行计算所有因子
factor_list = ['ret_5d', 'volatility', 'turnover', 'illiquidity']
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(compute_factor, factor_list, [data]*len(factor_list)))

嗯,这里要注意:ProcessPoolExecutor会把数据复制到每个子进程,如果数据太大,内存会爆。我曾经在16GB的机器上跑200个因子,结果直接OOM了。后来改用共享内存才解决。

4.3 分布式计算框架选型:Dask vs Ray

当你的因子库规模达到几百甚至上千个,单机并行已经撑不住了。这时候就需要分布式计算框架。目前业界主流就是两个:DaskRay

我两个都用过,说说我的感受。

维度 Dask Ray
学习曲线 低(API像pandas) 中(需要理解Actor模型)
数据处理 强(原生支持DataFrame) 弱(需要自己实现)
任务调度 静态图(DAG) 动态图(更灵活)
内存管理 自动(但容易OOM) 手动(更可控)
适合场景 批量数据处理、ETL 实时计算、强化学习

说白了,如果你主要做因子计算和回测,Dask是更自然的选择。因为因子计算本质上就是大规模DataFrame操作,Dask的API几乎和pandas一模一样,迁移成本极低。

而Ray更适合需要复杂状态管理和实时交互的场景。比如你有一个因子需要实时订阅行情并更新,Ray的Actor模型就很好用。

避坑指南:我曾经在一个项目里用Dask处理3000个因子的日频计算,集群有20个节点。一开始一切正常,但跑了两个月后,发现Dask的任务调度图越来越大,最终导致调度器内存溢出。后来我改用「分批提交」的方式,每次只提交100个因子的任务,问题就解决了。所以,不要一次性提交太多任务,Dask的调度器扛不住。

4.4 实战选型建议

说了这么多,到底怎么选?我给出一个简单的决策树:

  • 因子数量 < 100,数据量 < 50GB:单机并行就够了,别折腾分布式。
  • 因子数量 100-500,数据量 50GB-500GB:上Dask。用它的dask.dataframe替代pandas,几乎不用改代码。
  • 因子数量 > 500,或者需要实时计算:考虑Ray。虽然学习成本高,但灵活性和扩展性更好。

另外,我建议你不要一开始就上分布式。先单机跑通,再并行,最后分布式。每一步都验证结果的一致性。分布式计算最怕的就是「结果对不上」,排查起来非常痛苦。

4.5 核心逻辑流程图

下面这张图展示了因子计算引擎的整体架构,从单因子到分布式,层层递进:

因子计算引擎架构图 数据源 行情 / 财务 / 另类数据 单因子计算 向量化操作 · 原子化设计 · 结果校验 多因子并行计算 因子间并行 · 因子内并行 · ProcessPoolExecutor 分布式计算框架选型 Dask(批量处理) vs Ray(实时计算) 因子库输出 数据流方向

从数据源开始,经过单因子计算、多因子并行,再到分布式框架选型,最终输出因子库。每一步都有对应的技术选型和注意事项。我个人建议你从最左边开始,一步步往右走,不要跳级。

一个小技巧:在搭建因子计算引擎时,先把「单因子计算」的接口定义好。比如统一输入输出格式、统一参数传递方式。这样后续无论是并行还是分布式,都只需要改调度层,不用动因子逻辑层。我在团队里推行这个规范后,因子开发的效率提升了至少30%。

好了,关于因子计算引擎的内容就这些。记住:先单机,再并行,后分布式。每一步都稳扎稳打,你的因子库才能跑得又快又准。

专注资料整理