☰
基于深度学习的股票量化交易实战:数据、特征与回测避坑指南
2026/10/1 19:35:01 网站建设 项目流程

简介:面向毕业设计、课程设计或入门量化研究的深度学习股票量化交易项目,围绕利用CNN、RNN等算法分析历史行情、挖掘股价波动规律并制定交易策略展开。资源共49个文件,约2.93MB,以Python脚本为主体(42个py),配以Jupyter Notebook实验、测试配置与readme说明,涵盖模型构建、数据加载、特征处理、策略交易及监控等完整环节。项目内置NLinear、DLinear、TFT、TSMixer等多种时序模型,以及LightGBM、CatBoost基线对照;同时提供行情数据下载、多变量时间序列处理、qmt实盘/模拟盘回调、止损逻辑等落地脚本。代码按models、utils、load_data、strategy、tests等目录组织,并配套pytest测试用例,结构清晰、复用性强,便于理解从数据清洗到模型部署的完整流水线。目前已有91人学习,适合希望快速参考完整项目架构、开展二次开发的Python与深度学习爱好者。

1. 基于深度学习的股票量化交易:为什么值得自己搭一套,而不是买现成策略

拿到一份“基于深度学习的股票量化交易”代码包,很多人第一件事是跑预测、看收益曲线,然后被一张漂亮的回测图冲昏头。但真实情况往往是:模型在训练集上准确率很高,一上实盘就蔫;或者回测年化翻倍,换一只股票就变成稳定亏损。这个方向真正难的不是堆一个LSTM或Transformer,而是把数据、特征、回测这三件事做扎实。

这篇文章不聊玄学,就按我实际做过的路径拆开讲:怎么取数、怎么构造训练样本、怎么训练模型、怎么写一个不骗自己的回测,以及最容易翻车的几个坑。适合有Python基础、想用深度学习做A股或美股日线因子验证的从业者。你要的不是跑通代码,而是让每一步都有据可查。

2. 数据管道:从行情到训练样本,先把“喂给模型的东西”做对

深度学习模型的性能上限绝大多数由数据决定,这一点在量化交易里体现得尤其明显。很多代码包里的模型结构并不复杂,真正影响结果的是你拿到的行情是否稳定、复权方式是否合理、特征里有没有混进未来信息。所以这一章先把数据管道搭好。

2.1 数据源选型:日线还是分钟线,复权方式怎么定

常见的数据源有akshare、tushare、baostock,以及针对美股的yfinance。对于个人研究和策略验证,我一般推荐akshare或tushare pro的日线数据。免费接口够用,但要注意频率限制和字段稳定性——akshare的接口偶尔会改列名,建议在代码里做一次rename和类型转换。

日线和分钟线的选择要看你预测的周期。做5日以上的持仓,日线数据足够;做日内高频交易则必须用分钟线,但分钟线对数据清洗和存储的要求高一个量级。新手起步我建议只用日线,先把特征工程和回测闭环跑通,再去碰分钟级数据。

复权是另一个高频踩坑点。股票分红送股后价格会产生跳空,不复权的数据会让模型以为发生了剧烈波动。前复权会随最新价格不断修正历史价格,导致整个时间序列不稳定。所以我一般用后复权(adjust参数里对应hfq),它把历史价格按分红送股调整后长期保持可比,不会因为后续行情变化而改写过去。

import akshare as ak import pandas as pd # 拉取贵州茅台日线,后复权 df = ak.stock_zh_a_hist(symbol="600519", period="daily", start_date="20150101", end_date="20240101", adjust="hfq") # 统一列名,后面所有模型代码都基于这套命名 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df = df[["date", "open", "close", "high", "low", "volume"]].copy() df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 检查缺失和重复 assert not df["date"].duplicated().any(), "日期有重复" print(df.head()) print(df.tail())

参数说明:symbol是股票代码,A股直接传类似“600519”的代码;period="daily"指定日线;adjust="hfq"表示后复权。接口返回的是字符串类型的日期,需要转成datetime再排序。这里加了一个断言,防止拿到重复日期,因为有些数据源在停牌日不会返回数据,正常情况不会重复。

交易日历也要留意。A股有节假日和临时停牌,数据里会缺一些日期。对日线模型来说,缺几天没太大影响,只要不把相邻交易日之间的间隔当作相同时间步就好。如果要做序列模型,建议保留原始交易日节奏,不要强行按自然日填充。

2.2 特征工程:用ta库生成技术指标,避免未来函数

