Qlib特征工程全景概览

1. 引言:量化特征工程的特殊性

1.1 金融时序数据的三大挑战

在量化投资领域,特征工程面临着传统机器学习领域所未见的三大核心挑战,这些挑战直接决定了Qlib的设计哲学和技术路线。

挑战一:非平稳性(Non-stationarity)

金融时间序列的统计特性随时间漂移,这是量化特征工程最根本的难题。设时间序列 ,其分布 在不同时间窗口 上不满足平稳性条件:

这意味着:

  • 历史有效的因子在未来可能失效
  • 训练集和测试集分布存在差异
  • 模型需要持续更新和适应

举例来说,动量因子在牛市中IC可能达到0.08,但在熊市中可能降至0.02甚至变为负值。这种分布漂移要求特征工程系统具备:

  1. 时间窗口敏感性:自动检测因子衰减周期
  2. 动态因子选择:根据市场状态调整因子权重
  3. 回测一致性:确保历史表现具有外推性

挑战二:自相关性(Autocorrelation)

金融价格序列存在显著的自相关结构,这对特征工程提出了双重约束:

其中 是滞后 阶的自相关系数。

自相关性的影响:

  • 特征独立性假设失效:传统ML假设样本独立,但金融数据样本在时间上强相关
  • 交叉验证方法受限:不能使用随机K-Fold,必须采用时间序列交叉验证
  • 信息泄露风险增加:不小心引入未来函数会导致模型表现虚高

挑战三:信噪比低(Low Signal-to-Noise Ratio)

金融数据的信噪比极低,估计在 量级。这意味着:

低信噪比的后果:

  • 特征工程必须极其谨慎,每一步操作都可能放大噪声
  • 过拟合风险极高,需要严格的样本外验证
  • 因子组合需要通过大数定律分散风险

1.2 传统ML方法在量化中的失效案例

让我们通过一个典型失效案例,理解传统ML特征工程在量化中的陷阱。

案例:PCA在量化中的数据泄漏

假设你对50个技术指标进行PCA降维,使用sklearn标准流程:

from sklearn.decomposition import PCA
pca = PCA(n_components=5)
factors_pca = pca.fit_transform(factor_matrix)  # fit使用全部数据

这个操作存在两个致命问题:

问题1:未来信息泄露

PCA在计算主成分时,使用了整个时间窗口的数据。对于时刻 的因子值,PCA的计算实际上包含了 的信息:

这违反了因果性约束,导致回测结果虚高。

问题2:时序平稳性假设

PCA假设数据的协方差矩阵在整个时间窗口内稳定:

但在金融时序中, 随时间剧烈变化,导致历史训练的主成分在未来失效。

Qlib的解决方案

Qlib通过以下机制避免了上述问题:

  1. 表达式系统强制因果性:所有算子只能使用历史数据
  2. 增量计算:Rolling操作避免跨时间窗口的信息泄露
  3. 分步训练:每个时间窗口独立计算统计量,不使用未来信息

1.3 Qlib的诞生背景与设计哲学

历史背景

Qlib由微软亚洲研究院于2020年开源,旨在解决量化投资中特征工程的标准化和可复现性问题。在此之前,量化团队各自实现特征计算,导致:

  • 因子定义不统一,难以复现
  • 回测结果缺乏一致性
  • 因子研究效率低下

核心设计哲学

Qlib的设计基于三个核心原则:

原则1:可审计的计算图(Auditable Computation Graph)

特征工程不是黑盒,而是显式的DAG(有向无环图):

其中每个操作 的语义清晰、可追溯、可审计。

原则2:因果性前置约束(Causality-First Constraint)

在系统层面强制因果性,而不是依赖开发者自律:

所有算子在定义时就必须遵守”只能使用历史数据”的约束。

原则3:回测一致性优先(Backtest-Consistency Priority)

宁可牺牲部分灵活性,也要保证回测结果的可信度:

  • 限制某些看似强大但可能导致泄漏的操作
  • 提供严格的时序验证机制
  • 输出详细的因子构建日志

2. Qlib特征工程的核心定义

2.1 数学定义:Factor Tensor Transformation

Qlib特征工程的核心是一个从原始数据到因子张量的映射函数

其中:

  • :原始数据空间,包含行情、行为等基础字段
  • :时间维度(Time)
  • :资产维度(Number of instruments)
  • :因子维度(Factors)

原始数据的形式化定义

