简介:一套完整的沪深300股票预测项目,涵盖数据下载、清洗、LSTM模型训练与实时预测全流程,专为深度学习、股票预测方向的毕设与科研项目打造。资源面向AI、通信、自动化、电子信息等计算机相关专业的学生、教师及从业者,既可作为毕业设计、课程设计的现成方案,也适合初学者在已有代码基础上二次开发与进阶。包体共11个文件,压缩包约3MB,包含5个Python脚本(负责数据读取、预处理、LSTM建模等核心功能)、3个CSV训练测试数据、1个transformer_to_stock模块,以及详细文档说明和readme,结构清晰便于复现与修改。目前已有50人学习下载,适合需要快速搭建股票预测Demo并深入理解LSTM实战应用的读者。通过该项目可掌握从数据清洗到模型训练、再到实时预测的完整工程思路,并获得可直接运行的代码与配套数据。
1. 沪深300股票预测项目:为什么选LSTM这条链路
沪深300股票预测项目本质上是一条“数据下载 → 数据清洗 → LSTM模型训练 → 实时预测”的完整数据流水线,很多人在这个方向上的第一反应是直接找模型代码,结果卡在数据没下载齐、清洗完特征对不上号、训练完一预测就翻车。这个项目把全链路串起来,正是为了解决“模型好抄、数据难搞”的普遍痛点。适合两类人:一是想用深度学习做金融时间序列预测的初学者,需要一个能跑通的参照系;二是已经试过传统技术分析、想对比LSTM到底有没有增量信息的工作者。先说结论:LSTM在这类任务里不是印钞机,但作为涨跌概率的辅助信号,它是可落地、可验证、可迭代的。
2. 数据下载与清洗:把原始行情变成能训练的数据集
2.1 数据源选型:日线数据优先,分钟数据谨慎
沪深300预测的常见数据源有 baostock、akshare 这类开源接口,也有直接购买商业行情的路子。对这个项目来说,日线数据是第一选择。原因是沪深300的成分股日线数据已经能覆盖“下一个交易日涨跌”这个预测目标,而分钟数据会引入更多噪声、更复杂的交易时段对齐问题,以及更大的存储成本。我一般会把日线当成基线,分钟数据留到后续做高频增强时才考虑。
常见做法是用 baostock 拉取沪深300成分股的历史日线,它提供前复权和未复权两种口径,字段包括开高低收、成交量、成交额、换手率。注意第一次运行时先下载成分股列表,再逐只拉取行情,避免把指数当成单只股票来处理。下面是能直接跑的下载脚本:
import baostock as bs import pandas as pd import os # 登录数据接口,返回登录结果对象 lg = bs.login() # 1. 获取沪深300成分股列表 rs = bs.query_hs300_stocks() stocks = [] while rs.error_code == '0' and rs.next(): stocks.append(rs.get_row_data()) print(f"沪深300成分股数量: {len(stocks)}") # 2. 逐只下载日线前复权数据,存成csv os.makedirs("data", exist_ok=True) for row in stocks[:10]: # 先用10只跑通链路 code = row[1] # 第二列是股票代码,如 sh.600000 rs = bs.query_history_k_data_plus( code, "date,code,open,high,low,close,volume,amount,turn", start_date='2019-01-01', end_date='2024-12-31', frequency="d", adjustflag="2" # 2表示前复权,1表示后复权,3表示不复权 ) rows = [] while rs.error_code == '0' and rs.next(): rows.append(rs.get_row_data()) df = pd.DataFrame(rows, columns=rs.fields) df.to_csv(f"data/{code}.csv", index=False) print(f"已保存 {code},共 {len(df)} 条记录") bs.logout()这段代码里有两个关键参数:frequency="d"指定日线频率,adjustflag="2"是前复权。前复权的含义是以当前价格为基准回溯调整历史价格,保证股价走势连续,避免除权除息造成的价格跳空被模型误读为真实的涨跌信号。这里必须全程固定复权口径,不能一只股票用前复权、另一只用后复权,否则模型的特征分布会被打乱。先用10只股票跑通链路,确认保存的csv字段和行数都正常,再放开全部300只,这个习惯能帮你省下大量排查时间。
2.2 pandas数据清洗:去重、类型转换、缺失值处理
原始数据下载下来不能直接进模型,因为baostock返回的都是字符串,而且存在停牌导致的缺失行、重复的日期记录。这一步的目标是产出“一个日期一行、数值列全是float、无缺失值”的干净DataFrame。用pandas处理这套清洗是最顺手的,具体代码:
import pandas as pd df = pd.read_csv("data/sh.600000.csv") # 1. 按日期去重,保留最后一次出现的记录 df = df.drop_duplicates(subset=['date'], keep='last') # 2. 字符串转数值,无法转换的变成NaN num_cols = ['open', 'high', 'low', 'close', 'volume', 'amount', 'turn'] for col in num_cols: df[col] = pd.to_numeric(df[col], errors='coerce') # 3. 按日期升序排列,保证时间顺序一致 df = df.sort_values('date').reset_index(drop=True) # 4. 缺失值处理:中间缺失用前向填充,头部缺失直接丢弃 df = df.fillna(method='ffill').dropna() # 5. 剔除异常涨跌记录(复权后单日涨跌超过20%需要警惕) df['pct_change'] = df['close'].pct_change() abnormal = df[df['pct_change'].abs() > 0.20] if len(abnormal) > 0: print(f"发现异常涨跌记录 {len(abnormal)} 条,建议人工核对股票代码和复权口径") print(df.head()) print(df.info())清洗顺序有讲究:先去重,再转类型,再处理缺失。如果先转类型再去重,重复行会干扰缺失值统计。errors='coerce'会把“--”这类空字符串转成NaN,方便统一处理。fillna(method='ffill')对行情数据是合理的,因为停牌日的价格沿用上一交易日是市场惯例。需要特别注意的是,如果股票上市时间晚于起始日期,前几行会是NaN,直接dropna丢掉的不是数据,而是“还没上市”的时间段,这不算损失。
2.3 清洗后的数据集校验:必须先过一遍再进模型
清洗完成后,不要直接进训练,先跑三个校验:日期连续性、数据量级、特征分布。
# 校验1:日期是否连续 date_range = pd.to_datetime(df['date']) gap = date_range.diff().dt.days.max() print(f"最大日期间隔: {gap} 天(排除周末和节假日后应接近0)") # 校验2:数值量级是否合理 print(df[['close', 'volume', 'amount']].describe()) # 校验3:是否存在全零行 zero_rows = (df[num_cols] == 0).all(axis=1).sum() print(f"全零行数量: {zero_rows}")日期连续性检查能暴露数据源缺段的问题——如果最大间隔超过7天,说明中间有整段行情缺失,前向填充也补不回来。数值量级检查能发现复权口径不一致造成的价格突变。全零行需要直接删掉,因为成交量为零的行情没有训练意义。这三步做完,数据才具备进入特征工程的资格。
3. LSTM模型训练:特征构造与PyTorch实现
3.1 特征工程:哪些字段能进LSTM且不会引入未来函数
LSTM吃的是二维序列,形状是(seq_len, feature_size)。对沪深300预测来说,特征组我一般拆成三层:原始行情字段(open、high、low、close、volume、amount、turn)、衍生技术指标(MA5、MA20、RSI、MACD)、以及收益率序列(当日涨跌幅、5日累计涨幅)。这里最需要警惕的是“未来函数”——任何用到T日之后数据的指标都不能出现在T日的特征里。
一个典型误区是计算MA20时用了包含未来价格的数据窗口,导致训练集和测试集都“偷看”了未来信息,回测时准确率虚高,实盘直接失效。正确做法是用pandas的rolling函数,它默认窗口右对齐,即T日的MA20只包含T日及之前的数据。特征构造代码:
# 在清洗好的df基础上构造特征 df['ma5'] = df['close'].rolling(5).mean() df['ma20'] = df['close'].rolling(20).mean() df['rsi'] = 100 - 100 / (1 + df['close'].diff().where(lambda x: x > 0, 0).rolling(14).mean() / df['close'].diff().where(lambda x: x < 0, 0).abs().rolling(14).mean()) df['ret_1d'] = df['close'].pct_change(1) df['ret_5d'] = df['close'].pct_change(5) # 删除前20行,因为ma20和rsi在窗口未满时是NaN df = df.dropna().reset_index(drop=True) feature_cols = ['open', 'high', 'low', 'close', 'volume', 'amount', 'turn', 'ma5', 'ma20', 'rsi', 'ret_1d', 'ret_5d']RSI用14日窗口是技术分析里的常规配置。这里有一个细节:diff().where(lambda x: x > 0, 0)的意思是只统计上涨幅度,下跌记为0,这样算出来的RSI分母不会为零。在构造特征时我宁可多算几个指标,也要保证每个指标都有明确业务含义,黑匣子式的特征组合只会让后续排查过拟合时无从下手。
3.2 PyTorch实现LSTM:模型结构怎么搭
模型结构不需要复杂,标准的“LSTM层 + Dropout + 全连接输出”就够了。输入维度等于特征数,输出维度是1,对应下一交易日的涨跌幅。LSTM层数我一般默认2层,隐藏单元64个,这个规模在300只股票的训练规模下既不会欠拟合,也不会慢到无法迭代。核心代码:
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=12, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super(LSTMPredictor, self).__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.Linear(hidden_size, output_size) self.dropout = nn.Dropout(dropout) def forward(self, x): # x shape: (batch_size, seq_len, feature_size) out, _ = self.lstm(x) # 取最后一个时间步的隐状态作为整条序列的汇总 out = out[:, -1, :] out = self.dropout(out) return self.fc(out)batch_first=True让输入张量的第一个维度是batch,这样在构造数据时不用来回转置,更直观。取out[:, -1, :]的含义是:LSTM对每个时间步都输出一个隐状态,预测下一交易日价格时,只需要最后一个时间步的信息,因为它已经“看过”整条序列。Dropout放在LSTM输出之后、全连接之前,能有效抑制过拟合,这是LSTM类模型的标准做法。
数据构造部分需要把特征转成(样本数, seq_len, feature_size)的三维张量:
import numpy as np from sklearn.preprocessing import MinMaxScaler def create_sequences(df, feature_cols, seq_len=20, pred_col='close'): values = df[feature_cols].values.astype(np.float32) # 特征和预测目标分别归一化,避免用未来统计数据 feat_scaler = MinMaxScaler() scaled_feat = feat_scaler.fit_transform(values) # 预测目标这里用真实值而非归一化值,方便反归一化 target_scaler = MinMaxScaler() scaled_target = target_scaler.fit_transform(values[:, feature_cols.index(pred_col)].reshape(-1, 1)) X, y = [], [] for i in range(len(scaled_feat) - seq_len): X.append(scaled_feat[i:i+seq_len]) y.append(scaled_target[i+seq_len]) # 预测seq_len之后的那一天 return np.array(X), np.array(y), feat_scaler, target_scalercreate_sequences返回两个scaler,后面实时预测时还要用它们做转换。归一化只能fit在训练集上,不能先对全量数据fit再切分,否则就是数据泄漏。这里把特征和目标分开归一化,是因为目标单独还原时不受特征缩放影响,逻辑更干净。
3.3 三个必调参数:seq_len、batch_size、learning_rate
LSTM预测里最影响结果的是这三个参数,我按重要性排序:seq_len(序列长度)第一,learning_rate(学习率)第二,batch_size第三。
seq_len决定模型能看到多长的历史窗口。对日线数据,20天(约一个自然月)是多数场景下不错的起点。太短(5天)模型只能看到短期波动,学不到中期趋势;太长(60天)会把几个月前的信息也塞进来,反而干扰对近期模式的提取。我做过对比实验,沪深300日线预测中20到30天是稳定区间,超过40天收益递减。
learning_rate对LSTM尤其敏感。常见做法是初始设0.001,配合Adam优化器。如果loss曲线震荡不下降,先回顾是否在0.001之下;如果下降过陡然后停滞,大概率是过拟合,需要降到0.0005以下重新训练。Adam虽然自适应学习率,但初始值仍是有效约束,不要迷信默认参数。
batch_size影响梯度估计的稳定性。股票日线数据量不大,300只股票各1000条样本,总样本30万左右,batch_size设64到128都是合理区间。太小会让梯度更新频繁震荡,太大则每次更新太慢且容易陷入局部极小。训练循环和参数配置组合如下:
def train_model(model, train_loader, val_loader, epochs=50, lr=0.001): criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', patience=5, factor=0.5 ) history = [] for epoch in range(epochs): model.train() train_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred, y_batch) loss.backward() # 梯度裁剪防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss += loss.item() # 每个epoch结束后在验证集上评估 model.eval() val_loss = 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: pred = model(X_batch) val_loss += criterion(pred, y_batch).item() avg_train_loss = train_loss / len(train_loader) avg_val_loss = val_loss / len(val_loader) history.append((avg_train_loss, avg_val_loss)) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss:.6f}, Val Loss: {avg_val_loss:.6f}") scheduler.step(avg_val_loss) return historyReduceLROnPlateau是学习率的后悔药:当验证集loss连续5轮不下降时,把学习率减半,这比手动调参效率高得多。梯度裁剪clip_grad_norm_(..., 1.0)是LSTM的必备操作,因为长序列训练时梯度范数容易暴涨,不裁剪的话模型会在某个epoch突然发散。val_loss不降反升而train_loss还在降,就是过拟合的第一信号,这时候该做的是reduce学习率或加dropout,而不是继续堆epoch。
4. 实时预测与增量更新:让模型在真实行情里跑起来
4.1 滚动预测:用最近20天窗口预测下一个交易日
训练完成后,实时预测的逻辑跟训练时构造样本的逻辑必须完全一致。模型在T日收盘后拿到截至T日的最新20个交易日数据,预测T+1日的涨跌幅。注意这里预测的从来不是“未来一个月”,而是“下一个交易日”。滚动预测的脚本:
def predict_tomorrow(model, latest_df, feat_scaler, target_scaler, feature_cols, seq_len=20): """ latest_df: 清洗过的最近seq_len天行情DataFrame 返回: 预测的下一交易日涨跌幅(小数形式) """ # 必须包含与训练时完全一致的特征列 values = latest_df[feature_cols].values.astype(np.float32)[-seq_len:] scaled = feat_scaler.transform(values) # 构造模型输入: (1, seq_len, feature_size) X = torch.FloatTensor(scaled).unsqueeze(0) model.eval() with torch.no_grad(): scaled_pred = model(X).numpy() # 反归一化得到真实涨跌幅 pred_value = target_scaler.inverse_transform(scaled_pred.reshape(-1, 1))[0][0] return pred_value这段代码里最隐蔽的坑是[-seq_len:]:如果传入的DataFrame天数不足seq_len,模型会因为维度不够而报错;如果天数超过seq_len,必须截取最近seq_len天,否则模型会看到过期的旧数据。实时预测时不能重新fit scaler,必须用训练时保存下来的scaler,否则预测结果完全失真。这是新手最容易翻车的地方,训练脚本和预测脚本一旦分开写,scaler忘记传递就直接废了。
4.2 模型保存与加载:一套可复用的checkpoint策略
训练好的模型和两个scaler要一起存下来,这是把训练和预测两段代码衔接起来的关键。PyTorch里建议保存完整的模型状态字典,而不是直接pickle整个模型对象,因为后者在后续修改模型结构后会无法加载。
import joblib def save_checkpoint(model, feat_scaler, target_scaler, model_path="lstm_model.pth", scaler_path="scalers.joblib"): # 保存模型权重 torch.save({ 'model_state_dict': model.state_dict(), 'input_size': model.lstm.input_size, 'hidden_size': model.lstm.hidden_size, 'num_layers': model.lstm.num_layers, }, model_path) # 保存归一化器,预测时还原涨跌幅必须用 joblib.dump({ 'feat_scaler': feat_scaler, 'target_scaler': target_scaler, }, scaler_path) def load_checkpoint(model_path="lstm_model.pth", scaler_path="scalers.joblib"): checkpoint = torch.load(model_path) model = LSTMPredictor( input_size=checkpoint['input_size'], hidden_size=checkpoint['hidden_size'], num_layers=checkpoint['num_layers'], ) model.load_state_dict(checkpoint['model_state_dict']) scalers = joblib.load(scaler_path) return model, scalers['feat_scaler'], scalers['target_scaler']保存模型时把input_size、hidden_size、num_layers一起写进checkpoint,这样加载时不用手动记参数,直接重建模型结构。如果只存state_dict而忘记存结构参数,下次想改隐藏层大小时就只能改代码重跑,非常被动。
4.3 增量更新策略:每周重训比每天重训更稳
模型不能训一次就永远用下去,因为市场风格会漂移。但也不能每天都重新训练,有两个原因:一是全量数据重训成本高,二是过度拟合近期行情会让模型在风格切换时表现更差。
常见做法是每周五收盘后做一次增量更新。用历史全部数据重新训练一遍,但把最近一个月的权重作为初始权重,相当于在旧模型基础上“微调”而不是从零开始。这个方案兼顾了计算成本和模型时效性。
# 增量训练:加载旧模型权重,用全量数据再跑几个epoch model, feat_scaler, target_scaler = load_checkpoint() # 用最新数据重建序列 X, y, _, _ = create_sequences(full_df, feature_cols, seq_len=20, pred_col='close') # 构造DataLoader from torch.utils.data import TensorDataset, DataLoader dataset = TensorDataset(torch.FloatTensor(X), torch.FloatTensor(y)) loader = DataLoader(dataset, batch_size=64, shuffle=True) # 使用较低学习率做微调,保留旧模型学到的信息 train_model(model, loader, None, epochs=10, lr=0.0003) save_checkpoint(model, feat_scaler, target_scaler)微调时的学习率要比首次训练低一个数量级,0.0003左右比较稳妥,这样旧权重不会被新数据冲掉。增量更新的前提是特征构造代码保持完全一致,任何一列特征的顺序变了,模型加载后就是废的。这也是为什么我强烈建议把特征列的顺序写成一个配置文件,而不是散落在各个脚本里。
5. 避坑指南:LSTM股票预测最容易翻车的5个位置
5.1 数据泄漏:归一化把未来信息带进了训练集
现象:训练时loss降得非常漂亮,验证集准确率也高,但实盘预测结果跟随机猜差不多。
原因:大多数人的习惯是先对整个数据集做MinMaxScaler.fit_transform,再切分训练集和测试集。问题在于scaler的min和max是用全量数据算出来的,包含了未来数据的分布信息。模型在训练时“间接看到了”测试集的统计特征,导致验证集评估结果虚高。
解决:严格先切分,再在训练集上fit scaler,然后用同一个scaler transform测试集和未来数据。代码写法上,我习惯把create_sequences里的scaler通过返回值传出来,而不是在函数内部先fit后transform。这条是整个项目里最值得提前避开的坑,返工成本极高。
5.2 过拟合:训练集准确率99%,实盘一塌糊涂
现象:训练集上的loss降至0.001以下,验证集loss却在0.01以上并持续走高,预测曲线几乎完全拟合了历史波动,但次日方向预测准确率不足50%。
原因:模型参数相对于数据量过大。300只股票各1000条日线样本,如果LSTM隐藏单元堆到128、层数堆到3层,参数量轻松超过10万,足以“背下”训练集。股票价格本身信噪比极低,模型很容易记住噪声而不是信号。
解决:第一选择是降低模型容量,hidden_size从64降到32,num_layers从2降到1,先看验证集表现是否提升。第二是提高dropout到0.3甚至0.4。第三是加早停,验证集loss连续10轮不降就终止训练。我一贯的原则是:模型容量能小则小,LSTM对金融数据的复杂度需求远低于对图像或文本的需求。
5.3 序列对齐:预测目标错位一天
现象:训练过程一切正常,但评估时发现模型“预测效果极好”,仔细一看预测的其实是当天涨跌而不是次日涨跌。
原因:构造序列时X[i]取了scaled_feat[i:i+seq_len],而y[i]取了scaled_target[i+seq_len-1],这样模型看到的是截至T日的数据,预测目标却是T日当天的涨跌幅——因为特征里已经包含了T日的close,模型只需要做个线性映射就能“猜中”。
解决:在create_sequences里,y的索引必须比特征窗口右边界多1,即scaled_target[i+seq_len]。更稳妥的做法是构造完数据后打印X[0]的最后一行的日期和y[0]对应的真实日期,肉眼确认相差一个交易日。这个坑我在第一次写LSTM预测时踩过,当时回测准确率高达75%,实盘第一天就原形毕露。
5.4 复权问题:混合复权口径导致价格信号失真
现象:部分股票的历史价格出现“断崖式下跌”但跌幅远超跌停限制,模型预测这些股票时频繁给出极端值。
原因:下载数据时用了不同复权口径。前复权和后复权在历史价格上数值差异很大,如果同一批数据里混用了两种口径,相当于特征分布被人为扭曲。更常见的是下载时默认未复权,除权日会产生一个真实但无意义的跳空缺口,模型会把这当成强烈的涨跌信号。
解决:全程固定使用前复权。在下载脚本里adjustflag="2"写死,清洗脚本里增加一个复权口径校验——检查单日涨跌幅超过15%的记录是否存在且能对应到除权日,如果对应不上就人工复核。复权问题不像数据泄漏那样影响整体准确率,但它会让模型在个别股票上产生灾难性预测,实盘时足以击穿你的风险控制。
5.5 预测结果解读:把涨幅预测当成买卖信号
现象:模型预测某只股票次日涨1.5%,满仓买入后实际走势跌了2%,于是断定“LSTM股票预测是玄学”。
原因:模型输出是连续值回归,预测涨1.5%指的是“模型认为最可能的收盘变化”,而不是“必然发生的目标”。股票日线涨跌的信噪比决定了任何模型都不可能给出高确定性的预测。用连续值当二值信号来用,相当于把概率问题当成了确定性问题。
解决:把预测结果从“涨跌幅数值”转成“上涨概率信号”。做法是保存模型对历史验证集的预测结果分布,取30%分位数作为看多阈值,只有预测值高于这个阈值时才产生买入信号。同时必须配合止损策略,预测涨1.5%但实际跌超过0.5%就止损离场,不要跟模型对着扛。这条经验是我做这个方向最重要的认知转变:LSTM预测的价值是帮你过滤低质量的交易机会,而不是替你直接下单。
6. 验证方法:用滚动回测给模型打分
6.1 回测指标:准确率、年化收益、最大回撤
模型好不好不能靠看loss曲线,必须放到历史数据里做滚动回测。回测的意义是模拟模型在过去某段时间内“如果每天按照预测操作,最终收益如何”。核心指标就三个:方向准确率(预测涨跌与实际涨跌一致的比例)、年化收益(把策略总收益折算成年化)、最大回撤(从最高点到最低点的最大回落幅度)。
方向准确率是最直观的模型质量指标,超过52%才有讨论价值,50%等同抛硬币。年化收益要结合最大回撤一起看,一个年化30%但最大回撤40%的策略,实盘根本扛不住。
6.2 一个快速验证技巧:滚动起点回测
def rolling_backtest(model, full_df, feature_cols, seq_len=20, start_date='2023-01-01', step=5): dates = full_df['date'].unique() # 假设full_df按日期排列 pred_list, true_list = [], [] # 从start_date开始,每隔step天发起一次预测 for i in range(seq_len, len(dates) - 1, step): current_date = dates[i] if current_date < start_date: continue history = full_df[full_df['date'] <= current_date].tail(seq_len) if len(history) < seq_len: continue pred = predict_tomorrow(model, history.head(1).copy(), None, None, feature_cols, seq_len) # 这里需要把实际次日涨跌幅取出来 next_day = full_df[full_df['date'] == dates[i + 1]] actual = next_day['close'].values[0] / history['close'].values[-1] - 1 pred_list.append(pred) true_list.append(actual) # 方向准确率 correct = sum(1 for p, t in zip(pred_list, true_list) if (p > 0) == (t > 0)) print(f"方向准确率: {correct / len(pred_list):.2%}") # 模拟策略收益:预测涨就持有,否则空仓 equity = 1.0 for p, t in zip(pred_list, true_list): if p > 0: equity *= (1 + t) print(f"策略净值: {equity:.4f}")滚动回测的关键是每次预测只使用截止当天的数据,和实盘完全一致。step=5的意思是一周只交易一次,降低交易频次等于降低手续费和噪声影响。如果模型在滚动回测中方向准确率能稳定在52%以上,策略净值能跑赢沪深300指数本身,这个模型才值得投入实盘验证。
这个方向我做了两年多,最深刻的教训是:LSTM预测的结果必须当一个弱信号去用,而不是当事实去赌。我见过太多人把回测曲线拉出来就兴奋,结果实盘三个月亏掉一年收益。数据清洗的耐心、特征构造的克制、验证方法的严谨,这三样比模型本身更决定项目成败。希望帮到你。
本文还有配套的精品资源,点击获取