HMM-LSTM股票市场趋势分析:四种模型动态权重组合实战

发布时间:2026/10/11 0:20:34
HMM-LSTM股票市场趋势分析:四种模型动态权重组合实战
简介面向股票量化分析与时序建模学习者这套Python源码包聚焦HMM与LSTM融合的股价趋势预测提供四种模型的完整实现覆盖数据清洗、特征构造、模型训练、概率预测与效果评估等环节。压缩包共61个文件以.py源码为主28个另有编译生成的.pyc文件、运行结果图.jpg/.png、项目说明.md及配套论文.pdf整体仅2.54MB轻量且结构清晰。源文件按功能分为数据处理、模型构建、训练预测和结果可视化等模块便于按需查阅和二次开发。目前已吸引327人学习下载适合具备Python及机器学习基础、希望研究隐马尔可夫与长短期记忆网络组合建模的读者。资源内不仅包含GMM-HMM、XGB-HMM、LSTM等脚本还提供异常值处理、样本均衡、多因子合成、评估绘图等工具函数结合项目说明和参考论文可帮助理解股票趋势分析中的模型对比、参数调整与结果验证思路快速搭建自己的实验流程。1. HMM-LSTM 股票市场趋势分析四种模型为什么比单一模型强做股票趋势分析最反直觉的一点是单一模型越“成熟”越容易在实盘里被反复打脸。HMM 能把市场切成上涨、震荡、下跌几种隐藏状态但对中长期的趋势延续记忆很弱LSTM 对时序记忆很强却对市场风格的突然切换视而不见。这个资源包的做法是把 GMM-HMM、多因子 HMM、XGB-HMM 和 LSTM 四种模型同时跑起来再用动态权重 Bagging 合成一个组合预测器配套完整的清洗、切分、训练、评估流程全部以 Python 源码和项目说明的形式打包。它适合想完整跑一遍 HMM-LSTM 管线的量化从业者、需要复现论文实验的研究生以及想对比四种模型方向预测能力的人。它不会让你明天就赚钱但能让你把一个研究框架从原始行情一路推进到概率输出和评估图。2. 数据管线先把清洗、切分、因子拼接走通再讨论四种模型2.1 为什么数据工程必须先于模型这个包里的模型都对数据质量极其敏感。GMM-HMM 的发射概率要用几组高斯分布去拟合收益率分布行情里的异常值、涨跌停板、停牌空档会被 GMM 当成真实的尖峰去做拟合LSTM 则对特征的数值尺度非常敏感如果直接用收盘价训练注意力会被价格水平而不是价格变化带偏。所以整个代码包的第一步不是训练而是把原始行情处理成模型能吃的形态。我拿到的 code 目录里数据处理链是固定的process_on_raw_data.py 先做清洗form_df_all.py 把多段因子拼成总表random_cut.py 负责切分样本solve_on_outlier.py 处理异常值最后 form_model_dataset.py 把 DataFrame 转成 LSTM 和 HMM 各自需要的输入格式。理顺这条链路之后后面训练任何模型都是在同一份数据上做对比模型之间的差异才能真实反映到结果上。2.2 process_on_raw_data.py行情清洗与基础因子构造这个脚本的名字很直白它就是整个流程的入口。我在还原它的时候核心逻辑大致是读取多段行情 CSV按交易日期排序去重然后构造日频因子。这里要注意一个细节HMM 的观测序列必须是平稳的所以不能直接拿收盘价喂给模型而是要转成收益率、量比这类相对量。常见做法是先算日收益率再算 5 日量比和振幅最后把停牌造成的空档用前值填充或直接剔除。# process_on_raw_data.py 核心逻辑示意 import pandas as pd import numpy as np def load_raw_data(csv_paths): # 把多段行情 csv 读进来统一日期格式按日期排序 df pd.concat([pd.read_csv(p) for p in csv_paths], ignore_indexTrue) df[trade_date] pd.to_datetime(df[trade_date]) df df.drop_duplicates(subset[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) return df def build_factor_cols(df): # 收益率用 pct_change量比用 5 日成交额均值做分母 df[ret] df[close].pct_change() df[amount_ratio] df[amount] / df[amount].rolling(5).mean() df[amplitude] (df[high] - df[low]) / df[close].shift(1) df[close_ma5] df[close] / df[close].rolling(5).mean() - 1 return df.dropna().reset_index(dropTrue)这里的关键参数是 rolling(5)它决定量比和均线偏离的周期。5 日代表一周的短期节奏如果你面对的是更强趋势的市场可以放宽到 10 日。pct_change()会把第一行变成 NaN所以最后必须 dropna否则后面喂给 hmmlearn 时会直接报错。实际包里的 process_on_raw_data.py 还会处理多支股票拼在一起的情况dataset_code 目录下的原始数据就是这样组织起来的。2.3 random_cut.py 与 solve_on_outlier.py切分策略和异常值处理时序模型的训练集切分是个玄学问题。固定按年份切分很容易让模型只在某一段波动率区间里生效换一段行情就失效。random_cut.py 的做法是随机切出多个等长的连续子序列一部分做训练、一部分做验证这样模型见过不同波动环境泛化能力会更接近真实使用场景。# random_cut.py 的随机切分逻辑 def random_cut(df, seq_len600, n_seg8, seed42): rng np.random.default_rng(seed) usable len(df) - seq_len starts rng.choice(usable, sizen_seg, replaceFalse) segs [df.iloc[s: s seq_len].copy() for s in sorted(starts)] # 常见做法前 80% 段做训练后 20% 段做验证避免段间重叠 return segs切完之后,异常值处理紧接着进行。solve_on_outlier.py 我一般会这么理解它对收益率和量比做分位数截断把超过 99.5% 分位的极端值压回到边界同时对由于涨跌停导致的重复观测值加一个非常小的随机扰动避免 GMM 拟合出零方差分量。这一步看似不起眼实际能解决后面训练 HMM 时一大半的报错。2.4 form_df_all.py 与 form_model_dataset.py把 DataFrame 变成模型输入form_df_all.py 的作用是把前面生成的多段子序列和因子列拼成一张完整的总表然后合并到全量数据集。真正决定模型能不能跑通的是 form_model_dataset.py它负责把二维 DataFrame 转换成 LSTM 需要的那种三维数组也就是样本数, 时间步, 特征数。HMM 的输入相对简单直接取观测矩阵T, D就可以。# form_model_dataset.py 中构建 LSTM 数据集的思路 def make_lstm_dataset(df, feature_cols, n_steps20): X, y [], [] for i in range(len(df) - n_steps): X.append(df[feature_cols].iloc[i: i n_steps].values) # 二分类下一根收益率大于 0 记为 1否则为 0 y.append(1 if df[ret].iloc[i n_steps] 0 else 0) X np.array(X) # 形状 (样本数, n_steps, n_features) y np.array(y) return X, y这里的 n_steps20 是最容易调也最容易翻车的参数。窗口太短模型看不到趋势窗口太长训练样本显著减少而且特征会包含太多旧信息。包里的 FIGURE 目录下有 best_iter.png那张图实际上就是训练 LSTM 时记录下来的迭代曲线后面我会专门讲怎么看这张图决定要不要调窗口。到这一步四种模型面对的数据格式就统一了接下来的训练才有对比价值。3. 单模型基线GMM-HMM 的状态切分与 LSTM 的窗口记忆3.1 HMM 的隐藏状态到底在切什么HMM 的隐藏状态是这个包最核心的概念。它假设市场的涨跌不是一个随机过程而是由几个不可直接观测的状态驱动比如上涨、震荡、下跌。模型要学的是三组东西初始状态概率 pi状态转移矩阵 A以及观测概率 B。训练好之后模型可以对每个时间点做解码告诉你当前最可能处于哪个状态再根据状态的历史表现推测下一时刻的收益方向。这个包里的 GMM_HMM.py 用的是 GMM 做观测概率也就是把每个隐藏状态下的收益率分布用多个高斯分布去拟合。为什么要多个而不一个因为实际收益率分布是尖峰厚尾的单个高斯会严重低估极端行情的概率。n_mix 就是控制每个状态里高斯分量数量的参数我一般会从 3 到 5 之间调。# GMM_HMM.py 对应的训练方式 from hmmlearn.hmm import GMMHMM model GMMHMM(n_components3, n_mix4, covariance_typefull, n_iter50, random_state42) model.fit(X_train) # X_train 形状为 (T, D)观测为收益率、量比等连续特征 print(转移矩阵形状:, model.transmat_.shape)n_components3 对应三个隐藏状态n_mix4 表示每个状态用 4 个高斯分量。covariance_typefull 允许特征之间有相关性这个对多因子 HMM 很重要因为收益率和量比本来就不是独立的。train_HMM_model.py 的职责就是循环不同的因子组合和状态数把验证集效果最好的那一组参数保存下来跑一次可能要好几分钟但这是值得的。3.2 多因子 HMM 与行情 HMM 的定位差异除了 GMM-HMM包里有 HMM_duoyinzi.py 和 HMM_hangqing.py 两个文件。前者用多因子作为观测序列所以我把它理解为多因子 HMM后者只使用行情数据本身也就是纯量价 HMM。这两个模型在四种模型里占据两个名额但它们不是竞争关系而是消融对比的关系。多因子 HMM 的观测矩阵每列是一个因子比如收益率、量比、振幅、均线偏离它们共同决定隐藏状态的转移。纯行情 HMM 只靠收益率看的是仅凭价格能不能切出有效状态。实际跑下来纯行情 HMM 在震荡市里会频繁切换状态多因子版本更稳定一些。train_HMM_model.py 里会分别对这两个模型做参数搜索最后把结果交给主训练入口。3.3 LSTM.py网络结构为什么是两层 LSTM 加 DropoutLSTM 在这个包里承担的是记忆功能它与 HMM 的互补点在于HMM 状态切换是马尔可夫的只依赖当前时刻但 LSTM 能把过去 20 个交易日的特征综合起来捕捉趋势的加速度和延续性。LSTM.py 里定义的网络是典型的分类结构输入是n_steps, n_features输出是涨跌两个类别的概率。# LSTM.py 中的网络定义 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, LSTM def build_lstm(n_steps, n_features): model Sequential([ LSTM(64, return_sequencesTrue, input_shape(n_steps, n_features)), Dropout(0.3), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(2, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model第一层 LSTM 返回序列让第二层 LSTM 能继续看到每个时间步的隐藏状态如果第一层直接 return_sequencesFalse信息会压缩得过早第二层基本学不到东西。Dropout 放在两层之间是为了抑制时序过拟合股票数据本身就带很强的噪声不加 Dropout 的话训练集准确率能上 0.9测试集直接掉到 0.5。最后用 softmax 而不是 sigmoid是因为同时输出涨和跌的概率能直接和 HMM 的输出对齐方便后面做 Bagging 组合。3.4 train_LSTM_model.py训练主流程与 early stoppingLSTM 的训练并不需要真的跑满 100 轮包里的 best_iter.png 就是训练过程曲线从它能明显看到验证损失在某个 epoch 之后开始掉头向上那就是过拟合的起点。train_LSTM_model.py 里设置了早停机制patience 取 10也就是验证损失连续 10 轮没有改善就停住并且回滚到验证损失最小的权重。# train_LSTM_model.py 的训练与早停设置 from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint checkpoint ModelCheckpoint(best_lstm.h5, monitorval_loss, save_best_onlyTrue, modemin) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) model.fit(X_train, y_train, batch_size64, epochs100, validation_data(X_val, y_val), callbacks[checkpoint, early_stop], verbose1)batch_size64 是兼顾效率和稳定性的选择太小会让梯度更新过于频繁损失曲线像心电图一样跳动太大则每个 epoch 太慢而且容易陷在局部最优点。epochs 上限设 100配合早停实际通常 30 到 50 轮就会触发。训练结束后best_lstm.h5 会作为 LSTM 模型的最终权重后面的 main_train_model.py 会把这个权重载入并生成预测概率。4. XGB-HMM 与 Bagging 动态权重这个源码包最有价值的部分4.1 XGB-HMM用 XGBoost 重估 HMM 的 B 矩阵这是整个包里含金量最高的设计。传统 HMM 的 B 矩阵是用 GMM 拟合观测概率但 GMM 只能拟合分布形态没法利用多因子的非线性关系。XGB_HMM 的思路是先用 XGBoost 这种树模型对行情特征做监督学习预测上涨概率然后把预测结果转成 B 矩阵的初始值再交给 HMM 的迭代重估去修正。这样一来B 矩阵不再受限于高斯分布假设而是融入了树模型对特征交互的捕捉能力。# form_B_matrix_by_XGB.py 的核心思路 import xgboost as xgb # 先用滚动特征训练 XGB 分类器 clf xgb.XGBClassifier( n_estimators300, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42 ) clf.fit(X_feat_train, y_train) # 预测每个时间点的上涨概率 prob_up clf.predict_proba(X_feat_test)[:, 1]这个设计的好处是显而易见的。GMM 对每个状态拟合的是收益率分布而 XGBoost 直接对“下一根是否上涨”做有监督学习相当于在生成模型中强行注入了判别模型的能力。XGB_HMM 子目录里的 xgb.py 完成训练form_B_matrix_by_XGB.py 把概率映射成 B 矩阵re_estimate.py 再做一轮 Baum-Welch 重估。这三个文件串起来就是完整的 XGB-HMM 混合模型。4.2 XGB_HMM.py 与 predict.py混合模型如何产出预测概率XGB-HMM 训练完成之后predict.py 会做标准的前向算法计算每个时间点处于不同隐藏状态的概率再结合 B 矩阵推导出下一个观测的上涨概率。这里的输出不是简单的二分类 0 或 1而是一个 0 到 1 之间的连续概率值。这个概率值非常重要因为后面 Bagging 组合需要所有模型的输出处于同一个量纲。pred_proba_GMM.py 和 pred_proba_XGB.py 分别负责输出 GMM-HMM 和 XGB-HMM 的逐点概率。你会发现这两个文件名字高度对称它们本质上做的是同一件事把测试集上的每个时间点都过一遍模型得到方向概率序列。我一般会把这个概率序列存成 numpy 数组或者 csv方便后面组合时直接读取。4.3 Bagging 动态权重四个模型的组合策略四种模型的原始输出都拿到之后直接平均是一种选择但这个包用的是 bagging_balance_weight.py —— 按每个模型最近一段时间的预测命中率动态调整权重。近端表现好的模型权重高表现差的权重自动降低。这个机制比固定权重实用得多因为市场风格会轮动HMM 可能在震荡市更准LSTM 可能在单边趋势里更强动态权重能自动适应这种变化。# bagging_balance_weight.py 动态权重计算 def rolling_acc(proba, y_true, window120): pred (proba 0.5).astype(int) acc np.full(len(proba), np.nan) for i in range(window, len(proba)): acc[i] (pred[i - window:i] y_true[i - window:i]).mean() return acc # 四个模型的概率序列 probs np.stack([p_gmm, p_xgb, p_hmm_mf, p_lstm], axis1) accs np.stack([rolling_acc(p, y_true, 120) for p in [p_gmm, p_xgb, p_hmm_mf, p_lstm]], axis1) accs np.nan_to_num(accs, nan0.5) # 归一化得到权重再加一个极小值防止除零 weights accs / (accs.sum(axis1, keepdimsTrue) 1e-8) final_prob (weights * probs).sum(axis1)window120 表示用过去 120 个交易日的命中率来评估模型近期状态。窗口太短权重会频繁抖动太长模型风格切换的响应又太慢我尝试过 60 和 250120 是一个比较稳的折中。combine_allow_flag.py 在 final_prob 的基础上加了交易开关当组合概率落在 0.45 到 0.55 这个模糊区间时标记为“不交易”这能有效过滤那些模型分歧很大的时间点。4.4 evaluate_plot.py 与 form_accuracy.py如何衡量组合模型的效果组合结果不能只看 准确率 一个数evaluate_plot.py 会把 final_prob、真实收益、买卖信号画在同一张图上输出到 FIGURE 目录下的 test1.jpg 和 test2.jpg。form_accuracy.py 则输出更细的指标整体方向命中率、上涨段命中率、下跌段命中率。这两个脚本搭配起来才能判断组合模型到底是在捕捉趋势还是仅仅拟合了噪声。用我自己的经验来说单看整体命中率很容易被多头市场骗。如果 70% 的样本都是上涨模型天天猜涨也能拿到 0.7 的准确率。所以要重点看下跌段的命中率那才是模型真正有没有区分度的地方。这个包里的 main_single_score.py 可以逐个模型单独打分main_train_model.py 则是统一跑完整流程。先用前者做基线对比再跑后者出组合结果这个顺序能帮你快速定位某个模型是不是拖后腿了。5. 避坑与排查复现 HMM-LSTM 时最常翻车的六个问题5.1 未来函数测试集准确率虚高到 0.9现象测试集方向命中率异常高高到明显不合理。 原因random_cut 切分后特征构造发生在切分之前rolling 均值里混入了后面一段的数据。比如第 400 个样本的 5 日均线用的可能是第 401 到 405 天的数据这等于模型偷看了未来。 解决把特征构造放到切分之后每个 segment 单独算 rolling跨 segment 的数据一律断开。我每次拿到新数据源都会强制检查一遍特征计算的起始位置这个坑走过一次就记住了。5.2 B 矩阵维度对不上现象训练 HMM 时直接报 ValueError提示维度不匹配。 原因form_B_matrix_by_XGB.py 输出的 B 矩阵是 XGB 的分类概率列数由类别数决定而 HMM 的发射矩阵行数必须等于隐藏状态数。XGB 输出的是二分类两列如果直接把它当成 B 矩阵塞进去维度自然对不上。 解决在 re_estimate.py 里显式检查 B.shape 与 model.transmat_.shape 是否一致不一致就先做一次状态归并。调试阶段我习惯在训练前打印这两个矩阵的形状一眼就能看出问题。5.3 LSTM 验证损失完全不下降现象训练集 loss 一路降到 0.1val_loss 却稳稳停在 0.69 附近基本等于随机猜。 原因特征没有归一化或者归一化时把整个数据集放在一起 fitscaler 看到了验证集的信息。后者会掩盖真实分布差异导致验证损失表现奇怪。 解决在 form_model_dataset.py 之前用 StandardScaler 只在训练集上 fit_transform测试集上只做 transform。这个顺序写错一行就是两种结果代码里我会固定写成scaler.fit(X_train_raw)再X_val_raw scaler.transform(X_val_raw)永远不会写反。5.4 涨跌停日让 GMM 拟合出零方差分量现象hmmlearn 报错说协方差矩阵非正定或者某个高斯分量方差趋近于 0。 原因一字板或者停牌日的收益率是 0.0大量重复的 0 值让 GMM 的某个分量试图用零方差去拟合这个尖峰。 解决在 solve_on_outlier.py 里对重复观测值加一个微小噪声比如np.random.normal(0, 1e-6)。这只是让数值计算稳定不影响分布形状但能救回训练过程。5.5 随机种子不固定两次运行结果差 5% 以上现象同一个包同一条命令跑两次准确率差一截。 原因random_cut 的切分、LSTM 的初始化、XGB 的列采样全部带随机性任何一个没固定种子都会让结果不可复现。 解决在 random_cut、LSTM、XGB 三处统一固定 random_state42并且把种子作为参数暴露出来写进日志。我习惯把种子写进文件名比如lstm_42.h5这样后续调参时不会拿错模型。5.6 涨跌样本不均衡模型退化成“永远猜涨”现象预测结果几乎全是 1但整体准确率还有 0.6。 原因样本里上涨天数占大头模型学到的最优策略就是猜上涨损失函数并没有惩罚这种偏科。 解决给损失函数加 class_weight把下跌类的权重调高到 1.5 到 2 之间。具体值看下跌样本占比占比越低权重越高。Bagging 组合之后还要在 combine_allow_flag.py 里检查最终概率分布如果概率中位数长期大于 0.6就说明某个模型又偏科了。6. 验证与进阶让 best_iter.png 和 test1.jpg 替你说话拿到这个包之后第一步不是直接改参数而是先把 FIGURE 目录下的四张图全部看一遍。train1.jpg 和 train2.jpg 是训练集上的预测概率叠加真实走势test1.jpg 和 test2.jpg 是测试集上的结果。判断方法很简单如果训练集拟合得漂亮测试集却明显稀疏且频繁错过拐点那就是过拟合要回去把 LSTM 的 patience 从 10 调到 6或者把 Dropout 从 0.3 提到 0.4。如果训练和测试的表现接近说明这个组合在样本外是成立的。best_iter.png 是 LSTM 训练过程中记录下来的损失曲线。这张图的价值在于它告诉你早停具体停在了第几个 epoch。我发现大多数情况下最佳 epoch 落在 20 到 40 之间很少需要跑满 100。如果 best_iter.png 里的验证损失曲线是一条几乎没有起伏的直线先别怀疑模型结构回到第 2 章确认一下特征是否归一化这个顺序能省下大量排查时间。进阶用法方面第四个模型也可以用指数衰减权重替代滚动窗口。滚动窗口的问题是过去 120 天内的每一天权重相同但实际上 3 个月前的命中率对明天的参考价值远不如最近 5 天。用指数衰减的方式那效果通常会更跟手# 用指数衰减替代滚动窗口近端误差权重更大 def exp_decay_weight(proba, y_true, alpha0.97): err ((proba 0.5).astype(int) ! y_true).astype(float) smoothed np.zeros_like(err) smoothed[0] err[0] for t in range(1, len(err)): smoothed[t] alpha * smoothed[t - 1] (1 - alpha) * err[t] # 平滑后的误差越小权重越高 return 1 - smoothedalpha0.97 意味着大约 33 个交易日之前的误差权重衰减到一半比固定窗口更贴近市场风格切换的节奏。这套改动只需要替换 bagging_balance_weight.py 里的权重计算函数其他部分完全不用动。从那以后我每次拿到新的行情数据源都会强制自己先把随机种子固定、切分再想特征、单模型跑完再碰 Bagging这个顺序救过我太多次。如果你也想用这份源码包复现那篇 arXiv 2104.09700 论文的实验建议从 dataset_code 里的原始数据开始自己动手跑一遍四种模型和动态权重组合希望帮到你。本文还有配套的精品资源点击获取