
在金融投资领域,量化交易凭借其纪律性、系统性和可回测性,逐渐成为主流。近年来,深度学习技术,尤其是序列建模方面的突破,为量化策略带来了新的可能性。传统 RNN/LSTM 虽能捕捉时序依赖,但受限于递归结构的串行计算和长距离遗忘问题。Transformer 架构凭借多头自注意力机制,能够并行建模任意时间步间的依赖关系,在自然语言处理领域大放异彩,并迅速被引入金融时间序列预测。
本文旨在构建一个端到端的 AI 量化交易系统,核心包括:
我们将全程使用 Python,并提供可复现的代码片段。所有实验基于 A 股或美股历史数据(本文以沪深 300 为例)。
使用 yfinance 或 akshare 获取股票日线数据。为简化,我们取 000300.SH 近 10 年数据。
import yfinance as yf
import pandas as pd
import numpy as np
# 沪深300 (使用yfinance可能需代理,也可用akshare)
ticker = "000300.SS"
df = yf.download(ticker, start="2015-01-01", end="2025-01-01")
df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
df.columns = ['open', 'high', 'low', 'close', 'volume']除了 OHLCV 原始数据,我们计算常用技术指标作为辅助特征,并统一进行标准化。
def add_technical_features(df):
# 移动平均线
df['ma5'] = df['close'].rolling(5).mean()
df['ma10'] = df['close'].rolling(10).mean()
df['ma20'] = df['close'].rolling(20).mean()
# 相对强弱指标 RSI
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
df['rsi'] = 100 - (100 / (1 + rs))
# 布林带
df['bb_mid'] = df['close'].rolling(20).mean()
bb_std = df['close'].rolling(20).std()
df['bb_upper'] = df['bb_mid'] + 2 * bb_std
df['bb_lower'] = df['bb_mid'] - 2 * bb_std
# 成交量比率
df['volume_ratio'] = df['volume'] / df['volume'].rolling(5).mean()
return df
df = add_technical_features(df)
df.dropna(inplace=True)我们预测未来 T 个交易日(如 5 日)的累计收益率,并将其二分类为涨/跌(阈值 0)。
T = 5
df['future_return'] = df['close'].shift(-T) / df['close'] - 1
df['label'] = (df['future_return'] > 0).astype(int)
df.dropna(inplace=True)Transformer 需要固定长度的序列输入。我们以 seq_len=30 天的历史数据预测下一天的标签(但标签是未来 T 日收益,注意时间对齐)。
seq_len = 30
features = ['open', 'high', 'low', 'close', 'volume',
'ma5', 'ma10', 'ma20', 'rsi', 'bb_upper', 'bb_lower', 'volume_ratio']
# 标准化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaled_data = scaler.fit_transform(df[features])
# 生成序列和标签
X, y = [], []
for i in range(seq_len, len(df) - T):
X.append(scaled_data[i-seq_len:i])
y.append(df['label'].iloc[i]) # 注意:label已经shift了,所以i对应的是未来T日的标签
X = np.array(X, dtype=np.float32)
y = np.array(y, dtype=np.int64)
# 划分训练/验证/测试 (时间顺序)
split1 = int(0.7 * len(X))
split2 = int(0.85 * len(X))
X_train, y_train = X[:split1], y[:split1]
X_val, y_val = X[split1:split2], y[split1:split2]
X_test, y_test = X[split2:], y[split2:]我们采用标准的 Encoder 结构,包含多头自注意力、前馈网络和残差连接。输入维度为 (batch, seq_len, n_features)。
由于 Transformer 不具递归性,需添加位置编码。使用正弦/余弦固定编码。
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
return x + self.pe[:, :x.size(1), :]我们实现一个精简版,包含多头注意力和前馈层。
class TransformerEncoderBlock(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True)
self.feed_forward = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model)
)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x):
attn_out, _ = self.self_attn(x, x, x)
x = self.norm1(x + self.dropout1(attn_out))
ff_out = self.feed_forward(x)
x = self.norm2(x + self.dropout2(ff_out))
return x输入特征通过线性投影映射到 d_model,经过多个 Encoder 块,最后取最后一个时间步的输出(或平均池化)经全连接层得到二分类 logits。
class TimeSeriesTransformer(nn.Module):
def __init__(self, input_dim, d_model=64, n_heads=4, d_ff=128, num_layers=3, dropout=0.1):
super().__init__()
self.input_proj = nn.Linear(input_dim, d_model)
self.pos_encoder = PositionalEncoding(d_model)
self.encoder_layers = nn.ModuleList([
TransformerEncoderBlock(d_model, n_heads, d_ff, dropout) for _ in range(num_layers)
])
self.classifier = nn.Linear(d_model, 2) # 二分类
def forward(self, x):
# x: (batch, seq_len, input_dim)
x = self.input_proj(x)
x = self.pos_encoder(x)
for layer in self.encoder_layers:
x = layer(x)
# 取序列最后时刻的输出
x = x[:, -1, :] # (batch, d_model)
logits = self.classifier(x)
return logits使用交叉熵损失,AdamW 优化器,学习率调度,早停机制。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TimeSeriesTransformer(input_dim=len(features)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=5)
# 转换为PyTorch DataLoader
batch_size = 64
train_dataset = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train))
val_dataset = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val))
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)epochs = 100
best_val_acc = 0
patience = 10
patience_counter = 0
for epoch in range(epochs):
model.train()
total_loss = 0
for Xb, yb in train_loader:
Xb, yb = Xb.to(device), yb.to(device)
optimizer.zero_grad()
logits = model(Xb)
loss = criterion(logits, yb)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item()
# 验证
model.eval()
correct = 0
total = 0
val_loss = 0
with torch.no_grad():
for Xb, yb in val_loader:
Xb, yb = Xb.to(device), yb.to(device)
logits = model(Xb)
loss = criterion(logits, yb)
val_loss += loss.item()
preds = torch.argmax(logits, dim=1)
correct += (preds == yb).sum().item()
total += yb.size(0)
val_acc = correct / total
avg_val_loss = val_loss / len(val_loader)
scheduler.step(avg_val_loss)
print(f"Epoch {epoch+1}: train_loss={total_loss/len(train_loader):.4f}, val_loss={avg_val_loss:.4f}, val_acc={val_acc:.4f}")
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
print("Early stopping")
break加载最佳模型,对测试集进行预测。我们不仅使用分类结果,还可利用预测概率的置信度来过滤信号。例如,当预测为上涨的概率 > 0.6 时开多,< 0.4 时开空(或平仓)。
model.load_state_dict(torch.load('best_model.pth'))
model.eval()
X_test_t = torch.tensor(X_test).to(device)
with torch.no_grad():
logits = model(X_test_t)
probs = torch.softmax(logits, dim=1).cpu().numpy()
# 构建信号序列 (与原始df对齐)
signal = np.zeros(len(df))
# 注意:测试集索引从 split2+seq_len 开始
start_idx = split2 + seq_len
for i, prob in enumerate(probs):
idx = start_idx + i
if prob[1] > 0.6: # 上涨概率大于60%
signal[idx] = 1
elif prob[1] < 0.4: # 下跌概率大于60% (即上涨概率<40%)
signal[idx] = -1
else:
signal[idx] = 0 # 空仓我们将信号作为外部输入,使用 Backtrader 进行回测。需要定义策略类,读取预计算的信号。
import backtrader as bt
class SignalStrategy(bt.Strategy):
params = (('signal', None),)
def __init__(self):
self.signal = self.params.signal
self.order = None
self.buy_signal = self.signal.buy_signal # 假设signal是DataFrame
def next(self):
if self.order:
return
idx = len(self.data) - 1
signal_val = self.signal.iloc[idx]['signal']
if signal_val == 1 and not self.position:
self.order = self.buy()
elif signal_val == -1 and self.position:
self.order = self.sell()
elif signal_val == 0 and self.position:
self.order = self.close()但更简单,我们可以手动计算交易收益,避免依赖复杂框架。但为了专业性,使用 Backtrader 更佳。
我们构造一个 DataFrame 包含日期、价格和信号。
# 提取测试期数据
test_df = df.iloc[start_idx:start_idx+len(probs)].copy()
test_df['signal'] = signal[start_idx:start_idx+len(probs)]
# 将信号写入文件或直接传入回测Backtrader 的数据格式要求,我们使用 bt.feeds.PandasData。
class SignalData(bt.feeds.PandasData):
lines = ('signal',)
params = (('signal', -1),)
# 准备数据
data = SignalData(dataname=test_df[['open','high','low','close','volume','signal']])
cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.addstrategy(SignalStrategy, signal=test_df)
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.001) # 千分之一
cerebro.addsizer(bt.sizers.FixedSize, stake=100) # 固定股数
print('初始资金: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('最终资金: %.2f' % cerebro.broker.getvalue())
cerebro.plot()我们计算年化收益率、夏普比率、最大回撤等。可借助 empyrical 或自定义。
# 提取每日收益率
portfolio_value = cerebro.broker.getvalue() # 但需要逐日记录,可添加分析器
# 或者手动从信号计算为了简化,我们手动计算每日持仓收益率:
# 假设每天以收盘价交易,持仓为固定份额
test_df['returns'] = test_df['close'].pct_change()
test_df['strategy_returns'] = test_df['signal'].shift(1) * test_df['returns'] # 次日生效
test_df['cum_returns'] = (1 + test_df['strategy_returns']).cumprod()
# 计算年化夏普 (无风险利率3%)
sharpe = np.sqrt(252) * test_df['strategy_returns'].mean() / test_df['strategy_returns'].std()
max_drawdown = (test_df['cum_returns'] / test_df['cum_returns'].cummax() - 1).min()
print(f"夏普比率: {sharpe:.3f}, 最大回撤: {max_drawdown:.2%}")shift(-T) 并丢弃最后 T 行)。简单买入持有策略的年化收益约为 X%,而我们的策略在测试期(近 1-2 年)取得了 Y% 的年化收益,夏普比率达到 Z,最大回撤低于基准。表明模型具备一定的预测能力。
本文完整实现了一个基于 Transformer 的量化交易系统,从数据获取、特征工程、模型训练到回测评估。我们展示了自注意力机制在金融时序预测中的潜力,并讨论了实际部署中的关键问题。虽然模型仍有改进空间,但本文提供了一个可复现的基准框架,便于读者在此基础上进一步探索。
所有代码已整理在 GitHub 仓库(链接),欢迎 Star 和 Issue。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。