从玄学到科学:量化择时中的机器学习特征工程实践

本文面向具备一定经验的工程师与技术负责人,旨在深入探讨机器学习在金融量化择时领域的应用核心——特征工程。我们将跳过基础概念,直击金融时间序列的非平稳性、低信噪比等核心痛症,从第一性原理出发,剖析如何构建具备预测能力的特征,并最终将其工程化、系统化。本文的核心观点是:在量化交易中,模型(如 LightGBM、Transformer)是标准化的武器,而真正构成核心壁垒的,是对数据和市场的深度理解,并将其转化为稳定、有效的特征(Alpha)的能力。

现象与问题背景

任何一个尝试将机器学习应用于金融市场的团队,都会迅速遇到一个共同的困境:模型的“失效”。一个在历史数据上回测曲线优美如画的策略,上线实盘后表现却一塌糊涂,甚至不如简单的买入持有策略。这种现象背后,并非模型不够强大,而是数据——作为模型输入的特征——存在根本性缺陷。这通常表现为以下几个方面:

  • Alpha 衰减(Alpha Decay):市场是自适应的。一个有效的信号(Alpha)一旦被发现和利用,其有效性会随之降低。依赖于简单技术指标(如移动均线、RSI)的策略在如今由算法和高频交易主导的市场中,早已沦为噪音。
  • 极低的信噪比:金融时间序列数据本质上是随机游走叠加微弱信号。大部分价格波动是市场情绪、宏观事件、流动性冲击等因素共同作用下的“噪音”,真正可供模型学习的“信号”极其微弱。
  • 非平稳性(Non-stationarity):这是金融数据最棘手的特性。数据的统计属性(均值、方差)随时间动态变化。一个在2018年牛市中有效的特征,在2022年的熊市中可能完全失效甚至产生负向预测。机器学习模型的基本假设之一就是数据分布的稳定性,非平稳性直接破坏了这一根基。
  • 伪相关与过拟合:在海量数据和特征维度下,模型极易找到一些偶然的、在历史数据中成立但在未来无效的“伪相关性”。这导致了严重的过拟合问题,也是回测与实盘表现脱节的直接原因。

因此,量化择时的战场,早已从寻找“圣杯模型”转移到了特征工程的“军备竞赛”。我们的目标,就是从充满噪音的原始数据中,设计、提取并验证那些能够穿越市场周期、具备统计鲁棒性的信号。

关键原理拆解

在进入具体实现前,我们必须回归到几个计算机科学与统计学的基础原理。这决定了我们后续所有工程实践的理论基石。

第一原理:平稳性(Stationarity)

一个时间序列如果其统计特性(如均值、方差、自相关性)不随时间推移而改变,则称其为平稳的。机器学习模型,尤其是那些基于线性假设或距离度量的模型,在平稳数据上的表现远胜于非平稳数据。价格序列(Price)本身是典型的非平稳序列(因为它有长期趋势),而收益率序列(Return)则更接近平稳。这是为什么几乎所有量化模型都使用收益率而非价格作为输入。更进一步,我们可以通过分数阶差分(Fractional Differentiation)等手段,在尽可能保留原始序列“记忆”的同时,将其处理为平稳序列。这是一种在降噪和保留信息之间取得精妙平衡的数学工具。

第二原理:信息论(Information Theory)

如何衡量一个特征的“好坏”?信息论提供了严谨的数学框架。一个好的特征,应该与我们希望预测的目标(未来的收益)有尽可能高的互信息(Mutual Information),同时,一组好的特征之间,应该有较低的互信息以避免冗余。互信息可以捕捉线性和非线性的关系,远比简单的相关系数更强大。在特征选择阶段,我们可以计算每个候选特征与目标标签的互信息分数,并优先选择分数高的特征,这是一种纯粹基于数据分布的、不依赖于任何特定模型的过滤方法。

第三原理:维度灾难(Curse of Dimensionality)

随着特征数量(维度)的增加,特征空间的体积会指数级增长。这导致样本数据在特征空间中变得极其稀疏,任意两点之间的距离都会很大,使得基于距离或密度的学习算法(如 K-NN、SVM)难以有效工作。即使是树模型,在高维空间中也更容易找到伪相关的分割点,导致过拟合。因此,特征并非越多越好。我们需要一套系统化的方法来进行特征选择(Feature Selection)和维度约减(Dimensionality Reduction),确保输入模型的都是“精兵强将”。

系统架构总览

