跳转至

第 12 章:机器学习在量化中的应用 (Machine Learning)

随着人工智能的飞速发展,机器学习 (Machine Learning, ML) 已成为量化投资领域不可或缺的工具。与传统的基于固定逻辑(如双均线)的策略不同,ML 策略通过数据驱动的方式,自动从海量历史数据中挖掘非线性规律。

本章将结合《Advances in Financial Machine Learning》(Marcos Lopez de Prado) 的核心思想,探讨如何在 AKQuant 中构建科学的 ML 策略。

本章实践入口

快速运行与验收

python examples/textbook/ch12_ml.py

验收要点:

  1. 脚本可完成训练、预测与回测评估流程。
  2. 输出包含至少一个分类或回归性能指标与回测指标。
  3. 更换特征窗口后,模型表现变化具备统计解释。

12.1 机器学习范式 (Paradigms)

12.1.1 监督学习 (Supervised Learning)

最常见的范式。通过历史数据 \((X, y)\) 训练模型,预测未来的 \(y\)

  • 分类 (Classification):预测方向(涨/跌)。常用算法:逻辑回归、SVM、随机森林、XGBoost、LightGBM。
  • 回归 (Regression):预测具体数值(收益率、波动率)。常用算法:线性回归、Lasso、Ridge、神经网络 (LSTM/Transformer)。

12.1.2 非监督学习 (Unsupervised Learning)

没有标签 \(y\),仅探索数据 \(X\) 的内在结构。

  • 聚类 (Clustering):如 K-Means,用于股票风格分类、配对交易中的标的筛选。
  • 降维 (Dimensionality Reduction):如 PCA,用于提取主成分因子,解决多重共线性问题。

12.1.3 强化学习 (Reinforcement Learning)

智能体 (Agent) 在环境 (Environment) 中通过试错 (Trial-and-Error) 学习策略。

  • 动作 (Action):买入、卖出、持有、调整仓位比例。
  • 奖励 (Reward):盈亏 (PnL)、夏普比率、Sortino 比率。

12.2 金融特征工程 (Financial Feature Engineering)

特征 (Features) 决定了模型的上限。金融数据具有高噪声、非平稳的特点,直接使用原始价格通常效果不佳。

12.2.1 常用特征构造

  1. 动量类 (Momentum)
    • ROC (Rate of Change): 过去 N 日收益率。
    • Bias: 价格相对于均线的偏离度。
    • RSI/MACD: 传统技术指标的数值化。
  2. 波动类 (Volatility)
    • ATR (Average True Range): 真实波幅。
    • StdDev: 滚动标准差。
    • Bollinger Width: 布林带宽度。
  3. 成交量类 (Volume)
    • Turnover Rate: 换手率。
    • Volume Ratio: 量比。
    • OBV: 能量潮。
  4. 微观结构 (Microstructure)
    • Order Imbalance: 买卖盘口失衡(需 L2 数据)。
    • VPIN: 知情交易概率。

12.2.2 高级特征处理

1. 分数阶差分 (Fractional Differentiation)

传统的一阶差分(收益率)虽然平稳,但丢失了所有的价格记忆(Memory)。De Prado 提出分数阶差分(如 0.4 阶差分),可以在保留记忆实现平稳之间找到最佳平衡点。

\[ (1-L)^d X_t = \sum_{k=0}^{\infty} \omega_k X_{t-k} \]

2. 结构化数据重采样 (Structural Bar)

不按固定的时间(分钟/日)采样,而是按信息量采样:

  • Tick Bar: 每 N 笔交易生成一根 K 线。
  • Volume Bar: 每成交 N 手生成一根 K 线。
  • Dollar Bar: 每成交 N 金额生成一根 K 线(机构常用,性质最稳定)。

12.3 标签生成 (Labeling)

如何定义“正确的预测”?这是金融 ML 中最容易被忽视的环节。

12.3.1 固定时间窗口法 (Fixed-Time Horizon)

最简单的方法:

\[ y_t = \text{sign}(P_{t+h} - P_t) \]

预测 \(h\) 个周期后的涨跌。

  • 缺点:忽略了期间的波动。可能 \(t+h\) 时微涨,但中间经历了 50% 的回撤,这种样本标记为“正样本”是危险的。

12.3.2 三重屏障法 (Triple Barrier Method)

由 De Prado 提出,更符合实战逻辑。设置三个触碰屏障:

  1. 上轨 (Upper Barrier):止盈线 (Profit Taking)。触碰标记为 1。
  2. 下轨 (Lower Barrier):止损线 (Stop Loss)。触碰标记为 -1。
  3. 垂直轨 (Vertical Barrier):时间期限 (Time Limit)。触碰标记为 0(或根据当时收益标记)。