原始OHLCV可以直接喂给模型,但效果通常不好。价格绝对数值受大盘和个股基数影响大,模型很难跨时间段学习。常见做法是生成两类特征:一类是收益率、波动率等相对量,另一类是RSI、MACD、布林带这类技术指标。技术指标本身不创造新信息,但它们把价格变化做了非线性变换,相当于给模型提供了更合适的特征尺度。

特征工程里最要命的是“未来函数”。所谓未来函数,就是在计算某个t时刻的特征时用到了t+1甚至更晚的数据。比如你想用“5日均线”作为特征,必须用截止到t时刻的收盘价计算,不能顺手把整个序列的均线算完再对齐。pandas的rolling默认是右对齐,也就是窗口包含当前行,这没问题;但如果你用了shift(-1)来对齐特征,就引入了未来数据。

import talib as ta import numpy as np # 基础收益率特征 df["ret_1"] = df["close"].pct_change(1) # 今日相对昨日的收益 df["ret_5"] = df["close"].pct_change(5) # 5日累计收益 df["ma_5"] = df["close"].rolling(5).mean() # 5日均线 df["ma_20"] = df["close"].rolling(20).mean() # 20日均线 df["std_20"] = df["close"].rolling(20).std() # 20日波动率 # 经典技术指标 df["rsi_14"] = ta.RSI(df["close"].values, timeperiod=14) df["macd"], df["macd_signal"], df["macd_hist"] = ta.MACD( df["close"].values, fastperiod=12, slowperiod=26, signalperiod=9) df["bb_upper"], df["bb_middle"], df["bb_lower"] = ta.BBANDS( df["close"].values, timeperiod=20, nbdevup=2, nbdevdn=2) # 删除前20行NaN数据 df = df.dropna().reset_index(drop=True)

逻辑说明:pct_change(5)计算的是从t-5到t的累计收益率,特征里没有混入未来信息。ta.RSI、ta.MACD、ta.BBANDS这些talib函数只使用传入序列的当前值及其之前的值,天然是因果的。要注意rolling和talib都会在序列开头产生NaN,所以最后统一dropna()。删掉的只是前20个交易日,对整体样本量影响很小。

如果你的环境装不上talib,可以用pandas手写RSI和MACD,效果差别不大。talib在Windows上有时安装麻烦,建议直接pip install ta-lib前先安装对应二进制包,或者换用pip install pandas-ta。但无论如何,特征计算完成后,建议打印出特征矩阵的最后几行,人工确认一下NaN是否清理干净,否则后面训练时会报错或引入隐式截断。

2.3 构造标签与样本划分:预测涨跌还是收益率

标签设计决定了你的策略语义。最常见的做法是把“未来N日收益率是否为正”作为二分类标签。预测单日涨跌噪声太大,胜率很难超过55%,所以一般预测未来5日或10日。你也可以做回归,直接预测未来收益率数值,但回归模型的输出分布不稳定,交易信号不好定阈值。

我一般用未来5日收盘价是否高于当前收盘价作为标签。这样模型学到的是中期趋势,而不是隔夜跳空噪声。标签构造时用shift(-5)把未来价格挪到今天这一行,再和今天的收盘价比大小。注意这行代码里确实用到了未来数据,但它只是标签,标签是训练时监督信号,不参与特征计算,不构成未来函数泄漏。

T = 20 # 序列长度,每个样本包含过去20个交易日 H = 5 # 预测周期,看未来5日 # 标签:未来第5天收盘价高于今天收盘价 => 1 df["label"] = (df["close"].shift(-H) > df["close"]).astype(int) feature_cols = [c for c in df.columns if c not in ["date", "label"]] X, y = [], [] for i in range(T, len(df) - H): X.append(df[feature_cols].iloc[i - T:i].values) y.append(df["label"].iloc[i]) X = np.array(X) # (样本数, T, len(feature_cols)) y = np.array(y) print(f"样本数: {X.shape[0]}, 序列长度: {X.shape[1]}, 特征数: {X.shape[2]}") print(f"正样本比例: {y.mean():.2f}")

参数说明:T=20表示用过去20根日K线预测未来5日;H=5是预测周期。循环从i=T开始,因为前20天没有完整的序列历史;终止条件是len(df)-H,因为标签需要未来5天的数据。构造出的X是三维张量,形状为(样本数, 20, 特征数),这就是LSTM标准输入格式。