设原始数据为三维张量 ,其中 是原始字段数(如open, high, low, close, volume):

表示资产 在时刻 的字段 的值。

特征工程的目标

Qlib的目标是将 转换为可被模型学习的因子张量

其中:

  • :第 个因子
  • :第 个因子的计算表达式

2.2 Pipeline形式化表示

Qlib的特征工程Pipeline可以形式化为复合函数:

其中四个层次的操作分别为:

1. 表达式层(Expression Layer)

将原始字段通过算子组合成基础因子:

2. 时间结构层(Temporal Layer)

应用滚动、滞后、衰减等时间操作:

3. 横截面层(Cross-Sectional Layer)

在同一时刻对多个资产进行标准化、排序、中性化:

标准化定义为:

其中 是横截面均值, 是标准差。

4. 标准化层(Normalization Layer)

最终的标准化处理:

Pipeline的图示

原始数据 [T, N, V]
    ↓ Expression层
中间因子 [T, N, K]
    ↓ Temporal层
时间特征 [T, N, K]
    ↓ Cross-section层
横截面特征 [T, N, K]
    ↓ Normalization层
最终因子 [T, N, F]

2.3 与sklearn特征工程的对比表格

Qlib特征工程与传统机器学习(sklearn)特征工程在多个维度上存在根本差异:

维度sklearn特征工程Qlib特征工程
数据结构[N, F] 二维矩阵[T, N, F] 三维张量
时序假设样本独立(IID)样本时序相关
时间处理手动shift,易出错内置Ref/Lag,语义清晰
因果性保证无保证,依赖开发者系统强制约束
特征计算静态预处理动态计算图
横截面操作需手动实现内置Z-score/Rank/Neutralize
可回测性低,易引入未来函数高,时序验证机制
可解释性中等强,每步可追溯
适用场景图像、文本等静态数据金融时序、推荐系统等序列数据

关键差异解析

差异1:数据结构

sklearn处理的是静态特征矩阵,假设样本独立:

其中 是样本数, 是特征数。

Qlib处理的是动态特征张量,包含时间维度:

这个差异导致了两个系统在设计哲学上的根本不同:

  • sklearn适合静态分类/回归任务
  • Qlib适合时序预测任务

差异2:因果性保证

sklearn中的StandardScaler没有时序概念:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 使用全部数据计算μ和σ

这会导致数据泄漏:在计算 时刻的标准化值时,使用了 之后的数据。

Qlib中的标准化是时序感知的:

# Qlib伪代码
z[t, i] = (x[t, i] - μ[t]) / σ[t]
# μ[t]只使用t时刻的N个资产计算,不包含t+1, t+2...

差异3:可回测性

sklearn特征工程的回测结果往往不可信,因为:

  1. 容易引入Look-ahead Bias
  2. 没有时序验证机制
  3. 缺乏详细的因子构建日志

Qlib通过以下机制保证回测一致性:

  1. 强制因果性:所有算子只能使用历史数据
  2. 增量计算:Rolling操作避免跨时间窗口的信息泄露
  3. 审计日志:记录每个因子的计算过程

3. 四层架构深度解析

Qlib特征工程的四层架构是其设计的核心,每一层都有明确的职责和约束。让我们从底层向上逐层解析。

3.1 Layer 1:原始字段层(Raw Feature Layer)

定义

原始字段层是最基础的数据层,直接来自数据源的原始字段。这些字段通常来自:

  • 行情数据high, close, amount
  • 财务数据pe_ratio, roe, $debt_ratio
  • 衍生数据amplitude, $change_pct

数据特性

原始字段具有以下特性:

  • 低信噪比:单字段预测能力极弱
  • 多源异构:不同字段量纲、频率、覆盖率不同
  • 存在缺失:某些资产在某些时刻可能缺失某些字段
  • 包含噪声:数据清洗和去噪是预处理重点

代码示例

# 原始字段示例
$close          # 收盘价
$volume         # 成交量
$market_cap     # 市值
$turnover_rate  # 换手率

使用建议

  • 直接使用原始字段作为特征效果很差
  • 需要通过Layer 2的表达式层进行组合和变换
  • 缺失值处理需要在原始字段层完成

3.2 Layer 2:表达式层(Expression Layer)⭐

定义

表达式层是Qlib的灵魂,通过算子组合将原始字段转换为基础因子。表达式可以看作是一个有向无环图(DAG),每个节点是一个算子,边是数据流。

核心思想

表达式 = 算子 + 原始字段 + 参数

