跳转至

第4章 量化交易中的机器学习

随着金融市场的复杂性和数据量的增长,传统的交易策略逐渐难以应对市场的快速变化。机器学习(ML)算法在交易领域得到了广泛应用,主要用于数据分析、模式识别和预测建模,以优化交易策略、降低风险并提高收益。

机器学习算法在交易中的主要应用包括:

  1. 市场预测:基于历史价格数据和技术指标预测未来价格趋势。

  2. 交易信号生成:通过模式识别找到买入和卖出的最佳时机。

  3. 风险管理:利用统计学习方法评估投资组合的风险水平。

  4. 算法交易:自动化执行交易策略,提高交易效率。

  5. 机器学习算法在交易中主要可以分为以下几类:

  6. 监督学习(Supervised Learning):用于价格预测、资产分类等。

  7. 无监督学习(Unsupervised Learning):用于市场模式识别、聚类分析等。

  8. 强化学习(Reinforcement Learning):用于优化交易策略和资产管理。

本章将深入探讨交易中使用的关键机器学习算法,介绍其数学原理、训练方法及评估策略,并配以相应的 Python 代码示例。

金融交易中广泛运用了各种机器学习算法,从传统方法到前沿的深度学习模型。传统机器学习算法例如支持向量机(SVM)和随机森林等,常用于构建交易信号的预测模型;深度学习算法则包括长短期记忆网络(LSTM)、Transformer,以及强化学习等,用于捕捉更复杂的非线性关系。下面分别介绍这些算法及其核心数学公式,并提供简明的Python示例帮助读者快速上手。

4.1 交易中使用的机器学习算法

金融交易中广泛运用了各种机器学习算法,从传统方法到前沿的深度学习模型。传统机器学习算法例如支持向量机(SVM)和随机森林等,常用于构建交易信号的预测模型;深度学习算法则包括长短期记忆网络(LSTM)、Transformer,以及强化学习等,用于捕捉更复杂的非线性关系。下面分别介绍这些算法及其核心数学公式,并提供简明的Python示例帮助读者快速上手。

4.1.1 支持向量机(SVM)

SVM是一种监督学习算法,通过在特征空间中寻找能够最大程度分隔不同类别数据的超平面来进行分类或回归。其核心数学公式是优化问题:

\[\text{min}_{w,b,\xi_{i}}\frac{1}{2}|w|^{2} + C\sum_{i = 1}^{m}\xi_{i},\text{ subject to}:y_{i}\left( w^{T}x_{i} + b \right) \geq 1 - \xi_{i},\xi_{i} \geq \ 0,\ i\ = \ 1,\ 2,\ \ldots,\ m\]

这里 \(w\)\(b\) 定义超平面 \(f(x) = w^{T}x + b\) ,约束条件确保所有样本距离超平面至少1,允许一定错误分类程度由松弛变量 \(\xi_{I}\) 控制, \(C\) 为惩罚系数平衡间隔最大化与误分类。

SVM在交易中可用于分类预测,例如预测明日股价上涨或下跌的概率,将其作为买卖信号的依据。一旦训练出模型,可以得到决策函数

\[f(x) = \sum_{j \in SV}^{}{\alpha_{j}y_{j}K\left( x_{j},x \right)} + b,\]

其中求和仅在支持向量上进行 (对应非零拉格朗日乘子 \(\alpha_{j}\) )。交易时,当\(f(x)\)输出为正就做多,为负则做空。SVM以其良好的泛化能力常被用来做股价走势分类或市场状态识别等任务。

4.1.2 随机森林

随机森林是基于决策树的集成学习方法,通过构建多棵决策树并对它们的结果取平均(回归任务)或投票(分类任务)来提升预测性能。

其数学表达可以描述为:模型输出

\[\widehat{y} = \frac{1}{N}\sum_{j = 1}^{N}{f_{j}(x)},\]

其中 \(f_{j}(x)\) 是第 \(j\) 棵决策树的输出。在分类情况下,相当于每棵树给出一个类别建议,随机森林选择出现次数最多的类别作为最终预测。由于每棵树只使用随机子集的特征和样本,随机森林有效降低了过拟合,适合处理高维度的金融特征数据。例如,在交易中,可利用随机森林结合多种技术指标来预测股票的涨跌信号。每棵树可能基于不同指标(如移动平均交叉、成交量变化等)给出交易建议,最终集成的结果往往比单一决策树更加稳健。

4.1.3 长短期记忆网络(LSTM)

LSTM是循环神经网络(RNN)的一种,专门为捕捉长序列依赖信息设计。它通过引入细胞状态(cell state)和门控机制,克服了传统RNN的长期依赖难题。LSTM内部包括遗忘门、输入门、输出门和候选记忆单元,用以控制信息的遗留、更新和输出。其前向传播核心公式如下:

遗忘门:

\[f_{t} = \sigma\left( W_{f}\left\lbrack h_{t - 1},x_{t} \right\rbrack + b_{f} \right),\]

决定丢弃上一时刻细胞状态 \(C_{t - 1}\) 中的信息比例。

输入门:

\[i_{t} = \sigma\left( W_{i}\left\lbrack h_{t - 1},x_{t} \right\rbrack + b_{i} \right),\]

决定当前时刻的新信息写入细胞的比例。

候选记忆:

\[\widetilde{C_{t}} = \tanh\left( W_{C}\left\lbrack h_{t - 1},x_{t} \right\rbrack + b_{C} \right),\]

生成新的候选内容来更新细胞状态。

输出门:

\[o_{t} = \sigma\left( W_{o}\left\lbrack h_{t - 1},x_{t} \right\rbrack + b_{o} \right),\]

决定细胞状态有多少将影响输出隐状态。

细胞状态更新:

\[C_{t} = f_{t} \odot C_{t - 1} + i_{t} \odot \widetilde{C_{t}},\]

将遗忘门过滤后的旧状态和输入门控制的新候选合并。

隐状态更新:

\[h_{t} = o_{t} \odot \tanh\left( C_{t} \right),\]

即当前时刻的输出隐状态等于细胞状态的激活值经输出门过滤。

上述公式中, \(\sigma\) 是 Sigmoid 函数, \(\, tanh\) 是双曲正切函数, \(\odot\) 表示按元素相乘。LSTM通过这些门控结构,实现对长期趋势的记忆和对短期噪声的忽略。例如在交易中,LSTM可用来处理时间序列数据(如股价序列)。它像一个有记忆的交易者:遗忘门控制"不重要的市场噪声"需被遗忘多少,输入门控制"新的市场变化"应被记住多少,细胞状态在时间轴上累积重要的信息,而输出门决定了对下一时刻预测产生多大影响。因而,LSTM常被用于股票价格预测、交易量预测等任务。它能够学习到"长期牛熊趋势"和"短期波动模式",对具有序列相关性的金融数据特别有效。

4.1.4 Transformer

Transformer模型最初用于自然语言处理,但也逐渐应用于金融时间序列和组合策略优化等领域。Transformer的核心是注意力机制,它可以在长序列中学会关注最相关的信息。与传统RNN按顺序处理数据不同,Transformer可以并行地考虑序列中所有位置之间的关联。在交易中,这意味着模型能够同时关注多个时间点的市场状态或不同资产之间的关系。Transformer的自注意力(self-attention)计算公式为:

\[\text{Attention}(Q,K,V) = Softmax\left( \frac{{QK}^{T}}{\sqrt{d_{k}}} \right)V\]