这里有一个容易被忽略的统计问题:重叠窗口导致相邻样本高度相关。因为第i个样本和第i+1个样本共享19天历史,训练集和验证集如果按时间切分,边界附近会出现信息重叠。这不是未来函数,但会让验证集指标虚高。解决办法是让样本之间隔开H天,或者干脆按时间块划分训练集验证集,不要随机打乱。后面的训练代码会按时间顺序切分,就是为了避免这种信息泄漏。

3. 模型选型与训练:从MLP到LSTM,深度学习在量化里到底学什么

数据管道搭好后,进入模型环节。很多初学者一上来就堆Transformer,但金融数据的信噪比极低,模型容量越大越容易过拟合。我的建议是先从简单模型开始,做一条基线,再逐步加结构。

3.1 为什么LSTM适合序列建模,MLP做基线

股票数据天然是时间序列,每个样本里的20个交易日在时间上有先后顺序。MLP如果直接把每个时间步的特征展平成一维向量,等于强行丢掉时间顺序关系。LSTM通过门控机制逐时间步扫描输入,理论上能捕捉到“连续几天下跌后出现反弹”这类模式。

但LSTM不是神药。金融数据本身信噪比低,很多所谓规律是噪声。所以我会先跑一个MLP或逻辑回归作为基线。如果LSTM在验证集准确率和策略收益上不能显著超过MLP,问题大概率不在模型结构,而在特征或标签设计上。这个反向验证过程能帮你省下大量调参时间,也能避免在错误的道路上越走越远。

Transformer同样可以做序列建模,多头注意力能捕捉长距离依赖,但在几千个样本的日线数据上,它很容易过拟合。实践中LSTM在中等规模金融时序上稳定性更好,而且部署时显存占用小、推理速度快。如果你数据量超过10万条,再考虑换成Transformer也不迟。

3.2 网络结构与PyTorch实现

下面是一个标准的双层LSTM分类器。输入形状是(batch, seq_len, features),输出是一个概率值(logit)。之所以取最后一个时间步的输出,是因为我们预测的是未来,最后一个时间步聚合了整个序列的信息。如果取平均或最大池化,反而会稀释最近几天的行情变化。

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 最后一个时间步的隐含向量 return self.fc(last).squeeze(-1)

参数说明:hidden_size=64是LSTM隐层维度,决定了模型记忆容量,太小欠拟合,太大过拟合。num_layers=2是层数,两层LSTM能提取更高层的时间抽象,但训练更慢,也更容易记住噪声。batch_first=True让输入维度顺序更符合直觉。dropout=0.3放在LSTM层间和最后全连接层前,作用是随机屏蔽一部分神经元,逼迫模型学会更鲁棒的特征。

注意这里不能使用双向LSTM。双向LSTM每个时间步会同时看到前后两个方向的信息,在NLP里是合理的,但在时序预测里意味着t时刻会用到t+1之后的数据,这种结构上的未来泄漏是最难排查的。

3.3 训练循环:损失函数、优化器、早停与样本划分

训练时最关键的两件事:样本按时间顺序划分,标准化只用训练集统计量。很多人图省事,用train_test_split(X, shuffle=True),这在时序数据里是灾难,因为随机打乱会让模型“看到未来”。正确做法是按百分比切分:前70%训练,中间15%验证,最后15%作为模拟的样本外测试。

标准化也容易踩坑。如果对整个X计算mean和std,再切分训练集测试集,测试集的均值方差信息已经混进训练过程。所以必须先切分,再只从训练集计算标准化参数,然后应用到验证集和测试集。