语法示例

# 基础算子
Ref($close, 1)           # 昨收盘(滞后1期)
Mean($close, 20)         # 20日均线
Std($close, 20)          # 20日标准差
Max($high, 5)            # 5日最高价
Min($low, 5)             # 5日最低价
 
# 组合算子
($close - Mean($close, 20)) / Std($close, 20)  # BOLL(布林带)
Mean($close, 5) / Mean($close, 20) - 1        # MA乖离率
($close - Ref($close, 1)) / Ref($close, 1)     # 日收益率

实用表达式库

以下提供10+个常用的Qlib表达式,涵盖技术分析的主要类别:

技术指标类

  1. 移动平均线(MA)

    ma_5 = Mean($close, 5)
    ma_20 = Mean($close, 20)
    ma_60 = Mean($close, 60)
  2. 指数移动平均(EMA)

    ema_12 = EMA($close, 12)
    ema_26 = EMA($close, 26)
  3. 相对强弱指数(RSI)

    # RSI = 100 - (100 / (1 + RS))
    # RS = 平均涨幅 / 平均跌幅
    rsi_14 = RSI($close, 14)
  4. 布林带(BOLL)

    ma_20 = Mean($close, 20)
    std_20 = Std($close, 20)
    boll_upper = ma_20 + 2 * std_20
    boll_lower = ma_20 - 2 * std_20
    boll_width = (boll_upper - boll_lower) / ma_20

动量类

  1. 变动率(ROC)

    roc_5 = ($close / Ref($close, 5)) - 1
    roc_20 = ($close / Ref($close, 20)) - 1
  2. 动量因子

    momentum_10 = $close - Ref($close, 10)
    momentum_20 = $close - Ref($close, 20)

波动类

  1. 平均真实波幅(ATR)

    # ATR = MA(True Range, N)
    # True Range = max(high-low, abs(high-ref(close,1)), abs(low-ref(close,1)))
    atr_14 = ATR($high, $low, $close, 14)
  2. 历史波动率(HV)

    # HV = Std(收益率, N)
    returns = ($close / Ref($close, 1)) - 1
    hv_20 = Std(returns, 20)

成交量类

  1. 能量潮(OBV)

    # OBV = Σ sign(close变化) * volume
    obv = OBV($close, $volume)
  2. 量价关系

    vol_price_ratio = $volume / $close
    vol_ma_5 = Mean($volume, 5)
    vol_ratio = $volume / vol_ma_5

复合因子

  1. MACD

    dif = ema_12 - ema_26
    dea = EMA(dif, 9)
    macd = (dif - dea) * 2
  2. KDJ

    # K值、D值、J值的计算
    rsv = ($close - Min($low, 9)) / (Max($high, 9) - Min($low, 9))
    k = SMA(rsv, 3)
    d = SMA(k, 3)
    j = 3 * k - 2 * d

特点与约束

Qlib表达式层有以下特点:

特点1:可组合性(Composability)

表达式可以任意组合,形成复杂的计算图:

例如:

factor = Std(
    ($close - Mean($close, 20)) / Std($close, 20),
    10
)

特点2:语义清晰(Semantic Clarity)

每个算子都有明确的金融语义,不是黑盒:

  • Mean($close, 20):20日均线(平滑价格)
  • Ref($close, 1):滞后1期(避免未来函数)
  • Std($close, 20):20日波动率(风险度量)

特点3:强制因果性(Causality Constraint)

所有算子只能使用历史数据,这是系统级约束:

例如,Ref($close, 1) 是正确的,因为使用的是 时刻的数据。 错误的写法:Ref($close, -1),因为使用了 时刻的数据。

特点4:天然支持Rolling/Lag

表达式系统天然支持滚动窗口和滞后操作:

RollingMean($close, 20)  # 20日滚动均值
Lag($close, 5)            # 滞后5期

这些操作在计算时会自动处理时间对齐,避免数据泄漏。


3.3 Layer 3:时间结构层(Temporal Feature Engineering)

定义

时间结构层关注如何在时间维度上提取特征,这是金融时序数据的本质难点。核心问题是:

模型学到的是”时间结构”还是”噪声”?

常见操作

Qlib在时间结构层提供了以下操作:

1. Lag(滞后)

将历史数据取到当前时刻:

应用场景:

  • 获取历史价格:Lag($close, 1) 获取昨收盘
  • 构建时滞特征:Lag($volume, 5) 获取5日前的成交量
  • 避免未来函数:使用滞后数据计算当前指标