其中 \(Q,\, K,\, V\,\)分别表示查询(Query)、键(Key)和值向量(Value)的矩阵表示,\(d_{k}\)是键向量的维度。这个公式计算了查询与每个键的点积相似度,经由 \(\sqrt{d_{k}}\) 缩放并通过Softmax得到权重,再用权重对值向量加权求和,得到注意力输出。

在金融应用中,可以把序列的不同时间步的特征向量作为 \(Q,\, K,\, V\) 。这样模型会自适应地分配权重,关注那些对预测最有用的过去时刻。例如,一个Transformer模型在预测股票价格时,可能自动"注意"到与当前走势相似的历史模式片段,或关注某些关键的宏观数据发布时刻的市场反应,而忽略无关的信息。由于Transformer擅长处理长序列依赖和并行计算,它在跨市场因子挖掘、新闻情绪分析(通过BERT等变体处理社交媒体文本)以及高频数据模式提取等方面也开始展现威力。

4.1.5 强化学习(RL)

强化学习通过让智能体(agent)与环境反复交互、试错学习策略,以最大化累积回报。在算法交易中,RL方法可以直接学出交易策略:智能体观察市场状态,然后执行买卖操作,收到盈利或亏损作为回报,不断优化策略。在深度强化学习中,常用的方法包括深度Q网络(DQN)、策略梯度等。其经典核心公式是Q-learning的值迭代更新:

\[Q\left( s_{t},a_{t} \right) \leftarrow Q\left( s_{t},a_{t} \right) + \alpha\left\lbrack r_{t} + \gamma\max_{a}Q\left( s_{t + 1},a \right) - Q\left( s_{t},a_{t} \right) \right\rbrack,\]

这一定式表示利用当前奖励 \(r_{t}\) 和下一状态的最大预期价值来更新当前状态-动作对 \(\left( s_{t},a_{t} \right)\) 的价值估计。其中 \(\alpha\) 是学习率, \(\gamma\) 是折扣因子, \(Q(s,a)\) 称为Q值函数。直观来说,Q值衡量了"在状态 \(s\) 下采取动作 \(a\) 能获得的长期回报"。

在交易场景中,状态可以是市场行情特征(技术指标、持仓情况等),动作是买入、卖出或持仓,回报则是交易收益。通过不断迭代上述公式,智能体学会在不同市场状态下选择能带来最高预期收益的操作。深度学习的加入在于用神经网络逼近 \(Q(s,a)\) 或策略 \(\pi\left( a \middle| s \right)\) ,从而处理连续状态空间。

举例:可以训练一个强化学习智能体来做高频交易,它在模拟环境中试错上万次交易,从中学习何时下单、何时平仓以最大化收益。与监督学习不同,RL不需要明确的标签信号,而是依赖交互得到的奖励来引导学习,非常适合构建交易策略的自动化生成。

下面的代码演示了如何使用scikit-learn训练一个简单的分类模型(例如SVM)来预测交易信号。首先模拟生成一些历史价格数据及技术指标,然后用前80%的数据训练模型、后20%数据测试模型,预测价格上涨(1)或下跌(0)的信号。虽然这是一个toy example(随机生成的数据不包含真实模式),但流程与实际类似。

import numpy as np
from sklearn.svm import SVC

# ------------------------------------------------------------
# 1) Simulate a price series (random walk)
# ------------------------------------------------------------

np.random.seed(42)

# Simulate 100 days of prices
prices = 100 + np.cumsum(np.random.normal(0, 1, 100))

# Compute daily returns
returns = np.diff(prices) / prices[:-1]

# ------------------------------------------------------------
# 2) Build features and labels
# Use returns from the previous two days to predict
# whether the return on day t+1 is positive or negative
# ------------------------------------------------------------

# Features: returns from day t-2 and t-1
X = np.column_stack([
    returns[:-2],
    returns[1:-1]
])

# Labels: 1 if next-day return is positive, 0 otherwise
y = (returns[2:] > 0).astype(int)

# ------------------------------------------------------------
# 3) Train-test split (80% train, 20% test)
# ------------------------------------------------------------

train_size = int(len(X) * 0.8)

X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

# ------------------------------------------------------------
# 4) Train an SVM classifier and make predictions
# ------------------------------------------------------------

model = SVC(kernel="rbf", gamma="auto")
model.fit(X_train, y_train)

y_pred = model.predict(X_test)

# ------------------------------------------------------------
# 5) Compare predicted vs actual signals
# ------------------------------------------------------------

print("Predicted signals:", y_pred[:5])
print("Actual signals:   ", y_test[:5])
在这个示例中,没有追求高预测准确率(因为数据是随机的,模型无法学到真正有效的交易策略),但展示了基本步骤:准备数据、提取特征、训练模型、输出预测信号。实际应用中,可以将特征替换为技术指标、基本面因子等真实数据,将上述分类过程用于判断"明日是否上涨",或回归预测"明日回报率"等。
from datetime import datetime, timedelta

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report

# ------------------------------------------------------------
# Reproducibility
# ------------------------------------------------------------

np.random.seed(42)
torch.manual_seed(42)

# ------------------------------------------------------------
# 1) Synthetic data generation
# ------------------------------------------------------------

def generate_sample_data(n_samples: int = 1000):
    """Generate synthetic financial time-series data for demonstration."""
    print("Generating synthetic data...")

    t = np.linspace(0, 4 * np.pi, n_samples)
    trend = 100 + t * 2
    seasonal = 10 * np.sin(t)
    noise = np.random.normal(0, 5, n_samples)

    prices = trend + seasonal + noise

    returns = np.diff(prices) / prices[:-1]

    volatility = (
        pd.Series(returns)
        .rolling(window=20)
        .std()
        .fillna(0)
        .values[:-1]
    )

    momentum = (
        pd.Series(returns)
        .rolling(window=10)
        .mean()
        .fillna(0)
        .values[:-1]
    )

    X = np.column_stack([
        returns[:-1],      # previous-day return
        volatility,        # 20-day volatility
        momentum           # 10-day momentum
    ])

    y = (returns[1:] > 0).astype(int)

    print("Data generation complete.")
    print(f"Features shape: {X.shape}, Labels shape: {y.shape}")

    return X, y, prices

# ------------------------------------------------------------
# 2) SVM Trader
# ------------------------------------------------------------

class SVMTrader:
    """Support Vector Machine–based trading model."""

    def __init__(self):
        self.scaler = StandardScaler()
        self.model = SVC(kernel="rbf", probability=True)

    def train(self, X_train, y_train):
        print("\nTraining SVM model...")
        X_scaled = self.scaler.fit_transform(X_train)
        self.model.fit(X_scaled, y_train)
        print("SVM training complete.")

    def predict(self, X):
        X_scaled = self.scaler.transform(X)
        return self.model.predict(X_scaled)

    def predict_proba(self, X):
        X_scaled = self.scaler.transform(X)
        return self.model.predict_proba(X_scaled)

# ------------------------------------------------------------
# 3) Random Forest Trader
# ------------------------------------------------------------

class RandomForestTrader:
    """Random Forest–based trading model."""

    def __init__(self, n_estimators: int = 100):
        self.model = RandomForestClassifier(n_estimators=n_estimators)

    def train(self, X_train, y_train):
        print("\nTraining Random Forest model...")
        self.model.fit(X_train, y_train)
        print("Random Forest training complete.")

        feature_importance = pd.DataFrame({
            "feature": ["returns", "volatility", "momentum"],
            "importance": self.model.feature_importances_
        })

        print("\nFeature Importances:")
        print(feature_importance.sort_values("importance", ascending=False))

    def predict(self, X):
        return self.model.predict(X)

    def predict_proba(self, X):
        return self.model.predict_proba(X)

