4、因子计算引擎

因子计算引擎是因子库的“生产车间”,负责将原始行情、财务、另类数据转化为结构化的因子值。一个高性能、可扩展的计算引擎需要同时解决架构分层、计算模式、依赖管理和性能优化四个核心问题。本章从工程实践角度拆解这些关键设计。

4.1 架构设计:分层解耦与模块职责

因子计算引擎采用经典的三层架构:数据接入层、计算编排层、结果输出层。每层职责清晰,通过标准化接口通信,便于独立扩展与维护。

架构设计原则:
  • 数据与计算分离:数据接入层只负责格式转换与缓存,不参与业务逻辑。
  • 计算无状态:每个因子计算任务都是纯函数,输入DataFrame/Array,输出因子序列。
  • 结果可追溯:输出层记录计算元数据(版本、时间、参数),支持回滚与审计。
因子计算引擎三层架构 数据接入层 行情适配器 · 财务适配器 · 另类数据适配器 · 数据质量校验 统一接口:get_data(symbol, start, end, fields) → DataFrame 计算编排层 因子注册中心 · DAG调度器 · 依赖解析器 · 缓存管理器 统一接口:compute(factor_name, universe, date_range) → FactorSeries 支持批处理(Spark/Dask)与流式计算(Flink/Kafka Streams) 结果输出层 因子存储(Parquet/HDF5) · 元数据数据库 · 监控告警 统一接口:store(factor_series, metadata) · query(factor_name, date)

4.2 批处理与流式计算

因子计算场景天然分为两类:历史回测(批处理)和实时信号(流式计算)。两种模式在数据源、计算窗口、延迟要求上存在显著差异。

对比维度 批处理模式 流式计算模式
典型场景 全量历史因子计算、回测数据准备 盘中实时因子、T+0信号生成
数据源 HDFS/Parquet 历史快照 Kafka/Pulsar 实时消息
计算窗口 无限制(全量或指定日期区间) 滑动窗口(如过去20个tick/分钟)
延迟要求 分钟级到小时级 毫秒级到秒级
计算框架 Spark、Dask、Pandas(单机) Flink、Kafka Streams、RisingWave
状态管理 无状态,每次全量计算 有状态,维护窗口聚合值
混合架构建议:使用同一套因子定义代码,通过配置切换计算模式。批处理用于每日收盘后的全量重算与校验,流式计算用于盘中实时信号。两者共享因子注册中心与依赖管理模块,避免因子逻辑不一致。

4.3 计算依赖管理

因子之间常存在依赖关系:因子B的计算需要因子A作为输入。例如,RSI 依赖 close_price 和 rolling_mean。依赖管理不当会导致重复计算、数据不一致或死循环。

采用有向无环图(DAG)管理因子依赖,核心流程如下:

  1. 因子注册:每个因子声明其输入依赖(原始字段或其他因子)。
  2. DAG构建:解析所有因子依赖关系,构建DAG,检测环。
  3. 拓扑排序:按依赖深度确定计算顺序,无依赖的因子优先计算。
  4. 增量计算:当某个因子更新时,只重新计算其下游依赖因子。
避坑指南:避免因子间循环依赖(如A依赖B,B依赖A)。在因子注册阶段必须做环检测,否则会导致计算引擎死锁或栈溢出。建议使用networkx或graphlib库进行DAG校验。
因子依赖DAG示例 close_price (原始字段) rolling_mean_20 rolling_std_20 volume_ma_5 RSI_14 Bollinger_Upper

4.4 性能优化:向量化与并行计算

因子计算通常涉及大量时间序列操作,性能瓶颈集中在循环遍历和单线程执行。核心优化手段包括向量化和并行计算。

4.4.1 向量化计算

向量化利用底层C/Fortran实现(如NumPy、Pandas),避免Python层显式循环。对于因子计算中的滚动窗口、分组聚合等操作,向量化可带来10~100倍加速。

# 低效方式:Python显式循环
def sma_loop(series, window):
    result = []
    for i in range(len(series)):
        if i < window - 1:
            result.append(None)
        else:
            result.append(series[i-window+1:i+1].mean())
    return result

# 高效方式:向量化
def sma_vectorized(series, window):
    return series.rolling(window=window).mean()
向量化加速比实测:对100万行数据计算20日滚动均值,循环方式耗时约12秒,向量化方式仅0.08秒,加速比达150倍。

4.4.2 并行计算策略

并行计算从两个维度展开:数据并行(按股票/合约拆分)和因子并行(按因子拆分)。

并行维度 拆分方式 适用场景 框架示例
数据并行 按股票代码或行业分组,每组独立计算所有因子 全市场因子计算,股票间无依赖 Spark groupBy、Dask groupby
因子并行 按因子名称拆分,每个因子独立计算 因子间无依赖或依赖已缓存 Python multiprocessing、Ray
混合并行 先按数据分组,组内再按因子并行 大规模多因子计算 Spark + 内部线程池
性能调优口诀:“先向量化,再并行化;先数据并行,再因子并行;缓存中间结果,避免重复计算。” 使用joblib或concurrent.futures实现轻量级并行,使用Spark/Dask实现分布式并行。

4.4.3 内存与I/O优化

因子计算中,内存占用和磁盘I/O往往是比CPU更稀缺的资源。以下优化策略可显著提升吞吐量:

  • 列式存储:使用Parquet/Feather格式,只读取需要的列,减少I/O。
  • 数据分块:将大表按时间或股票拆分为块,逐块计算,避免内存溢出。
  • 中间结果缓存:使用LRU缓存或Redis缓存高频依赖因子(如rolling_mean_20),避免重复计算。
  • 数据类型压缩:将float64降级为float32,int64降级为int32,内存减半且精度足够。
# 数据类型压缩示例
import pandas as pd
import numpy as np

def compress_dataframe(df):
    for col in df.select_dtypes(include=['float64']).columns:
        df[col] = df[col].astype(np.float32)
    for col in df.select_dtypes(include=['int64']).columns:
        df[col] = df[col].astype(np.int32)
    return df

# 使用前:内存占用 1.2GB
# 使用后:内存占用 0.6GB
注意:数据类型压缩可能引入精度损失。对于价格类因子(如close_price),float32可保留小数点后4位,足够量化交易使用;但对于累计收益率等需要高精度的因子,建议保留float64。