这种方法的本质是预测:在止损或到期之前,能否先止盈?

12.3.3 元标签 (Meta-Labeling)

这是一种二次模型思想:

  1. 初级模型 (Primary Model):决定方向(买/卖)。可以是简单的均线策略。
  2. 次级模型 (Secondary Model):决定是否采纳初级模型的建议。输入是初级模型的信号和市场特征,输出是二分类(交易/不交易)。

Meta-Labeling 可以显著提高策略的胜率和夏普比率。

12.4 模型验证 (Model Validation)

12.4.1 交叉验证的陷阱

在传统的机器学习任务(如图像分类)中,我们可以放心地使用 K-Fold 交叉验证,因为样本是独立同分布(I.I.D.)的。但在金融时间序列中,样本之间存在极强的自相关性 (Serial Correlation)

例如,如果你使用过去 5 天的收益率作为特征,并预测未来 5 天的收益率。那么,\(t\) 时刻的标签 \(y_t\)(依赖于 \(P_{t+5}\))和 \(t+1\) 时刻的标签 \(y_{t+1}\)(依赖于 \(P_{t+6}\))在时间跨度上有 4 天的重叠。

如果随机打乱数据进行 K-Fold:

  1. 训练集中可能包含 \(t\) 时刻的样本。
  2. 测试集中可能包含 \(t+1\) 时刻的样本。
  3. 由于 \(y_t\)\(y_{t+1}\) 高度相关,模型实际上是在利用训练集的信息“偷看”测试集的答案,导致过拟合虚高的夏普比率

12.4.2 净化交叉验证 (Purged K-Fold CV)

为了解决上述问题,De Prado 提出了净化交叉验证 (Purged K-Fold CV)。其核心思想是:在训练集和测试集之间建立“隔离带”,剔除那些与测试集有重叠的训练样本。

具体步骤:

  1. 分组:将数据按时间顺序切分为 \(K\) 组。
  2. 测试集:选取第 \(k\) 组作为测试集。
  3. 净化 (Purging):从训练集中剔除所有其标签区间与测试集标签区间有重叠的样本。
  4. 隔离 (Embargo):为了防止测试集的信息通过某些长周期特征泄露到紧随其后的训练集,通常在测试集之后再额外剔除一段数据(例如特征窗口长度的一半)。

这种方法虽然减少了可用的训练样本,但保证了验证结果的真实性,避免了“自欺欺人”。

12.4.3 滚动窗口验证 (Walk-Forward Validation)

这是量化领域最经典、最稳健的验证方法,因为它完全模拟了真实的时间流逝过程,不存在任何未来函数的可能性。

  1. 初始窗口:选取最早的一段数据 \([T_0, T_1]\) 作为训练集 (In-Sample)。
  2. 首次测试:使用训练好的模型在随后的数据 \([T_1, T_1 + \Delta]\) 上进行预测和交易 (Out-of-Sample)。
  3. 滚动
    • 扩展窗口 (Expanding Window):训练集变为 \([T_0, T_1 + \Delta]\),测试集变为 \([T_1 + \Delta, T_1 + 2\Delta]\)。适合数据量较小,或者认为历史越久越有价值的情况。
    • 滑动窗口 (Rolling Window):训练集变为 \([T_0 + \Delta, T_1 + \Delta]\),测试集变为 \([T_1 + \Delta, T_1 + 2\Delta]\)。适合市场风格变化快,模型需要快速适应新环境的情况。

AKQuant 框架内置了 run_walk_forward 工具,支持上述两种滚动模式,并自动拼接各段测试结果,生成完整的资金曲线。

12.5 AKQuant 实战:滚动训练策略

下面的代码展示了一个完整的 ML 策略框架。我们使用 scikit-learn 的逻辑回归模型,结合滚动窗口机制,预测次日涨跌。

12.5.1 策略逻辑

  • 特征returns_1 (1日收益率), returns_5 (5日收益率), ma_dist_20 (20日均线乖离率)。
  • 目标target (次日涨跌,1或-1)。
  • 训练机制:每隔 20 个交易日 (Re-train Interval) 使用过去 200 天的数据 (Lookback Window) 在线重训练一次。

12.5.2 代码实现

