4. 因子数据获取与处理

在Qlib中,因子研究不是从“写一个Python函数”开始的,而是从表达式开始的。Qlib把因子定义抽象为可解析、可缓存、可复用的表达式字符串,再通过数据加载器统一读取底层二进制数据。理解这条链路,是后续风险模型和组合优化能够稳定复现的前提。

一、核心原理:表达式驱动的因子计算

Qlib的因子表达式通常写成类似 Ref($close, 1)/$close - 1 的形式。其中 $close 表示字段,Ref 表示引用历史值,Mean、Std、Corr 等算子负责时序或截面计算。表达式的好处是:因子逻辑与数据存储解耦,Qlib可以自动推断依赖字段、按时间窗口加载数据,并利用缓存避免重复计算。

一个典型的表达式处理流程如下:

  1. 解析表达式,识别字段与算子;
  2. 根据表达式所需时间窗口,确定数据加载范围;
  3. 从Qlib数据目录读取对应字段;
  4. 按表达式逐层计算,生成因子值;
  5. 对结果做缺失值、标准化和去极值处理。

关键点:表达式中的时间窗口决定了数据加载的“前置长度”。例如计算20日均线,必须多加载至少20个交易日的历史数据,否则首个有效值会缺失。

二、数据加载:从表达式到DataFrame

在Qlib中,最常用的入口是 D.features。它接收股票池、字段表达式、时间范围等参数,返回一个多层索引的DataFrame。下面是一个典型调用:

import qlib
from qlib.data import D

qlib.init(provider_uri="./qlib_data/cn_data", region="cn")

instruments = D.instruments(market="csi300")
fields = [
    "$close",
    "Ref($close, 1)/$close - 1",
    "Mean($volume, 20)/$volume",
    "Std($close, 20)/$close"
]

df = D.features(
    instruments=instruments,
    fields=fields,
    start_time="2020-01-01",
    end_time="2023-12-31",
    freq="day"
)
print(df.head())

返回的DataFrame通常以 (instrument, datetime) 为索引,列名就是表达式本身。这样做的好处是:因子名称即表达式,便于追溯和复现。但也要注意,表达式列名较长,后续建模前建议重命名为简洁字段。

实践建议:把因子表达式集中配置在YAML或Python字典中,不要散落在脚本里。这样在回测、训练和实盘之间切换时,只需替换配置,不必改代码。

三、缺失值处理、标准化与去极值

原始因子数据往往包含停牌、涨跌停、新股上市等造成的缺失值。直接丢弃或填充都可能引入偏差。Qlib本身提供了一些处理器,但在自定义研究中,通常需要显式处理。

处理环节 常用方法 注意事项
缺失值 前向填充、截面中位数填充、直接剔除 停牌股不宜简单前向填充,需结合可交易状态
去极值 MAD法、3σ法、分位数截断 应在截面上做,避免时序信息泄露
标准化 Z-Score、Rank、Min-Max 截面标准化更符合选股场景
中性化 对行业、市值回归取残差 风险模型前常做,但需防止未来函数

下面是一个截面处理的示例:

import numpy as np
import pandas as pd

def mad_winsorize(s, n=5):
    med = s.median()
    mad = (s - med).abs().median()
    upper = med + n * 1.4826 * mad
    lower = med - n * 1.4826 * mad
    return s.clip(lower, upper)

def cross_section_process(df, factor_col):
    out = []
    for dt, g in df.groupby(level="datetime"):
        s = g[factor_col]
        s = s.fillna(s.median())
        s = mad_winsorize(s)
        s = (s - s.mean()) / s.std()
        out.append(s)
    return pd.concat(out)

常见误区:先做全样本标准化再去极值,或先填充再统一标准化,都会把未来信息带入历史截面。正确顺序是:按交易日分组,先缺失值处理,再去极值,最后标准化。

四、案例复盘:一个动量因子的完整处理

假设我们要构建20日动量因子 Ref($close, 20)/$close - 1。在Qlib中加载后,可能发现部分股票在某些日期因子值为NaN。复盘时可按以下清单检查:

  • 是否因为上市不足20个交易日?
  • 是否因为停牌导致价格未更新?
  • 表达式窗口是否与数据加载范围匹配?
  • 截面标准化是否在每日独立完成?
  • 去极值阈值是否过松或过紧?

如果发现因子分布尾部极厚,可以先用MAD截断,再观察IC变化。若标准化后因子均值仍显著偏离0,说明分组或填充逻辑可能有问题。最终,处理后的因子应满足:每日截面近似标准正态、缺失率可控、极值影响被削弱,同时保留原始排序信息。

一句话总结:Qlib因子处理的核心不是“算出来”,而是“算得可复现、可追溯、无泄露”。表达式负责定义,数据加载负责取数,缺失值、去极值和标准化负责把原始值变成可用于风险模型和组合优化的稳定输入。