from torch.utils.data import TensorDataset, DataLoader def split_data(X, y, train_ratio=0.7, val_ratio=0.15): n = len(X) idx1 = int(n * train_ratio) idx2 = int(n * (train_ratio + val_ratio)) X_tr, y_tr = X[:idx1], y[:idx1] X_val, y_val = X[idx1:idx2], y[idx1:idx2] X_te, y_te = X[idx2:], y[idx2:] return X_tr, y_tr, X_val, y_val, X_te, y_te X_tr, y_tr, X_val, y_val, X_te, y_te = split_data(X, y) # 只从训练集计算均值和标准差 mean = X_tr.mean(axis=(0, 1), keepdims=True) std = X_tr.std(axis=(0, 1), keepdims=True) + 1e-8 X_tr = (X_tr - mean) / std X_val = (X_val - mean) / std X_te = (X_te - mean) / std train_ds = TensorDataset( torch.tensor(X_tr, dtype=torch.float32), torch.tensor(y_tr, dtype=torch.float32) ) train_dl = DataLoader(train_ds, batch_size=256, shuffle=True) model = LSTMPredictor(input_size=X_tr.shape[2]) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) criterion = nn.BCEWithLogitsLoss() best_val_loss = float("inf") patience = 10 trigger = 0 for epoch in range(50): model.train() train_loss_sum = 0.0 for xb, yb in train_dl: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss_sum += loss.item() # 验证 model.eval() with torch.no_grad(): val_logits = model(torch.tensor(X_val, dtype=torch.float32)) val_loss = criterion(val_logits, torch.tensor(y_val, dtype=torch.float32)).item() if val_loss < best_val_loss: best_val_loss = val_loss trigger = 0 torch.save(model.state_dict(), "best_model.pt") else: trigger += 1 if trigger >= patience: print(f"Early stop at epoch {epoch}") break print(f"Epoch {epoch}: train_loss={train_loss_sum / len(train_dl):.4f}, val_loss={val_loss:.4f}")

逻辑说明:AdamW在学习率、权重衰减和解耦上比普通Adam更稳定,尤其配合Transformer和LSTM。weight_decay=1e-4是L2正则,抑制过拟合。clip_grad_norm_把梯度范数限制在1.0,防止RNN常见的梯度爆炸。早停机制记录最佳验证集loss,连续10个epoch不改善就停止,然后把保存的权重恢复出来。这比跑满固定epoch要省心的多,也是防止过拟合的最后一层保险。

训练完成后,在测试集上计算AUC和准确率。注意这里不需要再调整任何超参数,测试集在你整个调参过程中只能碰一次。如果碰两次,它就不再是真正的样本外数据了。

3.4 评估指标:准确率、AUC和策略收益哪个可信

模型训练结束后,大多数人第一个看的是准确率。但在类别不平衡时,准确率会骗人。比如正样本占60%,模型全部预测正类,准确率就有60%,但策略完全没有区分度。我一般看三个指标:AUC、预测为正时的样本内收益、以及最简单但最实用的“方向准确率”——只统计模型预测置信度高于0.6的那些样本,看其中多少真的上涨。

准确率和AUC只能描述模型的排序能力,不能直接代表策略收益。真正决定策略赚不赚钱的是模型在极端行情下的判断,尤其是大涨大跌时是否站对方向。所以评估时要把预测结果转成信号,然后立刻进入回测环节。只有回测资金曲线才是最终裁决者。

4. 回测框架与策略落地:别让回测结果骗了你

训练模型只完成了一半工作。接下来要写一个尽量尊重交易规则的回测器。很多人直接在预测概率上画收益曲线,忽略手续费、滑点、T+1限制,结果回测收益是真实交易的三倍以上。这个差距就是所谓的“回测美颜滤镜”。

4.1 信号生成:从概率到仓位

模型输出的是logit,要先经过sigmoid转成0到1的概率。然后设定买卖阈值。直接用0.5作为买卖线可以,但会造成交易频繁换手,因为概率经常在0.5附近抖动。我习惯设定一个缓冲区:概率高于0.6才买入,低于0.4才卖出,中间区域保持原仓位不变。这样能降低交易次数,也避免在震荡市里来回打脸。

import numpy as np def generate_signals(probs, buy_thresh=0.6, sell_thresh=0.4): """ 根据预测概率生成交易信号。 返回数组:1=买入,-1=卖出,0=持有不动。 """ signals = np.zeros(len(probs)) position = 0 # 0表示空仓,1表示持仓 for i, p in enumerate(probs): if position == 0 and p >= buy_thresh: position = 1 signals[i] = 1 elif position == 1 and p <= sell_thresh: position = 0 signals[i] = -1 return signals

参数说明:buy_thresh和sell_thresh构成了死区。阈值越高,交易越少,但有可能错过一波快速上涨;阈值越低,交易越多,手续费累积越重。我一般在验证集上试几组阈值,比如0.55/0.45、0.6/0.4、0.7/0.3,观察交易次数和收益曲线的变化。如果阈值稍微调高收益就大幅下降,说明模型信号很弱,这时要回去改特征或标签,而不是继续调阈值。

4.2 回测引擎:手续费、滑点与收益计算

下面这个回测函数是单标的、全仓进出的简化版。你会在真实项目中遇到很多限制,但核心框架先立起来,后面再加。买入时以收盘价上浮滑点成交,卖出时下浮,模拟最不利的成交环境。费用单边按0.15%算,包含佣金和印花税的粗略值。