"""
第 12 章:机器学习在量化中的应用 (Machine Learning).

本示例展示了如何将机器学习 (ML) 融入到 AKQuant 策略中:
1. **特征工程 (Feature Engineering)**:构造滞后收益率、均线偏离度等因子。
2. **滚动训练 (Rolling Window)**:使用过去 N 天的数据训练模型。
3. **实时预测 (Real-time Prediction)**:使用训练好的模型对当前 Bar 进行预测。

示例模型:
- 使用 scikit-learn 的 LogisticRegression (逻辑回归) 预测次日涨跌。
- 目标变量 (Label):次日收益率 > 0 (1: 涨, 0: 跌/平)。
- 特征 (Features):
    - returns_1: 过去 1 天的收益率
    - returns_5: 过去 5 天的收益率
    - ma_dist_20: 当前价格相对于 20 日均线的偏离度

注意:由于 ML 模型训练较慢,本示例为了演示仅使用简单的线性模型。
实际生产中推荐使用 LightGBM/XGBoost,并配合 AKQuant 的 `run_walk_forward` 进行滚动回测。
"""

from typing import Any

import akquant as aq
import numpy as np
import pandas as pd
from akquant import Bar, Strategy

# 尝试导入 sklearn,如果未安装则跳过
try:
    from sklearn.linear_model import LogisticRegression
    from sklearn.preprocessing import StandardScaler

    HAS_SKLEARN = True
except ImportError:
    HAS_SKLEARN = False


# 模拟数据生成
def generate_mock_data(length: int = 1000) -> pd.DataFrame:
    """生成模拟数据."""
    np.random.seed(42)
    dates = pd.date_range(start="2020-01-01", periods=length, freq="D")

    # 构造一些有规律的信号 (动量效应)
    # 如果前一天涨,今天大概率涨
    returns = np.random.randn(length) * 0.01
    for i in range(1, length):
        if returns[i - 1] > 0:
            returns[i] += 0.005  # 增加正向动量
        else:
            returns[i] -= 0.005

    prices = 100 * (1 + returns).cumprod()

    df = pd.DataFrame(
        {
            "date": dates,
            "open": prices,
            "high": prices * 1.01,
            "low": prices * 0.99,
            "close": prices,
            "volume": 100000,
            "symbol": "MOCK_ML",
        }
    )
    return df