2. Rolling(滚动窗口)

在滚动时间窗口上计算统计量:

常见Rolling算子:

  • RollingMean(X, w):滚动均值
  • RollingStd(X, w):滚动标准差
  • RollingMax(X, w):滚动最大值
  • RollingMin(X, w):滚动最小值
  • RollingCorr(X, Y, w):滚动相关系数
  • RollingRegression(X, Y, w):滚动回归

应用场景:

  • 技术指标计算:RollingMean($close, 20) 是20日均线
  • 波动率度量:RollingStd($return, 20) 是20日波动率
  • 动量识别:RollingCorr($close, $market, 20) 衡量与市场相关性

3. Decay(衰减)

对历史数据应用指数衰减,赋予近期数据更高权重:

其中 是衰减因子。

应用场景:

  • EMA(指数移动平均):EMA($close, 20) 本质是衰减均值
  • 动量衰减:Decay($volume, 0.95) 赋予近期成交量更高权重
  • 信号平滑:衰减操作比简单Moving Average更平滑

4. Horizon对齐(Label Shift)

将未来的Label对齐到当前时刻,这是量化特征工程的核心操作:

详细原理将在文档2中展开。

时间因果关系

Qlib强制你把”时间因果关系”写进特征定义,而不是交给模型猜。

错误示例(交给模型猜):

# 直接把原始价格扔给模型,让模型自己学时间关系
feature = $close
model.fit(feature, label)

正确示例(显式时间结构):

# 显式定义时间结构:20日均线、5日动量
feature1 = Mean($close, 20)       # 20日均线
feature2 = ($close / Ref($close, 5)) - 1  # 5日动量
model.fit([feature1, feature2], label)

在链上高频/套利中的重要性

在链上高频交易和套利中,时间结构工程尤为重要:

场景1:MEV套利

链上交易存在时序依赖关系:

  • 用户提交交易 → 矿工看到交易 → 矿工插入MEV交易

特征工程需要显式建模这个时序:

# 链上时间结构特征
mempool_latency = block_timestamp - tx_timestamp  # 内存池等待时间
gas_price_momentum = (gas_price - Ref(gas_price, 1)) / Ref(gas_price, 1)

场景2:套利机会识别

DEX间套利需要识别价格时序模式:

# DEX A和B的价格时序关系
price_diff_momentum = (diff_A_t - diff_A_{t-1}) / diff_A_{t-1}

这些时间结构特征必须显式定义,而不是让模型从原始价格序列中自动学习。


3.4 Layer 4:横截面层(Cross-sectional Engineering)

定义

横截面层关注同一时刻不同资产之间的关系。这是很多非量化出身的人最容易忽略的层次,但却是量化投资的核心差异所在。

核心思想

你不是在预测价格,而是在预测”同一时刻资产之间的相对强弱”。

常见操作

Qlib在横截面层提供了以下操作:

1. Z-score标准化(去量纲化)

将同一时刻的所有资产值标准化到标准正态分布:

其中:

  • 时刻的横截面均值
  • 是标准差

应用场景

将不同单位的因子拉到同一量纲:

  • PE(倍数)和ROE(百分比)无法直接相加
  • 标准化后,两者都变成”偏离均值的标准差倍数”
  • 可以线性组合:factor = 0.6 * z_pe + 0.4 * z_roe

示例

假设 时刻有3只股票的PE值:

  • 股票A:PE = 30
  • 股票B:PE = 20
  • 股票C:PE = 10

计算:

2. Rank(排序标准化)

将因子值换成排序位置,再缩放到

其中 中的排序位置(从1开始)。

应用场景

彻底杀掉异常值

  • 某股票的PE可能是10000倍(数据噪声)
  • 在Rank眼里,它仅仅是第一名
  • 只在乎”谁比谁强”,不在乎强多少

示例

假设3只股票的PE值:[30, 20, 10000]

Z-score标准化:

  • 10000的股票对z-score影响很大

Rank标准化:

  • 排序:[2, 1, 3]
  • 标准化:[0.5, 0, 1]
  • 10000的股票只是第一名,对其他股票无影响

3. Neutralization(中性化/去相关)

做回归,取残差,剔除行业、市值等”作弊因素”:

残差 是中性化后的因子,表示”剔除风格因子后的纯Alpha”。

应用场景