一个工业级的机器学习择时系统,其核心是一套围绕数据和特征展开的流水线架构。它并非一个单一的程序,而是一个由多个解耦的服务组成的有机体。我们可以将其抽象为以下几个核心阶段:

  • 数据接入层(Data Ingestion):负责从各种数据源(交易所API、数据供应商FTP、消息队列)接入原始数据,包括L1行情(OHLCV)、L2深度订单簿(Depth of Book)、逐笔成交(Trade/Tick)等。这一层要求高可用和低延迟,原始数据需要被清洗、校验并存储到时序数据库(如 InfluxDB、DolphinDB)或分布式文件系统(如 HDFS、S3)中。
  • 特征生成引擎(Feature Engine):这是系统的“兵工厂”。它订阅原始数据,根据预定义的特征计算逻辑,实时或批量地生成特征。这一层通常是计算密集型的,需要考虑水平扩展。生成的特征被存储在专门的特征存储(Feature Store)中,供下游的训练和预测使用。特征存储是关键,它保证了训练和推理时特征的一致性,避免了线上线下数据口径不一的“天坑”。
  • 数据标注模块(Labeling Module):负责为历史数据生成用于监督学习的“答案”(Label)。如何定义“未来是涨还是跌”远比听起来要复杂。一个好的标注方法是策略成功的一半,我们将在下文详述。
  • 模型训练与验证平台(Training & Validation Platform):该平台拉取特征和标签数据,执行模型训练、超参数调优和严格的回测验证。核心是实现时间序列交叉验证(如 Purged K-Fold Cross-Validation)来模拟真实交易环境,有效评估并抑制过拟合。
  • 在线预测与交易执行(Inference & Execution):将验证通过的模型部署为在线服务。该服务订阅实时特征,输出预测信号(如买入/卖出/持有的概率),并由交易执行模块转化为真实的订单,与交易所进行交互。

整个系统是一个闭环。实盘的交易结果、模型的表现,都会被记录下来,作为数据反哺到特征生成和模型迭代的环节中,形成持续优化的飞轮。

核心模块设计与实现

理论是灰色的,生命之树常青。让我们深入到代码层面,看看关键模块如何实现。

数据标注:定义“未来”的 Triple Barrier Method

最简单的标注方法是固定时间窗口:未来 N 根K线的收益率大于某个阈值则标记为 1(上涨),小于则为 -1(下跌),否则为 0(盘整)。这种方法最大的问题是忽略了波动率。在剧烈波动的市场中,N 根K线可能已经走完了数轮涨跌;在平稳市场中,可能纹丝不动。De Prado 提出的三重障碍法(Triple Barrier Method)是一个更为先进和鲁棒的方案。

其核心思想是,为每一笔潜在的交易设置三个“障碍”:

  1. 上轨(Profit-Taking):价格上涨到某个幅度,触发止盈。
  2. 下轨(Stop-Loss):价格下跌到某个幅度,触发止损。
  3. 时间障碍(Time Barrier):在预设的时间窗口内,如果价格未触及上下轨,则交易结束。

标签不再是简单的涨跌,而是这笔交易最终触及哪个障碍。上下轨的幅度可以基于历史波动率动态调整,从而适应不同市场环境。这使得标签的含义更加贴近真实的交易行为。


# 
import pandas as pd
import numpy as np

def get_daily_vol(close, lookback=100):
    # 计算每日收益率的波动率,用于动态设置障碍宽度
    df0 = close.pct_change()
    df0 = df0.rolling(lookback).std().dropna()
    return df0

def apply_triple_barrier(close, events, pt_sl, molecule):
    # close: Series of prices
    # events: DataFrame with columns 't1' (time barrier) and 'trgt' (volatility target)
    # pt_sl: [profit_take_multiplier, stop_loss_multiplier]
    # molecule: subset of event start times to process
    
    out = events[['t1']].copy(deep=True)
    if pt_sl[0] > 0:
        pt = pt_sl[0] * events['trgt']
    else:
        pt = pd.Series(index=events.index) # No upper barrier
    
    if pt_sl[1] > 0:
        sl = -pt_sl[1] * events['trgt']
    else:
        sl = pd.Series(index=events.index) # No lower barrier
        
    for loc, t1 in events.loc[molecule, 't1'].fillna(close.index[-1]).iteritems():
        df0 = close[loc:t1] # path of prices
        df0 = (df0 / close[loc] - 1) * (1 if events.loc[loc, 'side'] == 1 else -1) # path of returns
        
        out.loc[loc, 'stop_loss_hit_time'] = df0[df0 < sl[loc]].index.min()
        out.loc[loc, 'profit_take_hit_time'] = df0[df0 > pt[loc]].index.min()

    return out