class MLStrategy(Strategy):
    """机器学习演示策略."""

    def __init__(self, train_window: int = 200) -> None:
        """初始化策略."""
        super().__init__()
        self.train_window = train_window  # 训练窗口长度 (例如使用过去 200 天训练)
        self.warmup_period = (
            train_window + 20
        )  # 预热期需要比训练窗口稍长,确保特征计算无空值

        self.model: Any = None
        self.scaler: Any = None

        # 记录最近一次训练的时间
        self.last_train_time = None
        self._bar_count = 0

    def calculate_features(self, df: pd.DataFrame) -> pd.DataFrame:
        """特征工程函数:计算技术指标作为特征."""
        df = df.copy()

        # 1. 计算特征 (X)
        df["returns_1"] = df["close"].pct_change(1)
        df["returns_5"] = df["close"].pct_change(5)

        ma20 = df["close"].rolling(20).mean()
        df["ma_dist_20"] = (df["close"] - ma20) / ma20

        # 2. 计算目标变量 (y)
        # 预测目标:次日收益率是否 > 0
        # shift(-1) 是将未来的收益率前移到今天,作为今天的 label
        df["target"] = np.where(df["close"].shift(-1) > df["close"], 1, 0)

        # dropna 会删除包含 NaN 的行。但对于最后一行(当前Bar),
        # 虽然 target 可能不准确(因为不知道明天价格),但特征是完整的,
        # 我们需要保留它用于实时预测 (Real-time Prediction)。
        # 只有在训练时才需要丢弃 target 无效的行。
        # 这里为了演示简单,我们仅丢弃特征计算产生的 NaN (前几行)。
        return df.dropna(subset=["returns_1", "returns_5", "ma_dist_20"])  # type: ignore

    def train_model(self, symbol: str) -> None:
        """在线训练模型."""
        if not HAS_SKLEARN:
            return

        # 获取历史数据
        # 我们需要 train_window + 额外一些 buffer 来计算指标
        df = self.get_history_df(count=self.train_window + 50, symbol=symbol)

        if len(df) < self.train_window:
            return

        # 准备数据
        data = self.calculate_features(df)

        # 使用最近 train_window 条数据进行训练
        train_data = data.iloc[-self.train_window :]

        feature_cols = ["returns_1", "returns_5", "ma_dist_20"]
        X = train_data[feature_cols]
        y = train_data["target"]

        # 标准化
        self.scaler = StandardScaler()
        X_scaled = self.scaler.fit_transform(X)

        # 训练逻辑回归模型
        self.model = LogisticRegression(random_state=42)
        self.model.fit(X_scaled, y)

        # 打印训练集准确率
        score = self.model.score(X_scaled, y)
        self.log(f"模型重训练完成 (样本数={len(train_data)}), 准确率={score:.2%}")

    def on_bar(self, bar: Bar) -> None:
        """收到 Bar 事件的回调."""
        if not HAS_SKLEARN:
            dt = pd.to_datetime(bar.timestamp)
            if dt.day == 1:  # 每月提醒一次
                self.log("未安装 scikit-learn,无法运行 ML 策略")
            return

        symbol = bar.symbol

        # 1. 定期重训练 (例如每月初)
        # 这里简化为:每隔 20 个交易日训练一次
        # 注意:在实盘中通常在盘后训练,这里为了演示放在盘中
        self._bar_count += 1

        if self.model is None or self._bar_count % 20 == 0:
            self.train_model(symbol)

        if self.model is None:
            return

        # 2. 实时预测
        # 获取最新的特征数据
        # 我们需要最近的一小段历史来计算当天的因子
        recent_df = self.get_history_df(count=30, symbol=symbol)
        if len(recent_df) < 30:
            return

        # 计算当天的特征
        # 注意:calculate_features 内部会有 dropna,所以要确保输入足够长
        features_df = self.calculate_features(recent_df)

        if features_df.empty:
            return

        # 取最后一行 (即当前 Bar 的特征)
        current_features = features_df.iloc[[-1]][
            ["returns_1", "returns_5", "ma_dist_20"]
        ]

        # 标准化
        X_curr = self.scaler.transform(current_features)

        # 预测概率
        # proba[0][1] 是预测为 1 (涨) 的概率
        prob_up = self.model.predict_proba(X_curr)[0][1]

        # 3. 交易逻辑
        pos = self.get_position(symbol)

        # 阈值设置:预测概率 > 0.55 才买入,< 0.45 卖出
        if prob_up > 0.55 and pos == 0:
            self.log(f"预测上涨概率 {prob_up:.2%} > 55%,买入")
            self.order_target_percent(0.95, symbol)

        elif prob_up < 0.45 and pos > 0:
            self.log(f"预测上涨概率 {prob_up:.2%} < 45%,卖出")
            self.close_position(symbol)


if __name__ == "__main__":
    if not HAS_SKLEARN:
        print("请先安装 scikit-learn: pip install scikit-learn")
    else:
        df = generate_mock_data()

        print("开始运行第 12 章 ML 策略示例...")
        result = aq.run_backtest(
            strategy=MLStrategy, data=df, initial_cash=100_000, commission_rate=0.0003
        )

        # 打印最终结果
        metrics = result.metrics_df
        end_value = (
            metrics.loc["end_market_value", "value"]
            if "end_market_value" in metrics.index
            else 0.0
        )
        print(f"回测结束,最终权益: {float(str(end_value)):.2f}")

12.6 常见陷阱与防范

  1. 前视偏差 (Look-ahead Bias):在计算特征时用到了未来数据(如使用当天的 Close 计算当天的特征,然后在盘中预测)。
    • 防范:特征计算必须严格滞后 (Lag)。feature_t 只能包含 \(t-1\) 及之前的信息。
  2. 过拟合 (Overfitting):模型在训练集完美,测试集拉胯。
    • 防范:使用正则化 (L1/L2),限制树的深度,特征选择 (Feature Selection),以及最重要的——特征逻辑性检查(不要完全依赖黑盒挖掘)。
  3. 非平稳性 (Non-Stationarity):市场规律会随时间变化(Regime Change)。
    • 防范:使用滚动窗口训练,让模型适应最新的市场环境。
  4. 算力消耗:在线训练非常消耗资源。
    • 优化:采用增量学习 (Incremental Learning)离线训练+在线预测 的架构。

12.7 前沿探索 (Advanced Topics)

12.7.1 深度学习与序列模型