行业中性化

  • 如果一个股票涨是因为整个白酒板块都在涨,这不叫你的因子厉害
  • 中性化后,剩下的才是这只股票超出所属行业的”纯粹特质”

市值中性化

  • 小盘股弹性大,天然涨得多
  • 中性化后,剔除市值效应
  • 比较的是”同等市值下的相对强弱”

示例

假设有3只股票:

  • 股票A:行业=白酒,市值=100亿,因子值=10
  • 股票B:行业=白酒,市值=100亿,因子值=8
  • 股票C:行业=科技,市值=50亿,因子值=9

如果不中性化:

  • 股票A表现最好(因子值=10)

行业中性化后(剔除行业效应):

  • 在白酒行业内部,A比B强(10 > 8)
  • 残差:A=+1, B=-1, C=0

市值中性化后(再剔除市值效应):

  • C是小盘股(50亿),有市值溢价
  • 中性化后,C的因子值会下降

横截面处理的意义

通过横截面标准化和中性化,你的思维模型发生了进化:

  1. 承认无知:承认自己无法准确预测宏观经济和大盘波动(Beta)
  2. 寻找秩序:相信即便在乱世或盛世,资产之间总有”好坏之分”
  3. 纯化信号:把那些”搭便车”的收益(行业、市值、大盘涨跌)全部扔掉,只捕捉那一点点代表公司真正竞争力的纯Alpha

在DeFi世界中的类比

在DeFi世界中,横截面处理同样重要:

同一区块里:哪个池子更”异常”

  • 比较同一区块内所有DEX池子的交易量、价格变化
  • 找出显著偏离平均水平的池子

同一时间窗内:哪个Token行为偏离更多

  • 比较同一小时内所有Token的价格波动、成交量变化
  • 找出表现异常的Token

横截面不是数学技巧,而是一种投资策略

我们不赌国运涨跌,我们只赌”优胜劣汰”。


4. 解决的三个核心问题

Qlib特征工程的设计旨在解决量化投资中的三个核心问题。让我们逐一深入分析。

4.1 问题一:如何定义因子(表达式层)

问题描述

在量化投资中,“因子”是一个高度抽象的概念。传统做法是:

  • 用Python/Pandas手动计算因子
  • 因子定义散落在不同脚本中
  • 难以复现和验证

这种做法存在三个问题:

  1. 可读性差:复杂的因子计算逻辑难以理解
  2. 可维护性差:修改因子需要改动多处代码
  3. 可复现性差:不同人对因子的理解可能不一致

Qlib的解决方案

Qlib引入表达式系统,将因子定义标准化:

核心思想

因子 = 表达式 = 算子的组合

表达式系统的优势

优势1:声明式定义

# 声明式:清晰表达意图
factor = ($close - Mean($close, 20)) / Std($close, 20)
 
# vs 命令式:实现细节混乱
def compute_factor(close):
    ma20 = close.rolling(20).mean()
    std20 = close.rolling(20).std()
    return (close - ma20) / std20

优势2:可组合性

表达式可以任意组合,形成复杂的因子:

# 基础因子
ma20 = Mean($close, 20)
vol20 = Std($close, 20)
boll = ($close - ma20) / vol20
 
# 组合因子
momentum_boll = boll * (($close / Ref($close, 5)) - 1)

优势3:自动优化

表达式系统可以自动优化计算:

  • 公共子表达式消除
  • 增量计算(避免重复计算Rolling)
  • 并行化执行

算子语义约束

Qlib的算子有严格的语义约束,确保:

约束1:因果性(Causality)

所有算子只能使用历史数据:

例如:

  • Ref($close, 1) ✓ 使用 时刻数据
  • Ref($close, -1) ✗ 使用 时刻数据(未来函数)

约束2:时间一致性(Temporal Consistency)

算子的输出时间戳与输入一致:

例如:

  • Mean($close, 20) 输出的是 时刻的均值(使用 的数据)
  • 输出时间戳仍然是 ,不是

约束3:可审计性(Auditability)

每个算子的计算过程可以追溯:

  • 输入数据:明确指定
  • 计算过程:语义清晰
  • 输出结果:可验证

4.2 问题二:如何对齐时间 & 横截面(金融时序的本质难点)

问题描述

金融时序数据的对齐是特征工程中最本质、最困难的问题,包含两个维度:

维度1:时间对齐(Temporal Alignment)

如何将”当前的因子”与”未来的收益”对齐?

场景示例

你有:

  • 时刻的因子:Factor[t] = 1.5
  • 时刻的收益:Return[t→t+5] = 5%