def get_labels(barriers):
    # 根据触碰的第一个障碍生成标签
    # -1: 触碰止损, 1: 触碰止盈, 0: 触碰时间障碍
    out = pd.DataFrame(index=barriers.index)
    
    # 优先处理时间最快的触碰
    first_touch = barriers[['stop_loss_hit_time', 'profit_take_hit_time']].min(axis=1)
    
    # 如果时间障碍先到,则按期末收益率标记
    out['label'] = 0
    mask_pt = first_touch == barriers['profit_take_hit_time']
    out.loc[mask_pt, 'label'] = 1
    mask_sl = first_touch == barriers['stop_loss_hit_time']
    out.loc[mask_sl, 'label'] = -1
    
    return out

# --- 使用示例 ---
# 假设我们有 close_prices (pd.Series) 和 entry_points (pd.DatetimeIndex)
# 1. 计算波动率
# daily_vol = get_daily_vol(close_prices)
# 2. 创建 events DataFrame
# events = pd.DataFrame(index=entry_points)
# events['t1'] = ... # 设置时间障碍,例如 entry_points + pd.Timedelta(days=5)
# events['trgt'] = daily_vol.loc[entry_points]
# events['side'] = 1 # 假设只做多
# 3. 计算障碍触碰时间
# barriers = apply_triple_barrier(close_prices, events, pt_sl=[1.5, 1.5], molecule=events.index)
# 4. 生成最终标签
# labels = get_labels(barriers)

特征工程:从数据中榨取 Alpha

特征的构建是一个创造性的过程,但可以遵循一些范式。以下是几个关键的特征类别:

  1. 基础价量特征:这是最基础的特征来源,但我们可以做得更精细。
    • 波动率特征:除了历史标准差,还可以使用 Garman-Klass、Parkinson 等对高低价更敏感的波动率估计器。波动率本身可以作为一个特征,高波动和低波动环境下的市场行为模式不同。
    • 动量/反转特征:不同时间尺度下的收益率、与移动均线的偏离度等。关键在于使用多个时间窗口(如5日、20日、60日)来捕捉不同周期的市场力量。
  2. 微观结构特征:如果能获取到高频数据(Tick Data),就能打开一个全新的特征维度。
    • 订单流不平衡(Order Flow Imbalance):通过分析逐笔成交数据,统计一段时间内主动买单和主动卖单的量差。这直接反映了市场参与者的意图和力量对比,是极强的短线预测因子。
    • 买卖价差(Bid-Ask Spread):价差的大小反映了市场的流动性。价差突然扩大可能预示着风险事件或流动性枯竭。
  3. 特征衍生与组合
    • 特征交叉:例如,将“波动率”和“动量”两个特征相乘。其背后逻辑是,在高波动环境下的强劲动量,可能比在低波动环境下的同样动量更具意义。
    • 与宏观经济数据结合:将市场数据与非农就业数据、CPI 等宏观指标结合,例如计算当前收益率在CPI发布日的条件概率分布。

特征选择:去伪存真

在产生了成百上千个候选特征后,必须进行严格的筛选。一个常用的高效方法是利用树模型(如 LightGBM、RandomForest)的副产品:特征重要性(Feature Importance)

在模型训练完成后,我们可以轻易获取每个特征对模型预测贡献的量化评分。这是一种嵌入式(Embedded)的特征选择方法,它考虑了特征之间的相互作用。


# 
import lightgbm as lgb
import pandas as pd
import matplotlib.pyplot as plt

# 假设 X_train 是特征矩阵, y_train 是标签
# model = lgb.LGBMClassifier(objective='multiclass', n_estimators=1000)
# model.fit(X_train, y_train, ...)

# 获取并可视化特征重要性
# feature_importances = pd.DataFrame({
#     'feature': X_train.columns,
#     'importance': model.feature_importances_
# }).sort_values('importance', ascending=False)

# print(feature_importances.head(20))

# 仅保留重要性排名前 N 的特征,或高于某个阈值的特征
# top_features = feature_importances[feature_importances['importance'] > threshold]['feature'].tolist()
# X_train_selected = X_train[top_features]
# X_test_selected = X_test[top_features]

# 然后用 X_train_selected, X_test_selected 重新训练和评估模型

通过这种方式,我们可以系统性地剔除掉那些贡献度低、可能引入噪音的特征,从而得到一个更精简、更鲁棒的模型。