传统的机器学习模型(如 XGBoost, LightGBM)虽然在处理表格数据上表现优异,但它们往往忽略了时间序列的时序结构。深度学习,特别是循环神经网络 (RNN) 及其变体,天然适合处理这种序列数据。

  1. LSTM / GRU

    • 通过门控机制(遗忘门、输入门、输出门)捕捉长距离依赖,解决了传统 RNN 的梯度消失问题。
    • 应用:直接输入过去 \(N\) 天的 OHLCV 序列,预测未来的价格走势或波动率。相比手工构造特征,LSTM 可以自动提取非线性特征。
  2. Transformer (Attention Is All You Need)

    • 最初用于 NLP,现在也被引入量化领域。
    • 自注意力机制 (Self-Attention):能够并行计算,并捕捉全局依赖关系。例如,今天的价格变动可能与 30 天前的某个宏观事件(如美联储议息会议)高度相关,Transformer 可以直接建立这种联系,而不需要像 RNN 那样逐步传递信息。
    • Temporal Fusion Transformer (TFT):谷歌提出的专门用于时间序列预测的模型,结合了静态变量(如股票所属行业)和动态变量(如每日量价),并具有一定的可解释性(可以看到哪些时间步、哪些特征最重要)。

12.7.2 强化学习 (Reinforcement Learning)

强化学习将量化交易看作一个马尔可夫决策过程 (MDP),智能体 (Agent) 在环境 (Environment) 中通过试错来学习最优策略。

  • 状态 (State, \(S_t\)):当前的市场环境。包括技术指标、账户持仓、资金余额、宏观经济数据等。
  • 动作 (Action, \(A_t\)):智能体的决策。买入、卖出、持有,或者具体的下单手数。
  • 奖励 (Reward, \(R_t\)):执行动作后的反馈。通常定义为当期收益 (PnL),或者夏普比率的变化 (Differential Sharpe Ratio)。
  • 策略 (Policy, \(\pi(a|s)\)):智能体根据当前状态选择动作的概率分布。

强化学习的优势在于它直接优化最终的交易目标(如总收益、夏普比率),而不是像监督学习那样仅仅优化预测准确率(MSE 或 Accuracy)。预测准确率高并不一定意味着赚钱(例如预测对了 99 次微涨,但错过了 1 次暴跌)。

然而,RL 在金融领域的应用也面临巨大挑战:

  1. 信噪比低:奖励信号非常嘈杂,Agent 很难区分运气和实力。
  2. 非平稳性:市场环境一直在变,昨天学到的最优策略今天可能就失效了。
  3. 探索成本:在真实市场中“试错”意味着真金白银的亏损。

12.8 MLOps 量化工程

随着模型越来越复杂,仅仅写好策略代码是不够的。我们需要一套工业级的机器学习运维 (MLOps) 体系来管理整个生命周期。

12.8.1 特征存储 (Feature Store)

在大型量化团队中,经常会出现“重复造轮子”的现象:A 组的研究员写了一套代码计算“波动率因子”,B 组的研究员也写了一套,但计算逻辑略有不同。这导致了数据的不一致和计算资源的浪费。

Feature Store 就是为了解决这个问题:

  • 离线存储 (Offline Store):存储历史特征数据,用于模型训练。通常基于数仓 (Hive/Snowflake) 或对象存储 (S3)。
  • 在线存储 (Online Store):提供低延迟的特征读取,用于实盘预测。通常基于 Redis 或 DynamoDB。
  • 一致性:保证训练时用的特征计算逻辑和实盘时完全一致,消除线上线下偏差 (Training-Serving Skew)

12.8.2 模型注册与版本管理 (Model Registry)

一个策略往往会经历多次迭代:V1.0 用了线性回归,V2.0 换成了 XGBoost,V2.1 调整了超参数...

我们需要一个系统(如 MLflow, Weights & Biases)来记录每一次实验:

  • 代码版本:Git Commit ID。
  • 数据版本:训练数据的快照 Hash。
  • 超参数:学习率、树深、正则化系数等。
  • 模型文件:训练好的模型权重 (Artifacts)。
  • 评估指标:回测的夏普比率、最大回撤等。

只有通过了严格的自动化测试(回测表现达标、风险指标合规)的模型,才能被标记为 Production 状态,并自动部署到实盘交易系统。

本章小结

  1. 机器学习策略的核心优势来自非线性建模与特征表达能力。
  2. 时间序列验证与数据泄漏防控比模型复杂度更关键。
  3. 从研究到实盘需要完整的模型治理与版本追踪体系。

课后练习

  1. 用两套不同特征集训练模型并比较样本外表现。
  2. 增加一次 walk-forward 验证,观察绩效稳定性变化。
  3. 在同一数据上对比线性模型与树模型的风险收益差异。

常见错误与排查

  1. 回测表现异常好:优先排查标签泄漏和未来信息使用。
  2. 实盘迁移失效:检查训练分布与线上分布是否发生漂移。
  3. 模型不可复现:固定随机种子并持久化数据与参数版本。