问题是:如何把它们放在同一行数据中训练模型?

错误对齐

Row_t: {Factor[t], Return[t]}  # Return[t]是t时刻收益,已实现

这会导致Look-ahead Bias,因为Return[t]在t时刻还未发生。

正确对齐

Row_t: {Factor[t], Return[t→t+5]}  # Return[t→t+5]是未来收益

维度2:横截面对齐(Cross-Sectional Alignment)

如何将不同时间频率、不同覆盖范围的数据对齐到同一时刻?

场景示例

  • 行情数据:每日(频率=1天,覆盖=全市场5000只股票)
  • 财务数据:季度(频率=1季度,覆盖=部分股票)
  • 行业数据:月度(频率=1月,覆盖=全市场)

如何把它们对齐到 时刻?

Qlib的解决方案

时间对齐:Horizon Shift

Qlib使用Label Shift操作将未来的Label对齐到当前时刻:

详细原理见文档2。

横截面对齐:统一时间网格

Qlib将所有数据对齐到统一的时间网格:

time_grid = [t_1, t_2, ..., t_T]
 
# 行情数据:已有,直接映射
price[t] = get_price(t)
 
# 财务数据:前向填充
financial[t] = latest_financial_data_before(t)
 
# 行业数据:对齐到最近月份
industry[t] = industry_data(round_down_to_month(t))

横截面对齐的挑战

挑战1:频率不一致

不同数据源的频率不同:

  • 行情数据:日频
  • 财务数据:季频
  • 宏观数据:月频

解决方案:降采样/升采样

  • 高频 → 低频:取最后值、均值、聚合
  • 低频 → 高频:前向填充

挑战2:覆盖范围不一致

不同数据源的覆盖范围不同:

  • 行情数据:覆盖全市场
  • 财务数据:覆盖部分股票(停牌、退市等)
  • 行业数据:覆盖全市场

解决方案:缺失值处理

  • 删除缺失值过多的时刻
  • 用行业均值填充
  • 用历史均值填充

挑战3:时间偏差

某些数据存在发布延迟:

  • 财报数据:公布日期晚于财务期间
  • 宏观数据:公布日期晚于统计期间

解决方案

  • 使用公布日期作为有效日期
  • 对于预测,只能使用公布前的数据

4.3 问题三:如何避免数据泄漏 + 保证可回测性

问题描述

数据泄漏是量化回测中最致命的问题,会导致:

  • 回测收益虚高
  • 实盘表现惨淡
  • 策略完全失效

常见的数据泄漏场景

场景1:未来信息泄露

在计算 时刻的因子时,不小心用到了 时刻的数据。

错误示例

# 计算t时刻的波动率,使用了未来数据
volatility[t] = std(price[t-10:t+10])  # 包含t+1到t+10的数据

正确示例

# 只使用历史数据
volatility[t] = std(price[t-20:t])  # 只使用t-20到t的数据

场景2:样本外信息混入

在训练模型时,使用了测试集的信息。

错误示例

# 在整个数据集上标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 使用了全部数据

正确示例

# 只在训练集上标准化,然后应用到测试集
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

场景3:Look-ahead Bias

在回测时,使用了未来才能知道的信息。

错误示例

# 回测时,知道t时刻的收盘价,然后决定是否买入
if price[t] > ma[t]:
    buy()  # 但实际上,t时刻你不知道price[t]

正确示例

# 回测时,只能使用t-1时刻的信息
if price[t-1] > ma[t-1]:
    buy()  # t-1时刻的收盘价,t时刻开盘前决定

Qlib的防护体系

Qlib通过多层防护机制避免数据泄漏:

防护1:算子级约束

所有算子强制只能使用历史数据:

系统会在算子定义时检查,违反约束的算子无法注册。

防护2:表达式级验证

表达式系统会自动验证因果性:

# 正确表达式
factor = Mean($close, 20)  # 只使用t-19到t的数据
 
# 错误表达式(系统会拒绝)
factor = Mean(Ref($close, -10), 20)  # 包含未来数据

防护3:时间戳追踪

Qlib会追踪每个因子的时间戳,确保时间对齐正确:

# 每个因子都有时间戳
factor = Mean($close, 20)
# factor的时间戳 = t(使用t-19到t的数据)
 
label = Ref($close, -5) / $close - 1
# label的时间戳 = t(使用t到t+5的数据)

防护4:回测一致性检查

