
简介这份资源是Python基于遗传算法优化LSTM模型的股市预测完整项目适合需要完成毕业设计、期末大作业或课程设计的Python学习者也适合对量化交易与时间序列预测感兴趣的开发者。资源内含源代码、训练好的模型和配套数据集代码附有详细注释从LSTM模型搭建到遗传算法参数寻优均有清晰说明便于新手理解与二次开发。压缩包共89个文件以Python脚本24个py、编译缓存19个pyc、文本说明13个txt及Web前端资源html/css/js为主另含少量Excel数据文件、sqlite3数据库和项目配置文件整体大小仅8.88MB结构紧凑且易于部署。项目经过严格调试确保可运行界面简洁、操作流程完整已被导师认可为高分项目目前已有74人学习使用可直接作为毕设或课程设计的核心成果省去从零搭建的繁琐过程。1. 遗传算法优化LSTM的股市预测超参调不动换个思路搜同样是拿日线数据预测下一交易日收盘价有人跑通三层LSTM依然亏损有人用单层结构配合合适的超参数做出像样的回测曲线。真正拉开差距的往往不是网络层数而是timestep、学习率、隐藏层神经元这些不起眼的超参数。遗传算法优化LSTM的股市预测方案本质就是把调参从手工试错变成有方向的进化搜索用GA自动寻找LSTM的超参数组合再训练模型做价格预测。下面按这套方案的完整链路展开覆盖数据集准备、GA与LSTM结合的代码实现、常见翻车点与验证方法适合准备做金融时序实验、想摆脱手动试参的开发者和研究者。预测结果只作技术验证不构成投资建议。2. LSTM超参数为什么难调从选型逻辑到搜索策略的选择股价日线数据是小样本、强噪声、非平稳的时间序列LSTM能不能从里面学到可迁移的模式很大程度不是网络结构决定的而是超参数决定的。同一个模型结构timestep从5改到20验证集损失可能差一个数量级。先搞清楚哪些参数最敏感再谈用什么策略去搜。2.1 决定预测效果的五个核心超参数我一般把LSTM在日线数据上的超参数分成五类它们对训练行为和最终预测的影响各不相同。下面这张表是常用的起始取值范围具体项目里可以按数据量缩放。参数参考范围对模型的影响timestep5~30记忆窗口长度决定模型看多少天历史hidden116~128第一层隐藏神经元数决定记忆容量hidden20~64第二层神经元数0表示不堆层learning_rate1e-4~1e-2梯度步长直接影响收敛质量batch_size16~64梯度估计稳定性影响训练方差dropout0~0.4随机失活比例抑制过拟合timestep是最容易出问题的参数。股市日线数据的有效记忆窗口通常在5到30个交易日之间对应一周到一个多月的价格走势。设得太短模型只看得到最近几天的随机波动学不到趋势设得太长输入里混入大量与当前走势无关的旧信息梯度在时间维上反复传播容易消失。hidden1则直接决定LSTM的记忆容量日线数据本身信息量有限128个神经元通常已经足够再往上加只会让训练变慢并加重过拟合。learning_rate和batch_size这对参数要放在一起看。LSTM在长序列上训练时梯度范数波动很大学习率稍高就会震荡稍低又收敛太慢batch_size太小梯度噪声大太大则每个epoch迭代次数少模型没看够数据就进入下一轮。dropout是一个容易被忽略的变量股市数据的信噪比极低不加dropout的LSTM很容易把噪声当成规律记下来。2.2 搜索策略选型网格搜索、随机搜索与GA的边界很多人第一反应是网格搜索把每个参数设几个档位排列组合挨个跑。问题是组合数增长太快。假设只搜四个参数每个参数给三个档就有81个组合import itertools grid { timestep: [5, 10, 20], hidden: [32, 64, 128], lr: [1e-4, 1e-3, 1e-2], batch: [16, 32, 64], } combos list(itertools.product(*grid.values())) print(f组合数: {len(combos)}) # 输出: 组合数: 8181次训练看起来不多但注意每次LSTM训练都要跑几十个epoch单次耗时几十秒到几分钟。真实项目里再加大dropout、hidden2、激活函数这些维度组合数立刻突破几千。网格搜索的致命问题不是慢而是它完全不利用已经跑过的组合的结果第50个组合和第3个组合的信息是孤立的。随机搜索比网格搜索聪明一些它能在同样的预算下覆盖更广的参数空间但依然不利用历史评价结果。遗传算法补上的正是这一点每一代根据种群适应度决定下一轮搜索方向优胜劣汰把「跑过的好参数」保留下来继续进化。GA还有两个和LSTM天然匹配的特性一是它对目标函数是否可导没有要求LSTM训练本身不可导但评估结果可以直接当适应度二是它能同时处理连续参数学习率和离散参数timestep、batch_size不用像贝叶斯优化那样做复杂的核函数设计。2.3 GA-LSTM的完整工作流程谁负责搜索谁负责训练GA和LSTM的分工很清楚GA搜超参数LSTM做训练和预测两者通过「评估函数」连接。整个流程是这样的定义每个超参数的取值范围生成一个「基因」即一组完整的超参数组合。初始化一个种群比如12个个体每个个体是一组随机超参数。对每个个体构建LSTM用训练集训练用验证集算适应度。根据适应度排序保留最优的若干个精英个体。用锦标赛选择挑出父代做交叉和变异生成新一代种群。重复第3到第5步直到达到预设进化代数。取最后一代适应度最高的个体作为最终超参数在全量数据上重新训练并测试。这里有一个成本约束必须提前想清楚每评估一个个体就要完整训练一次LSTM种群12个个体、进化10代意味着最多要训练上百个LSTM。所以种群规模不建议超过20进化代数控制在10代左右优先保证每代个体的质量而不是堆数量。3. 数据集准备与特征构造复权、滚动窗口与时序切分模型输入的数据质量决定了GA能搜到什么水平的参数。股市预测实验里最常见的失败不是算法写错而是数据集里混入了未来信息或者切分方式让验证集失真。这一章讲清楚怎么把原始行情数据处理成LSTM的标准监督样本。3.1 行情数据读取与清洗复权处理别偷懒先说数据来源。实际项目里可以直接用开源的财经数据接口拉日线数据但为了读者方便复现我用本地CSV作为输入字段结构统一为date、open、high、low、close、volume。真实项目替换成接口拉取即可后续处理逻辑完全一致。import pandas as pd # 读取本地行情CSV接入数据接口时替换这个读取函数即可 df pd.read_csv(stock_daily.csv, parse_dates[date]) df df.sort_values(date).drop_duplicates(subsetdate) df df.dropna(subset[close]) # 只保留建模需要的列统一列名 df df[[date, open, high, low, close, volume]].reset_index(dropTrue) print(df.head()) print(df[date].min(), -, df[date].max(), 共, len(df), 条)这段代码做了三件基础但重要的事按时间排序、按日期去重、丢弃收盘价为空的行。排序和去重必须做因为很多数据接口在停牌日会重复或乱序返回。时间索引统一之后后面所有rolling窗口和shift操作才有意义。这里有一个不能省的处理是复权。如果直接用不复权的原始价格遇到除权除息日股价会凭空跳空LSTM会把这种人为的价格断裂当成真实的行情波动去学习预测结果自然失真。常见的做法是使用前复权数据以最新交易日为基准把历史价格按分红送股比例折算。数据接口一般提供复权因子参数拉数据时直接指定前复权即可。3.2 特征构造价格、成交量与技术指标的窗口化LSTM的输入是二维矩阵形状为样本数timestep特征数。所以不能直接拿单列收盘价喂进去而是要把每一行数据扩展成多个特征再用滑动窗口切成样本。我常用的特征组合是价格、成交量和技术指标三类混搭。# 技术指标特征 df[ma5] df[close].rolling(5).mean() df[ma10] df[close].rolling(10).mean() df[ret] df[close].pct_change() # 当日收益率 df[vol_ma5] df[volume].rolling(5).mean() # 成交量5日均值 df[rsi] compute_rsi(df[close], 14) # RSI指标自行实现 # 标签预测下一交易日收盘价 df[label] df[close].shift(-1) df df.dropna().reset_index(dropTrue) features [open, high, low, close, volume, ma5, ma10, ret, vol_ma5, rsi] label_idx features.index(close) if close in features else 0特征列的选择有一个铁律t时刻的样本里任何一列都不能包含t1及之后的信息。MA5、MA10、RSI这些指标都是用历史数据算出来的没问题。但有些技术指标在做「未来N日均线」时会把未来数据带进来这种特征必须排除。rolling窗口产生的NaN在dropna之后会吃掉最前面的若干行这是正常的因为那些位置没有足够的历史数据来计算指标。3.3 时序切分与归一化先切分再缩放是铁律数据切分必须按时间顺序进行绝对不能随机打乱。股市数据是强时序相关的随机打乱等于把未来样本混进训练集模型会在验证集上表现出虚高的精度。我习惯按7:1.5:1.5的比例切训练集、验证集和测试集测试集留到最后一段模拟「对未来一段未知行情做预测」。train_cut int(len(df) * 0.70) val_cut int(len(df) * 0.85) train_df df.iloc[:train_cut] val_df df.iloc[train_cut:val_cut] test_df df.iloc[val_cut:] from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_scaled scaler.fit_transform(train_df[features]) val_scaled scaler.transform(val_df[features]) test_scaled scaler.transform(test_df[features])注意fit_transform和transform的使用位置。MinMaxScaler只能对训练集调用fit_transform让统计量只来自训练集验证集和测试集一律transform。如果对全量数据先fit再切分验证集和测试集的最大值、最小值信息会混进缩放公式这是一种非常隐蔽的数据泄漏后面避坑章节还会展开讲。切分完成后再用滑动窗口把连续序列切成LSTM的监督样本def make_windows(data, label_idx, timestep): X, y [], [] for i in range(len(data) - timestep): X.append(data[i:i timestep]) y.append(data[i timestep, label_idx]) return np.array(X), np.array(y) X_train, y_train make_windows(train_scaled, label_idx, timestep10) X_val, y_val make_windows(val_scaled, label_idx, timestep10) X_test, y_test make_windows(test_scaled, label_idx, timestep10)窗口生成必须在切分之后做不能在切分之前对整个数据集生成窗口再切否则相邻样本会跨阶段重叠训练集和验证集共享大量时间片段验证指标失真。这是整个数据准备环节最容易被忽略的坑。4. GA-LSTM的Python实现基因编码、进化循环与最小可跑示例这一章给出可以直接改的代码骨架。GA只负责搜索超参数LSTM负责具体训练两者通过适应度函数对接。代码按四个模块拆开讲基因编码、适应度计算、进化算子、主循环。4.1 基因编码与种群初始化混合参数空间的映射方案基因编码就是把一组超参数表示成一个对象。Python里最自然的表示是dict键是参数名值是这个个体的基因取值。遗传算法处理连续和离散混合的参数空间关键是给每个参数选择合适的采样方式。import random GENE_RANGE { timestep: (5, 30), # 整数记忆窗口 hidden1: (16, 128), # 整数第一层神经元数 hidden2: (0, 64), # 整数0表示不用第二层 lr: (1e-4, 1e-2), # 连续学习率 batch_size: (16, 64), # 整数batch大小 dropout: (0.0, 0.4), # 连续dropout比例 } def init_individual(): return { timestep: random.randint(5, 30), hidden1: random.randint(16, 128), hidden2: random.randint(0, 64), lr: 10 ** random.uniform(-4, -2), # 对数均匀采样 batch_size: random.choice([16, 32, 64]), dropout: random.uniform(0.0, 0.4), } # 生成初始种群 population [init_individual() for _ in range(12)]学习率的采样方式值得单独说明。学习率在1e-4到1e-2之间如果用均匀采样绝大多数随机数会落在数量级较大的区域导致小学习率几乎采不到。我习惯用对数均匀采样先在对数空间均匀取数再取10的幂这样1e-4、1e-3、1e-2三个数量级被采到的概率相同。batch_size不要用randint连续采样直接从[16, 32, 64]里选一个更符合实际训练习惯。4.2 适应度函数MSE与方向准确率的加权组合适应度是整个GA的指挥棒。如果只用均方误差MSE模型会倾向于拟合价格水平预测曲线贴着真实值走但涨跌方向可能完全不对。股市预测里真正影响决策的是方向所以我把适应度定义为验证集MSE减去方向准确率的加权值数值越小代表个体越优。from sklearn.metrics import mean_squared_error import numpy as np def direction_accuracy(y_true, y_pred): # 比较相邻两日预测的涨跌方向是否与真实一致 diff_true np.diff(y_true.flatten()) diff_pred np.diff(y_pred.flatten()) direction_true np.sign(diff_true) direction_pred np.sign(diff_pred) # 真实涨跌幅为0的情况按预测正确处理避免分母失真 correct (direction_true direction_pred) | (direction_true 0) return np.mean(correct) def evaluate_individual(ind, X_train, y_train, X_val, y_val, epochs30): model build_lstm(ind) # 按基因构建LSTM模型函数见4.4 history model.fit( X_train, y_train, validation_data(X_val, y_val), epochsepochs, batch_sizeind[batch_size], verbose0, callbacks[EarlyStopping(patience5, restore_best_weightsTrue)], ) y_pred model.predict(X_val, verbose0) mse mean_squared_error(y_val, y_pred) acc direction_accuracy(y_val, y_pred) return mse - 0.3 * acc # 加权组合数值越小越好MSE和方向准确率的量纲不同直接相加其实不严谨这个写法只是为了先跑通流程。习惯上我会再把两者分别做归一化后加权或者在MSE相近的个体里用方向准确率做二次筛选。真实项目里建议以MSE为主排序方向准确率作为淘汰条件比如方向准确率低于52%的个体直接淘汰这样更稳。4.3 选择、交叉、变异与精英保留的实现进化算子我习惯用三个固定套路锦标赛选择、单点交叉、均匀变异再加精英保留。这三个算子实现简单参数语义清楚是GA-LSTM场景下最不容易出问题的组合。def tournament_select(population, k3): # 随机抽k个个体返回适应度最好的 candidates random.sample(population, k) return min(candidates, keylambda ind: ind[fitness]) def crossover(p1, p2): # 单点交叉随机选一个基因位交换之后的所有基因 keys list(GENE_RANGE.keys()) point random.randint(1, len(keys) - 1) child1 {k: p1[k] for k in keys[:point]} | {k: p2[k] for k in keys[point:]} child2 {k: p2[k] for k in keys[:point]} | {k: p1[k] for k in keys[point:]} return child1, child2 def mutate(ind, rate0.15): # 每个基因以rate概率重新随机初始化 for key in GENE_RANGE: if random.random() rate: ind[key] init_individual()[key] return ind def retain_elites(population, num2): # 按适应度排序返回最优的前num个个体 return sorted(population, keylambda ind: ind[fitness])[:num]锦标赛选择的k3是比较常用的力度k越大选择压力越大种群会更快收敛但也更容易早熟。交叉用单点交叉对6个基因位的编码来说足够。变异率0.15起步是比较稳的太高会退化成随机搜索太低则容易进化崩溃后面避坑章节细说。4.4 主循环种群进化与LSTM训练如何串起来下面这个主循环可以把前面所有模块串起来。为了控制篇幅build_lstm函数直接用Keras的Sequential接口实现按照基因里的超参数构建模型骨架。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm(ind): model Sequential() model.add(LSTM(ind[hidden1], return_sequencesind[hidden2] 0, input_shape(ind[timestep], X_train.shape[2]))) if ind[hidden2] 0: model.add(LSTM(ind[hidden2], return_sequencesFalse)) model.add(Dropout(ind[dropout])) model.add(Dense(1)) model.compile(optimizeradam, lossmse, lrind[lr]) return model POP_SIZE 12 GENERATIONS 10 ELITE_NUM 2 population [] for i in range(POP_SIZE): ind init_individual() ind[fitness] evaluate_individual(ind, X_train, y_train, X_val, y_val) population.append(ind) for gen in range(GENERATIONS): elites retain_elites(population, ELITE_NUM) next_pop [dict(e) for e in elites] while len(next_pop) POP_SIZE: p1 tournament_select(population) p2 tournament_select(population) c1, c2 crossover(p1, p2) next_pop.append(mutate(c1)) if len(next_pop) POP_SIZE: next_pop.append(mutate(c2)) for ind in next_pop[ELITE_NUM:]: ind[fitness] evaluate_individual(ind, X_train, y_train, X_val, y_val) population next_pop best population[0] print(f第{gen1}代 best fitness{best[fitness]:.4f} flr{best[lr]:.2e} hidden{best[hidden1]} timestep{best[timestep]})每代进化要做的工作量大约等于POP_SIZE - ELITE_NUM次完整的LSTM训练12个个体的种群每代约10次训练10代就是100次。如果单次训练要跑40个epoch、耗时1分钟整个GA流程就是100分钟量级。第一次跑通时建议把epochs降到10、GENERATIONS降到3确认流程没跑错再放大。还要注意evaluate_individual里每一次都是新建模型、重新初始化权重GA搜的是超参数而不是权重这点不要搞混。5. 股市预测模型避坑手册数据泄漏、进化崩溃与过拟合伪装GA-LSTM这套流程的坑主要集中在数据切分和进化设置两个方向。以下五条都是真实会发生的问题按「现象、原因、解决」梳理每条都值得在执行前先对照检查。5.1 数据侧三个坑归一化泄漏、标签错位、样本重叠坑一归一化泄漏。现象是验证集loss低得离谱但测试集回测曲线整体偏移严重。原因是有人在切分之前对全量数据做了fit_transformMinMaxScaler的min和max把未来信息带进了训练阶段的缩放公式。解决方法是严格按3.3节的代码只对训练集fit验证集和测试集只transform。更隐蔽的变体是用全量数据的均值和标准差做Z-Score标准化同样属于泄漏必须在切分之后分别计算。# 错误示范先全局fit再切分验证集信息泄漏进训练集 scaler MinMaxScaler() scaled_all scaler.fit_transform(df[features]) train_scaled scaled_all[:train_cut] # 正确做法只fit训练集 scaler MinMaxScaler() train_scaled scaler.fit_transform(train_df[features]) val_scaled scaler.transform(val_df[features]) test_scaled scaler.transform(test_df[features])坑二标签错位。现象是训练时loss收敛得很好但预测结果总是比真实行情慢一天看起来像「用今天的信号预测了今天的价格」。原因是构造标签时shift方向用的不对或者refresh了数据后没有重新生成标签。预测下一交易日收盘价标签必须是close.shift(-1)也就是t1时刻的真实收盘价如果写成close本身模型学的就是「用今天预测今天」的自回归复制。坑三样本重叠。现象是验证集上的预测曲线贴得异常平滑几乎完美拟合但换到一段全新的行情上预测精度断崖式下跌。原因是滑动窗口切样本时相邻窗口重叠了绝大部分时间步训练集和验证集在时间上高度重合验证指标虚高。解决方法是切分时在训练集和验证集之间留出若干天的gap或者用不重叠的验证块。更彻底的办法是第6章的walk-forward滚动验证。5.2 进化与训练侧两个坑进化崩溃与过拟合伪装坑四进化崩溃。现象是种群适应度从第4代开始完全不再变化搜出来的timestep永远等于初始随机值GA形同虚设。原因是变异率太低加上精英保留过多最优个体在几代之内复制充满整个种群多样性归零交叉算子无新基因可用。解决办法是三个方向同时调变异率从0.15提到0.25精英数量从2降到1每代随机注入1到2个全新随机个体作为「移民」。还有一个容易被忽略的细节LSTM每次训练权重初始化是随机的同一个体两次评估的适应度会有抖动如果不对每个个体固定随机种子选择压力会被评估噪声干扰建议在build_lstm时设置tf.random.set_seed和np.random.seed。坑五过拟合伪装。现象是训练集loss一路降到很低验证集loss也不错但方向准确率始终在52%左右徘徊回调测就亏钱。原因是LSTM容量过大、技术指标特征过多、dropout太小模型把训练集的噪声当成规律记住了。解决方法是压缩搜索空间hidden1上限从128收到64hidden2直接去掉dropout下限从0改为0.1特征列删掉冗余的MA指标只保留价格和成交量。同时把适应度函数改成我们前面说的「MSE排序 方向准确率过滤」方向准确率低于阈值的个体直接淘汰避免GA朝着纯拟合的方向进化。# 方向准确率阈值过滤在evaluate_individual末尾追加 if acc 0.52: return 1e9 # 直接给一个非常大的惩罚值让该个体失去竞争力6. 从实验到滚动验证walk-forward、集成预测与可视化GA搜出的最优超参数不能直接宣布胜利还需要用接近真实交易的方式做验证。这一章的三个技巧是我认为这套方案从「能跑」到「可信」最关键的收尾动作。6.1 walk-forward用「只用过去」的方式做滚动预测常规的切分验证是静态的训练集、验证集、测试集各占一段模型只预测测试集这一段。实盘里模型要不断地用最新数据重新训练、预测下一天两种场景差异很大。walk-forward的做法是从某个时间点开始每次只用该点之前的数据训练预测之后若干天然后把时间点向前推进重复这个过程。这是防止数据泄漏最有效的手段。def walk_forward(df_scaled, timestep, horizon5): preds, truths [], [] for i in range(timestep horizon, len(df_scaled)): train_data df_scaled[:i] test_data df_scaled[i - timestep:i] model build_lstm(best_individual) model.fit(train_data, ..., epochs20, verbose0) pred model.predict(test_data.reshape(1, timestep, -1), verbose0) preds.append(pred[0, 0]) truths.append(df_scaled[i, label_idx]) return preds, truths这段代码刻意写得简洁重点是「每次重新训练」这个动作。它能暴露那些只在固定测试集上看起来完美的参数如果某个超参数组合在walk-forward里表现不稳定说明它过拟合了历史段落而不是学到了可迁移的规律。计算成本高是正常的我一般只对GA最后选出的前3个个体做walk-forward而不是对每个个体都跑。6.2 用GA种群做集成前五名个体取中位数GA运行结束时种群最后一代的前几名个体适应度相近但基因不同代表不同的搜索方向。与其只挑第一名不如把前五名都拿出来做集成预测。LSTM权重初始化随机单个模型的预测有方差取中位数比对极端预测更稳健。from statistics import median # top5是最后一代适应度最好的5个个体 ensemble_preds [] for ind in top5: model build_lstm(ind) model.fit(X_train_all, y_train_all, epochs30, verbose0) pred model.predict(X_test, verbose0) ensemble_preds.append(pred.flatten()) final_pred [median(values) for values in zip(*ensemble_preds)] direction_acc direction_accuracy(y_test, np.array(final_pred).reshape(-1, 1)) print(集成方向准确率:, direction_acc)这个技巧几乎没有额外成本GA反正已经把这些个体训练过了只需要重新用更多数据训练一次就能用于集成。GA种群的多样性在这里变成了一种白捡的稳定性。6.3 可视化验证把预测曲线平移一天看是否滞后最后做一次可视化检查。把真实收盘价和预测收盘价画在同一张图里然后刻意把预测曲线整体右移一个交易日观察两边的贴合程度。如果右移后依然贴合得很好说明模型实际上在预测「昨天的值」只是把输入里的收盘价延迟复制了一遍这是LSTM做金融预测最常见的失败形态。真正的预测应该是曲线整体右移后出现明显错位才是「用过去推未来」的正常表现。import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.plot(test_dates, y_test, label真实收盘价, linewidth1) plt.plot(test_dates, final_pred, label预测收盘价, linewidth1, alpha0.8) plt.plot(test_dates, np.roll(final_pred, 1), label预测值右移一天, linewidth1, linestyle--) plt.title(f方向准确率: {direction_acc:.2%}) plt.legend() plt.show()有次我在某只个股上把方向准确率调到了58%以为终于找到了稳健的参数组合结果walk-forward一跑立刻打回原形。原因是静态验证集和训练集的时间窗口重叠适应度评估被污染了。从那以后我把滚动验证当成模型上线前的最后一关GA搜出的参数再漂亮过不了这一关我也不会用它。这个方向最值得做的不是追求预测精度本身而是把一套带约束的搜索和验证流程跑通形成不用肉眼调参的实验体系以后换数据集、换标的都能复用。希望帮到你。本文还有配套的精品资源点击获取