def backtest(close, signals, fee_rate=0.0015, slippage=0.001): cash = 1.0 # 初始资金归一化为1 pos = 0.0 # 持仓市值 equity = [] for i in range(len(close)): if signals[i] == 1: # 买入:以收盘价+滑点成交,扣手续费 buy_price = close[i] * (1 + slippage) pos = cash * (1 - fee_rate) / buy_price cash = 0.0 elif signals[i] == -1: # 卖出:以收盘价-滑点成交,扣手续费 sell_price = close[i] * (1 - slippage) cash = pos * sell_price * (1 - fee_rate) pos = 0.0 equity.append(cash + pos * close[i]) equity = np.array(equity) total_return = equity[-1] / equity[0] - 1 # 最大回撤 peak = np.maximum.accumulate(equity) drawdown = (equity - peak) / peak max_drawdown = drawdown.min() return equity, total_return, max_drawdown

逻辑说明:买入时用close * (1 + slippage),卖出时用close * (1 - slippage),相当于保守估计成交价,把滑点成本算进去。equity记录每个交易日的总资产,包括现金和持仓市值。最大回撤用np.maximum.accumulate算历史峰值,再计算当前值相对峰值的下跌幅度。这个回测没有考虑当日买入不能当日卖出(A股T+1),后续要改进的话,需要把信号执行日期往后推一天,或者改用次日开盘价成交。

手续费和滑点参数是回测里最敏感的调参对象。一般我会做一次敏感性测试:把费率从0.1%调到0.3%,如果策略收益从正变负,说明策略毛收益很薄,实盘大概率亏钱。滑点也一样,换手越频繁、标的流动性越差,滑点越要放大。

4.3 参数:调仓频率与样本外测试

回测时还要匹配持仓周期。如果你的标签是“未来5日上涨”,那么比较合理的做法是每5个交易日调一次仓,而不是每天根据新预测换手。每天预测每天换仓,五个交易日里的信号变化会被滑点和手续费吃掉大部分收益。

一种常见落地方式是滚动调仓:每周最后一个交易日用模型预测未来一周涨跌,然后买入并持有5个交易日。这么做有两个好处,一是标签周期和持仓周期一致,二是降低交易频率,让费用模型压力变小。

样本外测试是回测闭环里的最后一道检验。训练集和验证集是你在调参过程中反复看的,不能作为最终判断。把模型从未见过的最近一段数据(比如最后10%)跑一遍信号和回测,如果收益和验证集差距很大,说明过拟合严重,需要回头降低模型复杂度或增加正则化。后面还有更严格的滚动验证方法,在那之前,先保留一段“金种子”测试集别碰。

5. 避坑指南:量化交易里最常见的5个翻车现场

这一章是整篇文章的血泪经验汇总。以下每个问题我都踩过,或者看着身边的人踩过。按“现象 → 原因 → 解决”写,方便你对着排查。

5.1 现象:训练集准确率95%,验证集只有51%

这是我见过最多的翻车现场。训练集上表现完美,一到验证集就打回原形,多半不是过拟合,而是数据泄漏。常见泄漏点有三个:标准化时用了全样本的均值和方差;特征计算时不小心用了未来数据;标签构造时由于shift方向错误,导致同一时刻的特征和标签重叠。

原因排查很简单。把训练好的模型在训练集上输出概率,随机抽几个样本,打印样本序列的最后一根K线日期和对应的标签日期,肉眼核对一下。如果特征里混入了未来信息,打印出来的日期关系会明显不对。解决方式是重建数据管道,标准化严格只用训练集统计量,特征统一用rolling和shift(0)计算,标签统一用shift(-H),然后重新划分数据集。

5.2 现象:回测年化50%,实盘或样本外直接亏钱

回测收益高得吓人,但一放到样本外就蔫了。原因通常是你在回测里多次调整了参数,比如买卖阈值、滑点、持仓周期,直到回测曲线好看为止。这套参数已经在偷偷借鉴未来的信息,相当于把测试集写进了训练过程。

解决思路是“参数冷冻”:把所有超参数的调整限制在训练集和验证集上,测试集只能跑一次。如果一定要做参数搜索,用嵌套式时间序列验证,即外层循环里把数据切分成多个训练/测试块,每次在内层训练块上调参,在外层测试块上验证。这套流程麻烦,但它是避免“回测幻觉”的最有效手段。