Qlib提供回测一致性检查工具,检测潜在的数据泄漏:

# 检查因子是否与未来价格相关
check_lookahead_bias(factor, price)
 
# 检查IC是否异常高
check_ic_validity(ic_series)

防护5:审计日志

Qlib会记录每个因子的计算过程,便于审计:

[2024-01-01] Factor: ma20
  Input: $close
  Operation: Mean($close, 20)
  Time range: [2023-12-10, 2024-01-01]
  Causality check: ✓ Passed

回测一致性的定义

回测一致性指:历史回测的表现能够外推到实盘。

如何保证回测一致性

Qlib通过以下机制保证:

机制1:严格时序划分

严格划分训练集、验证集、测试集,确保时间顺序:

Train: [t_1, t_2]
Validate: [t_2, t_3]
Test: [t_3, t_4]

机制2:增量学习

模型在每个时间点只使用历史数据学习,然后预测未来:

for t in time_grid:
    # 使用t之前的数据训练
    model.fit(X[:t], y[:t])
 
    # 预测t之后的收益
    pred[t] = model.predict(X[t:t+horizon])

机制3:样本外验证

严格使用样本外数据验证,绝不使用样本内数据调参:

# 错误:使用全部数据调参
best_params = grid_search(X, y, param_grid)
 
# 正确:只在训练集上调参
best_params = grid_search(X_train, y_train, param_grid)

机制4:前瞻性预测

预测未来的收益,而不是拟合过去的收益:

# 正确:预测未来收益
pred = model.predict(current_features)
 
# 错误:拟合过去收益
pred = model.predict(historical_features)  # 这是过拟合

5. 适用场景与局限性

5.1 适用场景清单

Qlib特征工程在以下场景中表现优异:

场景1:多因子选股

这是Qlib最核心的应用场景。多因子选股的核心思想是:

  • 构建多个因子(动量、价值、质量、波动等)
  • 通过因子组合选出表现最好的股票
  • 定期调仓(如每月、每季度)

Qlib的优势

  • 表达式系统快速定义和测试因子
  • 横截面标准化和中性化自动处理风格因子
  • IC/IR评估系统量化因子质量

示例

# 定义5个因子
factors = {
    'momentum': ($close / Ref($close, 20)) - 1,
    'value': 1 / $pe_ratio,
    'quality': $roe / $debt_ratio,
    'volatility': Std($return, 20),
    'liquidity': $turnover_rate
}
 
# 横截面标准化
factors_z = zscore_cross_sectional(factors)
 
# 中性化
factors_neu = neutralize(factors_z, industry, market_cap)
 
# 组合
composite_factor = 0.3 * factors_neu['momentum'] + \
                   0.3 * factors_neu['value'] + \
                   0.2 * factors_neu['quality'] + \
                   0.1 * factors_neu['volatility'] + \
                   0.1 * factors_neu['liquidity']
 
# 选股(Top 10%)
selected = rank(composite_factor) > 0.9

场景2:CTA策略因子构建

CTA(Commodity Trading Advisor)策略主要基于趋势跟踪,Qlib的表达式系统非常适合构建趋势因子。

Qlib的优势

  • 表达式系统方便定义趋势指标(MA、MACD、RSI等)
  • 时间结构层支持Rolling、Lag、Decay等操作
  • 可以快速测试不同参数的稳健性

示例

# 趋势因子
trend_factor = ($close - Mean($close, 60)) / Mean($close, 60)
 
# 动量因子
momentum_factor = ($close / Ref($close, 20)) - 1
 
# 波动率因子
volatility_factor = Std($return, 20)
 
# 组合
cta_factor = 0.5 * trend_factor + 0.3 * momentum_factor - 0.2 * volatility_factor
 
# 交易信号
long_signal = cta_factor > threshold
short_signal = cta_factor < -threshold

场景3:因子库管理

对于量化团队,管理数百个因子是一个挑战。Qlib提供了系统的因子库管理方案。

Qlib的优势

  • 表达式系统统一因子定义
  • 因子评估系统(IC/IR/衰减周期)
  • 因子组合和去重
  • 因子监控和预警

示例

# 因子库
factor_library = {
    'ma_cross': Mean($close, 5) / Mean($close, 20) - 1,
    'rsi': RSI($close, 14),
    'boll_width': (Mean($close, 20) + 2*Std($close, 20) - \
                   (Mean($close, 20) - 2*Std($close, 20))) / Mean($close, 20),
    # ... 更多因子
}
 