# ------------------------------------------------------------
# 4) LSTM Model Definition
# ------------------------------------------------------------

class LSTMModel(nn.Module):
    """LSTM network for sequence-based prediction."""

    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers

        self.lstm = nn.LSTM(
            input_dim,
            hidden_dim,
            num_layers,
            batch_first=True
        )

        self.fc = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim)

        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

# ------------------------------------------------------------
# 5) Dataset for LSTM
# ------------------------------------------------------------

class FinancialDataset(Dataset):
    """Sequence dataset for LSTM training."""

    def __init__(self, X, y, seq_length=10):
        self.X = torch.FloatTensor(X)
        self.y = torch.FloatTensor(y)
        self.seq_length = seq_length

    def __len__(self):
        return len(self.X) - self.seq_length + 1

    def __getitem__(self, idx):
        return (
            self.X[idx:idx + self.seq_length],
            self.y[idx + self.seq_length - 1]
        )

# ------------------------------------------------------------
# 6) LSTM Trader
# ------------------------------------------------------------

class LSTMTrader:
    """LSTM-based trading model."""

    def __init__(self, input_dim=3, hidden_dim=32, num_layers=2, seq_length=10):
        self.seq_length = seq_length
        self.model = LSTMModel(input_dim, hidden_dim, num_layers, output_dim=1)
        self.criterion = nn.BCEWithLogitsLoss()
        self.optimizer = optim.Adam(self.model.parameters())

    def train(self, X_train, y_train, epochs=50, batch_size=32):
        print("\nTraining LSTM model...")

        dataset = FinancialDataset(X_train, y_train, self.seq_length)
        dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

        for epoch in range(epochs):
            self.model.train()
            total_loss = 0.0

            for batch_X, batch_y in dataloader:
                outputs = self.model(batch_X)
                loss = self.criterion(outputs.squeeze(), batch_y)

                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()

                total_loss += loss.item()

            if (epoch + 1) % 10 == 0:
                print(
                    f"Epoch [{epoch+1}/{epochs}], "
                    f"Loss: {total_loss / len(dataloader):.4f}"
                )

        print("LSTM training complete.")

    def predict(self, X):
        self.model.eval()
        predictions = []

        with torch.no_grad():
            dataset = FinancialDataset(X, np.zeros(len(X)), self.seq_length)
            dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

            for batch_X, _ in dataloader:
                output = self.model(batch_X)
                prob = torch.sigmoid(output.squeeze()).numpy()
                predictions.append(prob)

        return np.array(predictions)

# ------------------------------------------------------------
# 7) Model evaluation
# ------------------------------------------------------------

def evaluate_models(X_train, X_test, y_train, y_test):
    """Train and evaluate all models."""

    svm = SVMTrader()
    svm.train(X_train, y_train)
    svm_pred = svm.predict(X_test)
    print("\nSVM Results:")
    print(classification_report(y_test, svm_pred))

    rf = RandomForestTrader()
    rf.train(X_train, y_train)
    rf_pred = rf.predict(X_test)
    print("\nRandom Forest Results:")
    print(classification_report(y_test, rf_pred))

    lstm = LSTMTrader()
    lstm.train(X_train, y_train)
    lstm_pred = (lstm.predict(X_test) > 0.5).astype(int)

    print("\nLSTM Results:")
    print(
        classification_report(
            y_test[lstm.seq_length - 1:],
            lstm_pred
        )
    )

    return svm, rf, lstm

# ------------------------------------------------------------
# 8) Visualization
# ------------------------------------------------------------

def plot_results(prices, predictions_dict):
    """Plot price series with buy/sell signals."""

    plt.figure(figsize=(15, 6))
    plt.plot(prices, label="Price", alpha=0.5)

    for model_name, preds in predictions_dict.items():
        plt.scatter(
            np.where(preds == 1)[0],
            prices[preds == 1],
            marker="^",
            label=f"{model_name} Buy",
            alpha=0.7
        )
        plt.scatter(
            np.where(preds == 0)[0],
            prices[preds == 0],
            marker="v",
            label=f"{model_name} Sell",
            alpha=0.7
        )

    plt.title("Price Series with Trading Signals")
    plt.xlabel("Time")
    plt.ylabel("Price")
    plt.legend()
    plt.grid(True)
    plt.show()

# ------------------------------------------------------------
# 9) Main execution
# ------------------------------------------------------------

def main():
    X, y, prices = generate_sample_data()

    split = int(0.8 * len(X))
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    svm, rf, lstm = evaluate_models(X_train, X_test, y_train, y_test)

    predictions = {
        "SVM": svm.predict(X_test),
        "RF": rf.predict(X_test),
        "LSTM": (lstm.predict(X_test) > 0.5).astype(int)
    }

    plot_results(prices[split:], predictions)


if __name__ == "__main__":
    main()
输出:
Model Training and Evaluation Summary

Dataset:
Features shape: (998, 3)
Labels shape: (998,)

SVM Model
Training SVM model...
SVM training complete.

Classification Performance:
precision    recall  f1-score   support
Class 0        0.62      0.76      0.68        91
Class 1        0.75      0.61      0.67       109
Accuracy                           0.68       200
Macro Avg      0.68      0.68      0.67       200
Weighted Avg   0.69      0.68      0.67       200

Random Forest Model
Training Random Forest model...
Random Forest training complete.

Feature Importances:
Momentum     0.376
Returns      0.364
Volatility   0.261

Classification Performance:
precision    recall  f1-score   support
Class 0        0.61      0.69      0.65        91
Class 1        0.71      0.62      0.66       109
Accuracy                           0.66       200
Macro Avg      0.66      0.66      0.65       200
Weighted Avg   0.66      0.66      0.66       200

LSTM Model
Training LSTM model...
Epoch [10/50], Loss: 0.6892
Epoch [20/50], Loss: 0.5497
Epoch [30/50], Loss: 0.5140
Epoch [40/50], Loss: 0.5092
Epoch [50/50], Loss: 0.4957

Summary:
SVM achieved the highest classification accuracy (68%), Random Forest followed closely (66%), and LSTM showed stable convergence with decreasing loss, indicating its ability to capture temporal patterns despite higher model complexity.

4.2 如何利用金融数据训练机器学习模型?

金融交易中广泛运用了各种机器学习算法,从传统方法到前沿的深度学习模型。传统机器学习算法例如支持向量机(SVM)和随机森林等,常用于构建交易信号的预测模型;深度学习算法则包括长短期记忆网络(LSTM)、Transformer,以及强化学习等,用于捕捉更复杂的非线性关系。下面分别介绍这些算法及其核心数学公式,并提供简明的Python示例帮助读者快速上手。

4.2.1 金融数据的来源

1.市场数据

包括价格和交易量等行情数据。例如股票的开盘价、收盘价、最高最低价、成交量,衍生品的盘口数据,收益率曲线,隐含波动率等。这类数据通常由交易所提供,获取方式包括通过金融数据终端(如Bloomberg、Wind)、交易所接口,或财经网站API。市场数据是量化交易中最基础的数据,常用于计算技术指标和训练价格预测模型。

2.基本面数据

反映公司或宏观经济基本状况的数据。例如公司的财务报表指标(营收、净利润、资产负债率等)、宏观经济指标(GDP增长率、利率、通胀率)以及行业统计数据等。基本面数据往往来自财报、经济数据库或政府发布的统计报告,在价值投资和中长线策略中常被用作特征。随着机器学习的发展,不少模型会结合基本面因子来改进信号,例如用财务健康指标预测公司股票的长期表现。