性能优化与高可用设计

在量化领域,“性能”和“高可用”有其独特的含义。

对抗过拟合:最重要的一战

过拟合是量化策略的头号杀手。除了上文提到的特征选择,最关键的防御手段是采用正确的验证方法。标准的 K-Fold 交叉验证在时间序列数据上是错误的,因为它破坏了数据的时间顺序,导致“数据泄露”(用未来的数据来预测过去)。我们必须使用前向展开交叉验证(Walk-Forward Cross-Validation)或更优的带清洗的 K-Fold 交叉验证(Purged K-Fold Cross-Validation)

Purged K-Fold 的核心思想是:在划分训练集和验证集时,从训练集中“清洗掉”那些时间上紧邻验证集开始样本的数据。这是因为在三重障碍法标注中,一个样本的标签可能依赖于其后一段时间的数据,如果不做清洗,训练集的“知识”就泄露到了验证集中。

避免前视偏差(Look-ahead Bias)

这是一个极其隐蔽但致命的编码错误。例如,在计算 10 日移动平均线时,错误的代码可能会在 `t` 时刻用到了 `t` 时刻的收盘价来生成特征,而 `t` 时刻的收盘价在 `t` 时刻开盘时是未知的。正确的做法是,`t` 时刻能用的所有数据,必须在 `t` 时刻的决策点之前是完全可得的。在 Pandas 中,这意味着在计算完 `rolling` 后,通常需要跟一个 `.shift(1)`。


# 
# 错误的方式:在 t 时刻使用了 t 时刻的收盘价
# df['ma_10_wrong'] = df['close'].rolling(10).mean()

# 正确的方式:在 t+1 时刻决策时,使用的是 t 时刻及之前的收盘价计算的均线
# df['ma_10_correct'] = df['close'].rolling(10).mean().shift(1)

这种看似微小的差异,是区分专业量化代码和业余脚本的关键。所有特征的计算都必须经过严格的审查,确保没有引入未来信息。

架构演进与落地路径

对于一个希望从零开始构建机器学习择时策略的团队,不可能一步到位。一个务实的演进路径如下:

  1. 阶段一:原型验证(MVP)
    • 数据源:从日线或小时线级别的 OHLCV 数据开始,数据量可控,处理简单。
    • 特征:实现 10-20 个经典的、公开的技术指标,如各类移动均线、布林带、RSI、MACD 等。
    • 标注:使用简单的固定时间窗口法。
    • 模型:使用 LightGBM 或 XGBoost,它们对特征的容忍度高,效果稳健。
    • 目标:打通从数据、特征、标注、训练到回测的完整流程。验证整个 pipeline 的正确性,而不是追求策略的盈利能力。
  2. 阶段二:Alpha 挖掘
    • 数据源:引入更高频的数据,如分钟线,甚至 Tick 数据。
    • 特征:深入挖掘微观结构特征、统计特征,并开始尝试特征组合。构建至少 100+ 的候选特征库。
    • 标注:实现并切换到 Triple Barrier Method。
    • 验证:引入 Purged K-Fold CV 和严格的特征重要性筛选流程。
    • 目标:在回测中找到至少一个或一组具备统计显著性正收益的策略原型。
  3. 阶段三:工业化与扩展
    • 系统化:构建独立的特征存储(Feature Store),实现特征的线上线下一致性,并支持特征版本管理和复用。
    • 自动化:建立自动化的模型训练、评估和部署流水线(MLOps)。监控线上模型的表现,并设置衰减预警,触发自动重训。

    • 多元化:探索另类数据源(如新闻情绪、链上数据、卫星图像等),并研究更前沿的模型(如 Transformer 在时间序列中的应用)。
    • 目标:建立一个可持续产出、迭代和管理多个 Alpha 策略的工业级系统。

总而言之,机器学习在量化择时中的应用是一项系统工程,它融合了金融学、统计学和计算机科学。在这条道路上,没有一劳永逸的“圣杯”。真正的壁垒在于对数据的深刻理解,对市场噪音的敬畏,以及通过严谨的工程实践,将微弱的信号放大为持续、稳定的超额收益的能力。

延伸阅读与相关资源

  • 想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
    交易系统整体解决方案
  • 如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
    产品与服务
    中关于交易系统搭建与定制开发的介绍。
  • 需要针对现有架构做评估、重构或从零规划,可以通过
    联系我们
    和架构顾问沟通细节,获取定制化的技术方案建议。
滚动至顶部