# 批量评估
factor_performance = {}
for name, expr in factor_library.items():
    ic_series = compute_ic(expr)
    factor_performance[name] = {
        'ic_mean': ic_series.mean(),
        'ic_std': ic_series.std(),
        'ir': ic_series.mean() / ic_series.std()
    }
 
# 筛选优质因子
good_factors = [name for name, perf in factor_performance.items()
                if perf['ir'] > 0.7]

场景4:学术研究

Qlib也适合学术界进行量化研究,特别是因子挖掘和资产定价研究。

Qlib的优势

  • 开源免费,可复现性强
  • 表达式系统灵活,便于实验
  • 内置常用因子库
  • 支持自定义算子

场景5:链上数据分析

随着DeFi和Web3的发展,链上数据量激增,Qlib的特征工程框架也可以用于链上数据分析。

Qlib的优势

  • 表达式系统可以定义链上行为因子
  • 时间结构层支持高频数据分析
  • 横截面层可以比较不同Token/协议的表现

详见文档5。


5.2 局限性与替代方案

局限性1:不适合纯深度学习端到端

Qlib的特征工程基于人工设计的因子,如果使用深度学习进行端到端学习,Qlib可能不是最优选择。

原因

  • 深度学习(如Transformer、LSTM)可以从原始序列中自动提取特征
  • Qlib的因子定义可能限制了模型的学习能力
  • 深度学习更适合高频、微观结构数据

替代方案

  • 使用PyTorch/TensorFlow搭建端到端深度学习模型
  • 使用专业的深度学习量化框架(如DeepQuant、FinRL)

局限性2:高频微秒级策略

对于微秒级的高频策略,Qlib的性能可能不够。

原因

  • Qlib基于表达式系统,计算开销较大
  • 没有针对实时流数据的优化
  • 横截面操作在N很大时性能瓶颈明显

替代方案

  • 使用C++编写的高频交易系统
  • 使用专业的实时数据流处理框架(如Flink、Kafka Streams)

局限性3:复杂结构数据

Qlib主要处理时序数据,对于复杂结构数据(如图谱、文本、图像)支持有限。

原因

  • Qlib的数据结构是[时间, 资产, 因子]的三维张量
  • 不支持图谱、树、图等结构数据
  • 没有内置自然语言处理或计算机视觉模块

替代方案

  • 图谱数据:使用图神经网络(GNN)
  • 文本数据:使用NLP模型(如BERT、GPT)
  • 图像数据:使用CNN或Vision Transformer

局限性4:实时流数据处理

Qlib的设计主要是批处理,对于实时流数据支持不足。

原因

  • Qlib的计算图是静态的,不支持动态更新
  • 没有内置流式处理机制
  • 横截面操作需要等待所有数据到达

替代方案

  • 使用流式处理框架(如Apache Flink、Apache Spark Streaming)
  • 使用专业的实时数据平台(如Kafka、Pulsar)

局限性5:多市场、多资产类别

Qlib主要针对单一市场(如A股),对于跨市场、多资产类别的场景支持有限。

原因

  • 不同市场的交易时间、币种、规则不同
  • 多资产类别(股票、债券、期货、期权、加密货币)的数据结构差异大
  • 横截面操作需要处理不同资产类别的差异

替代方案

  • 使用专门的多资产管理系统(如Bloomberg、Wind)
  • 自研多市场数据平台

总结

Qlib特征工程是一个专为量化投资设计的系统化框架,它通过四层架构解决了金融时序数据的三大核心挑战:非平稳性、自相关性和低信噪比。

核心要点回顾

  1. 四层架构:原始字段层 → 表达式层 → 时间结构层 → 横截面层
  2. Pipeline形式化
  3. 三大核心问题:因子定义、时间&横截面对齐、数据泄漏防护
  4. 适用场景:多因子选股、CTA策略、因子库管理、学术研究、链上数据分析
  5. 局限性:不适合端到端深度学习、微秒级高频、复杂结构数据、实时流处理、多市场

Qlib的独特价值

Qlib不是简单的特征工程库,而是一个可审计的计算图系统,它强制你在设计特征时就考虑因果性、时序对齐和回测一致性,从而避免量化研究中最致命的”未来函数”陷阱。

在下一文档中,我们将深入探讨Qlib特征工程中最核心的操作:Horizon对齐(Label Shift),这是量化投资中”消除未来函数并建立因果预测关系”的关键技术。