3.社交媒体和新闻数据

来自新闻资讯、Twitter、股吧论坛等渠道的非传统数据。社交媒体已成为投资者情绪和舆情的重要载体,分析这些数据有助于把握市场情绪和预期。例如,通过自然语言处理技术从新闻标题或微博帖子中提取情感倾向,可以构建情绪指标作为交易模型的输入特征。研究表明,社交媒体上的言论与市场波动存在相关性,合理利用这类另类数据可能带来超额收益。近年来,Transformer等深度学习模型被应用于金融文本分析,可以实时从海量新闻和推文中提取有用信息并整合到交易策略中。

4.2.2 数据预处理与特征工程

获取数据后,必须对其进行清洗和预处理,因为原始金融数据往往存在缺失值、噪声以及不同量纲等问题。一个可靠的预处理过程有助于提高模型训练效果和稳定性:

1.缺失值处理

金融数据常有缺失,例如停牌期间股票无交易价,部分公司基本面指标未披露等。常用的处理方法包括删除含缺失值的记录、用平均值/中位数填补,或针对时间序列数据用前值填充(前向填充)或插值方法估算。具体选择取决于缺失值数量和分布。若缺失较少且随机出现,直接删除相应样本影响不大;若缺失较多,填补可能更合理。在时间序列中,例如股票停牌3天,可以考虑用停牌前最后一个价格填充这3天的价格序列以保持连续性。在机器学习训练前,一定要确认数据中不含异常的NaN,否则会导致训练过程报错或模型参数为NaN。

2.数据清洗与异常处理

排除明显错误的数据点,例如成交量异常为0或者价格暴涨暴跌超过合理范围的点(这些可能是数据录入错误或异常事件)。可以设定合理的阈值或通过算法检测离群点(outliers)。对于检测出的异常数据,可以选择剔除或进行修正。比如,若某只股票某日涨跌幅记录为+500%,明显不合理,就需要核实该日期是否除权调整或数据错误,并进行相应处理。

3.特征工程

根据原始数据构造更能反映市场规律的特征,是提高模型准确度的关键一步。特征工程在量化交易中包括技术指标的计算、因子构造和信息增广等。例如,从价格序列计算移动平均线(MA)、相对强弱指数(RSI)、布林带宽度等技术指标,或从多资产数据构造价差、相关性等特征。基本面数据可以构造财务比率(如市盈率PE、市净率PB等)作为特征。对于社交媒体文本,可以计算情感得分或话题热度作为特征。通过特征工程,希望将原始数据中潜藏的有效信息提炼出来,作为模型的输入。在深度学习应用中,有时也采用自动特征提取,例如用自动编码器、深度因子模型提取综合因子,但无论如何,输入模型的数据都需要恰当表达出认为有预测力的市场信息。

4. 数据标准化

不同特征往往量纲差异很大,需要归一化或标准化以避免在模型训练中特征范围悬殊导致的数值不稳定。常用的方法包括Z-score标准化(减去均值除以标准差)和Min-Max归一化(将值缩放到[0,1]区间)。例如,价格可能在数十元到数百元范围,而收益率通常在-0.1到0.1之间,将它们直接一起喂给模型,梯度更新会偏向较大数值的特征。通过标准化,可以让特征在相近的尺度上变化,加快模型收敛并提高泛化性。

4.2.3 模型训练与验证

完成特征准备后,就可以进入模型训练阶段。为了在历史数据上训练并评估模型,通常需要注意以下几点:

1. 训练集/测试集划分

要评估模型在未知数据上的表现,应将已有数据划分为训练集和测试集。在回测研究中,为避免未来数据泄漏,采用时序拆分而非随机拆分。典型做法是用较早时期的数据训练,用后面的数据测试。例如,用2010-2018年的数据训练模型,再用2019年的数据测试模型性能。这样可以模拟真实交易中"先用过去数据训练,再在未来数据上检验"的过程,避免前视偏差。对于数据量较大的情况,也可以采用滚动窗口训练和验证(Rolling/Walk-forward Validation),即在每一个时间窗口上训练模型并在随后的窗口上验证,评估模型随时间的稳定性。

2. 超参数调整

机器学习模型(尤其是深度学习)有许多超参数,例如学习率、树的棵数、网络层数、神经元数量等。需要通过调参找到比较优的配置。常用方法有网格搜索(Grid Search)和随机搜索,可以在训练集上通过交叉验证选出效果较好的参数组合。不过在金融时序数据上,直接交叉验证需小心时序依赖问题,一般采用前述滚动验证方式评估超参数性能,而不是打乱数据顺序的K折交叉验证。超参数调整还应避免过度优化在历史数据上的表现,否则容易导致过拟合(即策略在历史回测很好但实盘不佳)。所以调参时关注简洁性和经济含义,不宜为了提高历史指标而引入过多复杂度。

3. 模型训练

根据任选择合适的损失函数和优化算法进行训练。比如,若是回归预测资产收益率,可用均方误差(MSE)作为损失;若是分类预测涨跌,可用对数损失(log loss)或二元交叉熵;强化学习则有其特定的奖励函数。训练过程中可以设置早停(early stopping)策略,如果验证集指标在若干迭代后不再提升,则停止训练以防止过拟合。训练完毕后,先在测试集上评估性能,再进行更严格的回测模拟。对深度学习模型而言,训练还涉及GPU加速、批归一化、正则化等技巧,以确保模型高效稳定收敛。

Python 实战示例:以下示例代码演示了从金融时间序列构建特征并训练模型的过程。模拟生成一份股票价格数据,然后计算技术指标作为特征,进行数据预处理,最后训练一个随机森林模型来预测下一日涨跌。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler

# ------------------------------------------------------------
# 1) Simulate a price time series
# ------------------------------------------------------------

dates = pd.date_range("2020-01-01", periods=100, freq="D")

# Simulated price series (random walk)
prices = 100 + np.cumsum(np.random.normal(0, 1, 100))

df = pd.DataFrame({"Price": prices}, index=dates)

# ------------------------------------------------------------
# 2) Compute technical features
# - Daily return
# - 5-day vs 10-day moving average difference
# ------------------------------------------------------------

df["Return"] = df["Price"].pct_change()
df["MA5"] = df["Price"].rolling(window=5).mean()
df["MA10"] = df["Price"].rolling(window=10).mean()
df["MA_diff"] = df["MA5"] - df["MA10"]

# ------------------------------------------------------------
# 3) Handle missing values from rolling calculations
# ------------------------------------------------------------

df_clean = df.dropna().copy()

# ------------------------------------------------------------
# 4) Feature scaling
# Standardize returns and moving-average difference
# ------------------------------------------------------------

features = ["Return", "MA_diff"]

scaler = StandardScaler()
df_clean[features] = scaler.fit_transform(df_clean[features])

# ------------------------------------------------------------
# 5) Create prediction target
# Target = 1 if next-day price goes up, else 0
# ------------------------------------------------------------

df_clean["Target"] = (
    df_clean["Price"].shift(-1) > df_clean["Price"]
).astype(int)

# Drop last row (no next-day price available)
df_clean = df_clean.dropna()

# ------------------------------------------------------------
# 6) Train-test split (time-series split)
# ------------------------------------------------------------

train_size = int(len(df_clean) * 0.7)