5.3 现象:模型预测准确率60%,策略却不赚钱

准确率和赚钱之间没有必然关系。模型可能只在波动小的日子里判断正确,而那些日子涨跌空间小,自然赚不到钱;真正的大涨和大跌它全判断错了,一次大亏就把前面的小赢吞掉。准确率是等权平均每个样本,而收益是按金额加权,两者逻辑不同。

解决方式是在模型评估阶段同时看策略收益、夏普比率和最大回撤。尤其是最大回撤,如果一个策略最大回撤超过30%,即使年化收益高也难坚持实盘。对个人投资者来说,心理承受能力和资金规模都会限制策略上线。

5.4 现象:换一只股票或换一段时间就完全失效

深度学习模型很容易学到单只股票特有的统计噪音,而不是通用的市场规律。比如用茅台的历史数据训练,模型可能学到“连续三天小阳线后大概率继续涨”,这在某一阶段成立,换到另一只股票或另一个行情阶段就失效。

对策是尽量做多股票混合训练。把几十只股票的样本拼在一起,每只股票在样本里占一块区域,模型被迫学习更普适的特征。如果担心不同股票的价格尺度差异,记得在标准化时按股票分组处理,不能用全市场统一的标准差。至于时间段失效,只能靠不断滚动更新模型,这是后话。

5.5 现象:GPU训练时内存不够,CPU预测时又慢到怀疑人生

日线数据量不大,但如果把特征维度做得太高,或者把序列长度拉到60天,再加上几千只股票,内存就会爆。另一个常见问题是基础班学生写训练循环时不做torch.no_grad(),在验证集上保留了梯度,导致显存持续累积。

解决方式有两个方向:一是砍数据,降低序列长度和特征维度,做一次相关性分析保留20到30个有效特征;二是改成批量循环,不要一次性把所有样本堆进GPU。预测阶段可以考虑把模型转成TorchScript或者ONNX,LSTM在CPU上也能跑得很快。如果实在太慢,减少层数或hidden_size,代价很小。

6. 最后一道工序:样本外滚动验证与模型上线前的检查清单

静态的一次样本外测试只能证明“这一刻没失效”,还不能证明策略可以长期使用。这里给出一个更严格的验证方法和一个基本检查清单,这是我每次上线前都会走一遍的流程。

6.1 滚动前推验证:walk-forward analysis

把时间序列切成多个窗口,比如用前3年训练,后6个月测试,然后窗口向前移动3个月,重新训练重新测试。这样会得到多个独立的样本外结果,能直观看到策略在不同市场状态下的表现是否稳定。

def walk_forward(X, y, train_len=756, test_len=126, step_len=63): results = [] start = train_len while start + test_len < len(X): X_train = X[:start] y_train = y[:start] X_test = X[start:start + test_len] y_test = y[start:start + test_len] # 训练并回测,记录指标 metrics = train_and_evaluate(X_train, y_train, X_test, y_test) results.append(metrics) start += step_len return results

参数说明:train_len=756约等于3年交易日,test_len=126约等于6个月,step_len=63约等于3个月滑步。每次训练都只用测试窗口之前的数据,保证没有任何未来信息泄漏。滚动验证如果显示收益在多个窗口中有正有负,说明策略不稳定;如果多数窗口为正,且回撤可控,才值得考虑实盘。

6.2 三个必做的稳健性测试

第一,改变随机种子重训三次,看指标波动。如果收益方差太大,说明模型很不稳定,需要降低复杂度。第二,把交易成本参数放大一倍,观察策略是否还能盈利。第三,按牛熊市分段回测,比如分别测试2018年熊市和2019年牛市,如果其中一个区间剧烈亏损,至少要知道亏在哪里。

6.3 上线前的检查清单:从模型到实盘的最后几步

上线前我会重新读一遍数据管道代码,确认没有未来函数,然后拿最新200个交易日做模拟盘,每天记录模型的预测概率,第二天对比实际涨跌,坚持一个月,统计胜率。实盘只投入很小比例资金,比如总资金的5%,因为再好的回测也不能保证未来。每次实盘交易后记录实际滑点和手续费,和回测参数做对比,偏差超过预期就回头修参数。

我早期做这个方向时最大的教训是把90%的时间花在调模型结构上,后来才发现数据泄漏和回测成本才是决定盈亏的关键。现在每做一个新策略,先跑通数据管道和回测,再回头调模型。这个顺序颠倒过来的话,损失的时间和金钱都会让人印象深刻。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询