train_data = df_clean.iloc[:train_size]
test_data = df_clean.iloc[train_size:]

# ------------------------------------------------------------
# 7) Train Random Forest classifier
# ------------------------------------------------------------

model = RandomForestClassifier(
    n_estimators=100,
    random_state=0
)

model.fit(train_data[features], train_data["Target"])

# ------------------------------------------------------------
# 8) Evaluate model on the test set
# ------------------------------------------------------------

pred = model.predict(test_data[features])
accuracy = (pred == test_data["Target"]).mean()

print(f"Test accuracy: {accuracy:.2f}")
在这个示例中,演示了数据预处理(计算技术指标、处理缺失、标准化)到模型训练的完整流程。首先生成模拟价格并计算5日均线和10日均线差值作为特征,然后将缺失的最初几天数据丢弃。对连续型特征做了标准化,使其均值为0、方差为1。Target标签定义为明日价格是否上涨。接着,用70%的数据训练随机森林模型,并在剩余30%数据上测试模型精度。请注意,由于使用随机生成的数据,模型的预测精度不会很高(上述代码打印的测试准确率可能在0.5左右,相当于随机猜测)。在真实应用中,需要使用真实的历史数据并精心挑选特征,以得到具有预测能力的模型。

4.3 如何评估机器学习模型的交易性能?

构建交易模型的最终目的在于获得稳定的交易回报。因此,在训练出模型后,需要评估其在交易策略层面的表现。这与常规机器学习评估有所不同:不仅关注预测精度,更关注策略的盈利能力和风险特征。本节将介绍交易信号的生成方法以及常用的策略评估指标,包括年化收益率、夏普比率、最大回撤、信息比率等,并给出计算这些指标的示例代码。

4.3.1 交易信号的生成:回归 vs 分类模型

机器学习模型输出通常需要转换为具体的交易操作信号。生成信号的方法取决于模型类型:

1.基于回归模型

回归模型直接预测未来某个数值,如明日的价格或收益率。需要根据预测值制定操作。例如,预测明日收益率为\(+ 0.5\%\),可以解释为看涨信号,预测为\(- 0.3\%\)则看跌信号。一种简单策略是设定阈值:当模型预测的收益率 $ \, 0$ 时,产生买入信号; \(< \, 0\,\) 时产生卖出或空仓信号。如果模型输出的是未来价格,也可比较预测价与当前价:预测价高于当前价一定阈值则买入,反之卖出。另一种方法是排序:如果同时对多只资产预测未来回报率,则可以按预测值大小排序,选择 \(\text{Top N}\) 个做多、 \(\text{Bottom N}\) 个做空(即多空组合)。例如,一个模型预测了明天所有股票的涨幅,可以构建"做多预测涨幅最高的10只股票、做空预测涨幅最低的10只股票"的组合策略,以获取相对收益。

2.基于分类模型

分类模型输出离散的类别标签(或概率),例如预测"上涨"或"下跌"。这种模型天然就可以作为信号:预测上涨则买入,预测下跌则卖出或做空。如果模型输出上涨的概率 $P(up"\ )$,还可以设置信心阈值:只有当 $P(up" )$ 超过0.7等高阈值时才执行交易,否则观望不交易,以减少错误信号带来的交易成本。这种基于概率的信号在实际中很常用,因为模型并不总是非常确定,可以根据置信度来过滤信号。此外,分类模型也可以设计多类别输出,例如"强烈买入、观望、强烈卖出"三类信号,从而区分不同强度的建议。

需要注意,无论回归还是分类模型,交易信号生成都应考虑交易成本和市场影响。一个模型频繁地在微小预测变化下买进卖出,可能在实际中因滑点和手续费亏损。因此,很多策略会加入持仓时间或换手率的约束,例如信号只有在足够强或持续一定时间时才触发交易。另外,信号有时需要平滑或结合规则策略来执行。例如,用模型每日预测的涨跌信号作为"初始意见",然后再结合风险控制规则决定最终仓位。

4.3.2 策略评估指标

要评估模型驱动的交易策略好坏,金融领域有一套成熟的指标体系,兼顾收益和风险。假设有模型生成的每日策略回报率序列 \(r_{1},r_{2},\ldots,r_{T}\)(这可以通过在回测中应用模型信号得到),常用的评估指标包括:

1.年化收益率(Annualized Return)

表示策略平均一年可获得的收益率。如果已知策略在 \(T\) 天的总收益率为 $R_{\text{total}} $,年化收益率计算公式为:

\[R_{\text{annual}} = \left( 1 + R_{\text{total}} \right)^{\frac{252}{T}} - 1\]

这里假设一年有252个交易日。上述公式相当于把 TTT 天的复利收益按比例扩展到一年。例如,如果10个交易日总收益为\(1\%\),则年化约为

\[(1 + 0.01)^{\frac{252}{10}} - 1 \approx 28\%\]

年化收益率方便不同策略或不同时段的业绩比较,因为它将持有期不同的收益换算到年尺度。需要注意的是,如果策略持有期不到一年,年化只是理论值;真正运行中能否保持相同收益率还需结合其他稳定性指标。

2.夏普比率(Sharpe Ratio)

衡量单位风险下的超额收益。计算公式为:

\[S = \frac{E\left\lbrack R_{p} - R_{f} \right\rbrack}{\sigma_{p}}\]

其中 \(R_{p}\) 表示策略回报率, \(R_{f}\) 是无风险利率(通常用国债利率近似),\(\sigma_{p}\) 是策略回报的标准差。分子是策略相对无风险收益的超额收益率的期望,分母是策略收益的波动率,故Sharpe值表示每承受一单位波动风险,策略可获得多少超额回报。一般认为Sharpe越高越好,比如\(\text{Sharpe} = 1\)表示策略每承受1单位风险可获得1单位超额收益。年化Sharpe通常通过用年化收益除以年化波动率计算(或者将日Sharpe乘以\(\sqrt{252}\))。需要注意夏普比率假设收益近似正态分布且波动可代表风险,但若策略收益分布偏斜或肥尾严重,仅靠Sharpe可能不足以全面评价风险。

3. 最大回撤(Max Drawdown)

衡量策略在运行过程中净值从高点回落的最大幅度。计算时,先构建策略净值曲线 \({"NAV"\ }_{t}\) (从收益率序列累乘得到),然后计算任意时点净值相对之前最高净值的回撤比例。最大回撤即最大值:

\[\text{MDD} = \max_{0 \leq i < j \leq T}\frac{\text{NAV}_{i} - \text{NAV}_{j}}{\text{NAV}_{i}}\]

其中 \({"NAV"\ }_{i}\) 是第 \(\ = \ 3\ \backslash*\ roman\ iii\) 天之前的峰值净值,\({"NAV"\ }_{j}\) 是之后某天的较低净值。简单来说,MDD表示策略曾经亏损过多少(相对之前最高点)。例如,最高净值100万元回落到80万元又反弹,那么那段时期最大回撤为20%。最大回撤越小越好,因为回撤大意味着需要更高的盈利才能弥补(例如亏50%需要赚100%才能回本)。投资者普遍关注此指标来衡量策略抗风险能力。

信息比率(Information Ratio):衡量策略相对于基准的超额收益风险比,计算类似\(\text{Sharpe}\),但用基准替代无风险收益。公式为:

\[R = \frac{E\left\lbrack R_{p} - R_{b} \right\rbrack}{\sigma_{p - b}}\]

其中 \(R_{b}\) 是基准(如指数)的回报,分子是策略对基准的超额收益期望,分母是策略相对基准跟踪误差(tracking error)的标准差。信息比率高表示策略相对于基准有稳定的超额收益。例如,一个信息比率为0.5的基金长期跑赢指数且波动适中。信息比率主要用于评价相对收益策略(如量化对冲基金)对基准的超额贡献。

4.其他指标

还有许多补充指标可评估策略性能。例如年化波动率(衡量风险水平)、卡玛比率(Calmar Ratio,年化收益/最大回撤)、胜率(盈利交易次数占比)、盈亏比(平均盈利交易额/平均亏损交易额)等。这些指标从不同角度刻画策略收益分布和风险特征,常配合使用进行全方位评估。

在评价模型的交易性能时,应综合考虑收益-风险权衡。高收益率的策略如果伴随极高的回撤和波动,未必是投资者能接受的。同样,风险极低但几乎无收益的策略意义也不大。夏普比率、信息比率帮助平衡收益和波动风险,而最大回撤关注极端风险场景下损失幅度。通常,一个稳健的策略应当在这些指标上都有不错的表现。

假设已经有一个策略的每日收益率列表(例如通过回测得到模型产生的交易信号下每天投资组合的涨跌幅)。可以用Python计算上述评估指标。下面的代码生成一组模拟的每日收益率,并计算年化收益率、夏普比率和最大回撤:

import numpy as np

# 1) Simulate one year of daily returns (252 trading days)
np.random.seed(0)
daily_returns = np.random.normal(
    loc=0.0005,   # small positive drift
    scale=0.01,   # daily volatility
    size=252
)

# 2) Annualized return
total_return = np.prod(1 + daily_returns) - 1
annualized_return = (1 + total_return) ** (252 / len(daily_returns)) - 1

# 3) Sharpe ratio (assume risk-free rate ~ 0)
sharpe_ratio = (daily_returns.mean() / daily_returns.std(ddof=1)) * np.sqrt(252)

# 4) Max drawdown
equity_curve = np.cumprod(1 + daily_returns)              # start at 1
running_max = np.maximum.accumulate(equity_curve)         # rolling peak
drawdown = 1 - (equity_curve / running_max)               # drawdown series
max_drawdown = drawdown.max()

print(f"Annualized Return: {annualized_return:.2%}")
print(f"Sharpe Ratio: {sharpe_ratio:.2f}")
print(f"Max Drawdown: {max_drawdown:.2%}")
示例输出:
Annualized Return: 21.22%

Sharpe Ratio: 1.30

Max Drawdown: 14.11%
上述结果解释:该模拟策略年化收益约21.22%,夏普比率约1.30,最大回撤约14.11%。这表示策略在一年内取得了约21%的收益,单位波动风险获得1.3的超额收益,比典型股票市场长期夏普0.5左右高出不少,且历史上最糟糕时刻曾亏损14%。从这些指标来看,策略收益较可观且风险调整后表现优秀,回撤也在可控范围。当然,这是随机生成的数据示例,不代表真实策略性能。

在实际回测中,可以进一步计算信息比率等指标。如果有基准(比如同期标普500指数收益率序列),可以类似地计算策略相对标普的超额收益序列,然后计算信息比率。此外,还可以绘制净值曲线图、月度收益热力图等来辅助评估。但无论采用何种评估方式,务必确保评估基于模型未见过的测试数据或模拟交易结果,避免对训练集内表现的过度乐观估计。只有经过严格回测并在多个市场环境下表现良好的模型,才有希望在实盘中取得优异成绩。

4.4 数学推导

4.4.1 支持向量机(SVM)的优化推导

在正文中给出了SVM软间隔形式的优化公式,这里简要推导其对偶问题和支持向量的形成。对于线性可分情况(硬间隔),SVM优化为:

\[\min_{w,b}\frac{1}{2}|w|^{2},\]
\[\text{s.t. }y_{i}\left( w^{T}x_{i} + b \right) \geq 1,\forall i\]

构建拉格朗日函数:

\[L(w,b,\alpha) = \frac{1}{2}|w|^{2} - \sum_{i = 1}^{m}{\alpha_{i}\left\lbrack \, y_{i}\left( w^{T}x_{i} + b \right) - 1\, \right\rbrack}\]

其中 \(\alpha_{i} \geq 0\) 是拉格朗日乘子。对偶问题来自对 \(w,\, b\) 求偏导令其为0:

\[\frac{\partial L}{\partial w} = w - \sum_{i = 1}^{m}{\alpha_{i}y_{i}x_{i}} = 0\]

可得

\[w = \sum_{i = 1}^{m}{\alpha_{i}y_{i}x_{i}}\]
\[\frac{\partial L}{\partial b} = - \sum_{i = 1}^{m}{\alpha_{i}y_{i}} = 0\]

可得

\[\sum_{i = 1}^{m}{\alpha_{i}y_{i}} = 0\]

将上述结果代入拉格朗日函数并消去 \(w,\, b\),得到对偶优化问题:

\[\max_{\alpha_{i} \geq 0}{\sum_{i = 1}^{m}\alpha_{i}} - \frac{1}{2}\sum_{i = 1}^{m}{\sum_{j = 1}^{m}{\alpha_{i}\alpha_{j}y_{i}y_{j}\left( x_{i} \cdot x_{j} \right)}}\]

这是一个二次规划,对偶变量为 \(\alpha_{i}\)。优化结果满足KKT条件:只有当约束严格等号成立时(即

\[y_{i}\left( w^{T}x_{i} + b \right) = 1\]

对应的 \(\alpha_{i}\) 才可能 $ \, 0$。这些点被称为支持向量,它们恰好落在边界上,对最优超平面的位置起决定作用。求解对偶问题可得每个支持向量的 \(\alpha_{i}\),从而通过

\[w = \sum_{}^{}{\alpha_{i}y_{i}x_{i}}\]

得到法向量 w,并由KKT条件

\[\alpha_{i}\left\lbrack y_{i}\left( w^{T}x_{i} + b \right) - 1 \right\rbrack = 0\]

选取任一支持向量求出偏置 \(b\)。这样就确定了分类决策函数:

\[f(x) = \sum_{i \in SV}^{}{\alpha_{i}y_{i}\left( x_{i} \cdot x \right)} + b,\]

预测时取 \(\text{sign(f(x))}\)判断类别。

对于非线性可分情况,软间隔SVM在原优化中引入松弛变量 \(\xi_{i}\) 及惩罚系数 \(C\),对偶问题会在约束中出现 \(0 \leq \alpha_{i} \leq C\) 的上界,但求解思路类似。核技巧(Kernel Trick)则是在对偶问题中用核函数 \(K\left( x_{i},x_{j} \right)\) 替换点积 \(x_{i} \cdot x_{j}\) ,从而使算法能在高维(甚至无限维)特征空间找到线性分类器,而不显式地计算坐标。

总的来说,SVM的理论保证了对训练误差和模型复杂度的权衡,可以在结构风险最小化框架下获得较好的泛化性能。

4.4.2 长短期记忆网络(LSTM)的内部机制

LSTM的公式在正文已经列出,这里补充一点直观理解和推导背景。LSTM通过把传统RNN的隐藏状态拆分成"隐藏状态 \(h_{t}\)"和"细胞状态 \(C_{t}\)"两部分,并增加门控控制信息流动。从RNN的角度看,标准RNN的状态更新是

\[h_{t} = \tanh\left( W_{h}h_{t - 1} + W_{x}x_{t} \right).\]

LSTM则将其扩展为一系列操作:首先计算遗忘门 \(f_{t}\)来衰减旧状态 \(C_{t - 1}\) ,计算输入门 \(i_{t}\) 和候选状态 \(\widetilde{C_{t}}\) 来决定增加的新信息,最终得到新的细胞状态 \(C_{t}\),然后根据输出门 \(o_{t}\) 决定输出的隐藏状态 \(h_{t}\) 。这些门的导出并非通过某个优化目标推导得到,而是LSTM结构设计者在人为增加 gating 单元后,依据避免梯度消失和梯度爆炸的经验来设置的。门控方程本身比较直接,就是将上一时刻隐藏状 \(h_{t - 1}\) 和当前输入\(\, x_{t}\,\)通过各自的权重矩阵线性变换后,送入激活函数(sigmoid或tanh)。比如遗忘门公式可以视为:

\[f_{t} = \sigma\left( U_{f}h_{t - 1} + W_{f}x_{t} + b_{f} \right)\]

这只是一个标准的全连接神经元,对 \(h_{t - 1}\)\(x_{t}\) 的连接权重合并表示为矩阵 \(W_{f}\)(在正文中用 \(\left\lbrack h_{t - 1},x_{t} \right\rbrack\) 符号表示了这种拼接)。同理可以得到输入门和输出门类似形式,以及候选记忆单元 \(\widetilde{C_{t}}\)\(\,\tanh\) 激活。然后细胞状态更新

\[C_{t} = f_{t}*C_{t - 1} + i_{t}*\widetilde{C_{t}}\]

是关键:这里的乘积和加法确保了原先 \(C_{t - 1}\) 中重要的信息(若 \(f_{t} \approx 1\))可以基本不变地传递下去,而不重要的信息(\(f_{t} \approx 0\))被遗忘,同时添加新信息(\(i_{t}\,\)控制添加多少)。通过这种线性传递\(\, C_{t}\),梯度在反向传播时可以在很长序列上保持不衰减(只要\(f\)门不一直趋近0或1),从而解决长期依赖问题。输出门最终控制从\(C_{t}\)中输出多少信息到\(h_{t}\)

\[h_{t} = o_{t}*\tanh\left( C_{t} \right)\]

因为只有当输出门开的时候(\(o_{t}\)接近1),细胞状态才大量流入隐藏状态供下一层或下一时刻使用,否则就被抑制。这就如同一个阀门控制信息输出。这套机制可以说是RNN领域通过结构设计而非数学推导获得的成果。它的有效性可以从梯度公式上分析:与普通RNN相比,LSTM的梯度多了门控导数项的乘积,但只要门控的导数不同时都很小,就能避免梯度过快衰减。总之,LSTM的推导主要在于理解这些门控方程来源于对RNN的改进设计,而非像SVM那样由优化问题推导而成。

强化学习Q-Learning公式推导:Q-learning基于BeLLMan最优方程导出。对于任意策略,BeLLMan期望方程描述了状态价值或Q值的递归关系。而BeLLMan最优方程针对最优策略,有:

\[Q^{*}(s,a) = E_{s^{'}}\left\lbrack \, r(s,a) + \gamma\max_{a^{'}}Q^{*}\left( s^{'},a^{'} \right)\, \right\rbrack,\]

其中 \(r(s,a)\) 是执行动作获得的即时奖励, \(\gamma\) 是折扣因子,期望取决于环境状态转移。

Q-learning并不直接求解这个期望方程,而是使用

迭代法:从任意初始\(Q(s,a)\)出发,不断用BeLLMan更新来逼近\(Q^{*}\)。第\(\, n\,\)次迭代令:

\[Q_{n + 1}(s,a) \leftarrow (1 - \alpha)Q_{n}(s,a) + \alpha\left\lbrack r(s,a) + \gamma\max_{a'}Q_{n}(s',a') \right\rbrack.\]

在学习率 \(\alpha\,\)适当减小、访问状态充分的条件下,\(Q_{n}(s,a)\) 会收敛到\(Q^{*}(s,a)\)。在正文中的公式正是这个更新规则的一个写法

推导这个更新等式其实来自将BeLLMan最优方程视为一种不动点,采用类似动态规划的价值迭代方法。直观理解是:用当前对于未来的价值估计(即 \(\max_{a'}Q(s',a')\) 部分)来更新当前\(Q\)值,如此反复逼近最优解。在实现上,Q-learning算法让智能体在环境中经历状态转换

\[s \rightarrow a \rightarrow r,s^{'},\]

然后依据获得的\(\, r\,\)和下一状态估计

\[\max_{a'}{Q\left( s^{'},a^{'} \right)}\]

按上述公式调整\(Q(s,a)\)。当过程遍历足够多次后,\(Q\)函数逐渐逼近最优,智能体行为也趋于最优策略(选择每个状态下Q值最大的动作)。以上是基于理论的推导说明。在实践中,由于状态空间巨大,用神经网络 $Q(s,a;\theta) \(来近似\)Q$值并通过拟合BeLLMan目标来更新参数 \(\theta\),这就是深度\(Q\)网络(DQN)的基本原理。

4.4.3 实际交易应用案例

为了将上述所有环节串联起来,这里提供一个综合案例:使用LSTM模型预测股票价格并进行回测评估。将以美国科技股苹果公司(AAPL)为例,展示从数据获取、模型训练到策略回测的流程。本案例仅用于演示深度学习应用于交易的过程,涉及的数据和模型参数可以按需调整。

1. 获取历史行情数据

使用yfinance库获取苹果公司2015年至2020年的历史日线价格数据,包括调整收盘价(考虑分红拆股)。这相当于的训练+测试数据。

import yfinance as yf
import pandas as pd

# Download historical data for AAPL from 2015-01-01 to 2020-01-01
data = yf.download(
    "AAPL",
    start="2015-01-01",
    end="2020-01-01",
    progress=False
)

# Prefer adjusted close if available, otherwise fall back to close
if "Adj Close" in data.columns:
    prices = data["Adj Close"]
else:
    prices = data["Close"]

print(prices.head())
这将下载AAPL股票在指定期间的每日价格。prices是一个Pandas Series,索引为日期。实际运行时需要确保网络连接正常并安装了yfinance库。示例输出(前几行)可能类似:
Date        Adj Close

2015-01-02  26.837507

2015-01-05  26.505341

2015-01-06  26.011019

2015-01-07  26.334650

2015-01-08  27.219362
(这里价格约26美元,是已对之后拆股做了调整的价格。)

2. 特征构造与数据集准备

将使用历史价格序列本身作为特征,通过一个LSTM来预测未来价格。具体做法是构建一个滑动窗口数据集:用前面的价格序列去预测下一个时刻的价格。例如取过去10天的价格序列作为输入特征,预测第11天的价格。这实际上是一种序列到序列的回归问题。预测目标可以是价格本身,或者简化为收益率/涨跌方向。在此案例中直接预测明日的价格,然后通过比较预测价和今日价来制定交易信号。

先将价格序列做归一化(使用Min-Max缩放到\(\lbrack 0,1\rbrack\)),以便LSTM训练更稳定。然后按照窗口长度切分数据。

import numpy as np
from sklearn.preprocessing import MinMaxScaler

# 1) Data preprocessing: normalization
scaler = MinMaxScaler(feature_range=(0, 1))

# Ensure prices is a NumPy array of shape (n_samples, 1)
price_values = prices.values.reshape(-1, 1)

scaled_prices = scaler.fit_transform(price_values)

# 2) Build sequence dataset
window = 10  # use past 10 days to predict the next day
X, y = [], []

for i in range(len(scaled_prices) - window):
    X.append(scaled_prices[i : i + window])
    y.append(scaled_prices[i + window])

X = np.array(X)  # shape: (num_samples, 10, 1)
y = np.array(y)  # shape: (num_samples, 1)

# 3) Train-test split (80% train, 20% test)
train_size = int(len(X) * 0.8)

X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

print("Training samples:", X_train.shape[0],
      "Testing samples:", X_test.shape[0])
此时,有了训练和测试用的时间序列样本。比如,如果总共有1258个交易日数据,使用10天窗口,则样本数约1248个,80%训练则训练样本近998个,测试样本250个左右。

3. 构建并训练LSTM模型

使用TensorFlow/Keras定义一个简单的LSTM网络。模型结构包括一层具有50个单元的LSTM层(输入维度是10天×1特征),接一个Dense全连接层输出单个值(预测下一天的价格)。使用均方误差(MSE)作为损失函数,Adam优化器训练模型5个epoch。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# 1) Define the LSTM model
model = Sequential([
    LSTM(
        units=50,                 # number of LSTM hidden units
        input_shape=(window, 1)   # (timesteps, features)
    ),
    Dense(1)                      # output layer: predict one value
])

# 2) Compile the model
model.compile(
    loss="mse",                   # mean squared error
    optimizer="adam"
)

# 3) Train the model
model.fit(
    X_train,
    y_train,
    epochs=5,
    batch_size=16,
    verbose=1
)
训练完成后,模型就学到了从过去10天价格预测下一天价格的映射关系。可以适当增加epoch数提升拟合,但需防止过拟合(可用验证集监控)。

4. 模型预测与交易策略

在测试集上让模型预测下一天价格,然后将预测值逆归一化还原为实际价格。一个简单的交易策略是:如果模型预测明天价格高于今天收盘价,则买入并持有一天,否则空仓。这样的日收益率就等于:如果信号=买入,则收益=(明日收盘价/今日收盘价) −1(明日收盘价/今日收盘价)-1(明日收盘价/今日收盘价)−1,如果信号=空仓则收益=0(假设空仓时资金持有现金无收益)。为简化,不考虑做空,如果要做空则当预测价格下跌时可以获得相应收益。

import numpy as np
import pandas as pd

# 1) Predict on test set (scaled)
pred_scaled = model.predict(X_test)

# 2) Inverse transform back to price level
pred_prices = scaler.inverse_transform(pred_scaled).flatten()   # predicted next-day price
actual_prices = scaler.inverse_transform(y_test).flatten()      # true next-day price

# --- Alignment notes ---
# X was built as: X[i] = prices[i : i+window], y[i] = prices[i+window]
# So each prediction corresponds to "today index" = i + window - 1
# and "next day index" = i + window

# In the original build, X_test starts at index = train_size in X space.
# Therefore, the first test prediction corresponds to:
start_i = train_size  # index in X/y space

# Map to prices index
today_idx_start = start_i + window - 1
next_idx_start = start_i + window

# Slice today and next prices to match the number of predictions
n = len(pred_prices)

today_prices = prices.iloc[today_idx_start : today_idx_start + n].values
future_prices = prices.iloc[next_idx_start : next_idx_start + n].values

test_dates = prices.index[next_idx_start : next_idx_start + n]  # dates of the "future" price

# 3) Generate trading signal:
# Long if predicted next-day price > today's price, else flat
signals = (pred_prices > today_prices).astype(int)

# 4) Strategy returns: if long, earn next-day return; if flat, earn 0
strategy_returns = np.where(signals == 1, (future_prices / today_prices) - 1.0, 0.0)

# Convert to Series for convenience
strategy_returns = pd.Series(strategy_returns, index=test_dates)

strategy_returns.head(), strategy_returns.describe()
这里signals是一个与测试集天数相同的数组,1表示买入持有一天,0表示空仓。需要注意索引对齐:用窗口长度之前的那天价格作为"今日价",相应地future_prices取测试集实际价格(明日价)。计算得到的strategy_returns即策略每天的回报率序列。

5. 策略绩效评估

最后,可以像第3节介绍的那样计算策略在测试集期间的表现。例如计算累计收益、年化收益率、夏普比率和最大回撤:

import numpy as np
import pandas as pd

# Make sure strategy_returns is a 1-D numpy array
if isinstance(strategy_returns, (pd.Series, pd.DataFrame)):
    strategy_returns_arr = np.asarray(strategy_returns).reshape(-1)
else:
    strategy_returns_arr = np.asarray(strategy_returns).reshape(-1)

# 1) Equity curve (start at 1)
strategy_equity = np.cumprod(1 + strategy_returns_arr)

# 2) Total and annualized return
total_return = strategy_equity[-1] - 1
annual_return = (1 + total_return) ** (252 / len(strategy_returns_arr)) - 1

# 3) Sharpe ratio (risk-free ~ 0)
sharpe = (strategy_returns_arr.mean() / (strategy_returns_arr.std(ddof=1) + 1e-12)) * np.sqrt(252)

# 4) Max drawdown
roll_max = np.maximum.accumulate(strategy_equity)
max_dd = np.max(1 - strategy_equity / roll_max)

print(f"Total Return: {total_return:.2%}")
print(f"Annualized Return: {annual_return:.2%}")
print(f"Sharpe Ratio: {sharpe:.2f}")
print(f"Max Drawdown: {max_dd:.2%}")
假设模型在测试集上的表现尚可(请注意这个策略非常简单,且LSTM未充分调参,在实际数据上可能并不理想,这里只是演示计算过程),可能得到如下输出:
Total Return: 15.30%

Annualized Return: 18.45%

Sharpe Ratio: 1.10

Max Drawdown: 8.20%
这表示在测试集约一年的数据中,策略总收益15.3%,折合年化18.45%,夏普1.10,最大回撤8.2%。这个结果相对于基准(假设同期标普500涨幅可能只有10%)是不错的,表明模型有一定Alpha获取能力。当然,这是基于历史数据的回测,未考虑交易成本,且模型结构和参数都很简单,实际应用中需要更深入的优化。

6. 可视化和进一步分析

还可以绘制策略净值曲线与标的资产价格曲线对比,以直观了解模型择时效果。例如,如果策略净值稳步上升且回撤小于资产本身,则证明模型在有效避开下跌并抓住了一些上涨。另外,可以统计策略在测试期的交易次数、胜率、平均盈亏比等微观指标评估模型交易行为是否合理。如胜率是否高于随机的50%,盈亏比是否大于1等。

需要强调的是,本案例主要为了展示深度学习模型在量化交易中的完整应用流程。为了追求可读性和运行速度,简化了许多步骤,如参数调优、更多特征的加入(只用了价格本身的序列)。在真实场景下,读者应结合特征工程方法,尝试添加技术指标、基本面数据等特征来丰富LSTM的输入,并仔细调参(例如调整LSTM层数、单元数、训练epoch等)以提高预测精度。同时,要将风险控制和交易成本纳入回测评估,确保策略具有实战价值。

通过这个综合示例,可以体会到深度学习模型(如LSTM)能够从海量的历史价格序列中自动学习复杂的时序关系,当其预测能力显著高于随机时,便可以转化为实际交易策略为带来收益。当然,金融市场瞬息万变,任何模型都有失效的可能,需要不断根据新数据重新训练和验证。希望本章节内容和示例能为读者应用机器学习、深度学习于量化交易提供一个入门指引,激发出更多创新思路。