
简介一份面向股票量化分析学习者与金融科技开发者的模型实现资源基于HMM-LSTM组合对股票市场趋势进行建模与预测并对比四种典型模型如隐马尔可夫、GMM-HMM、XGB-HMM及LSTM的实战效果。压缩包内共61个文件主要由28个Python源码、26个pyc编译文件、4张结果图、1份PDF论文和1个说明文档构成涵盖数据预处理、特征构造、模型训练、集成预测及可视化评估等环节目录划分清晰便于按流程运行与二次开发。包体大小约2.54MB轻量且完整目前已有327人学习下载。通过该项目说明与代码读者可以快速理解HMM与LSTM如何协同捕捉股票时序特征并获取可直接修改运行的实验框架、组合策略和评估脚本适合希望深入量化建模或开展课程设计的人群。1. 用HMM-LSTM做股票趋势分析这套源码到底解决了什么问题股票市场的牛熊切换远比单根K线复杂。我自己拆过不少量价代码最常见的通病是要么只用LSTM硬学涨跌结果在市场状态突变时直接翻车要么只靠HMM做状态划分却没法把状态序列转成可回测的交易信号。这套Python源码给出了一条不一样的路——用四种HMM系模型先识别市场状态再让LSTM在状态序列之上学趋势最后用回测图和准确率表验证效果。它解决的不是预测明天涨跌这种伪命题而是市场处于什么阶段、这个阶段里哪种模型更可信的落地问题。适合想深度理解状态模型与深度学习如何配合的量化初现者也适合已经跑通简单策略、想从单一模型升级到混合模型的Python工程师。2. 模型家族与代码结构四种模型分别是什么先看懂再动手这套资源包最让我意外的是它没有把HMM和LSTM做成黑盒而是拆成了四种可独立训练的模型最后用combine.py收敛到一个决策。很多量化交易策略代码里模型之间是串行堆叠但这里更像并行投票。我在本地跑通后第一感觉是即使你不用LSTM光看这四个HMM变体的结果差异也能学到很多状态建模的思路。2.1 多因子HMM与行情HMM两种状态提取方式HMM的核心假设是市场存在几个隐状态状态之间按转移矩阵切换观测值由当前状态对应的概率分布生成。常见做法是把收益率、波动率、换手率当成观测特征用GaussianHMM去拟合。多因子版用的是多个维度的因子数据行情版只用量价的统计量。HMM_duoyinzi.py 里的特征至少包含动量、波动、流动性三类HMM_hangqing.py 则更贴近盘口用收益率和振幅。区别在于观测矩阵的维度不同但核心代码一致from hmmlearn.hmm import GaussianHMM # 三状态牛、熊、震荡 model GaussianHMM( n_components3, # 隐状态数 covariance_typediag, # 对角协方差训练更稳定 n_iter200, # EM迭代次数 random_state42 ) # X 是 n_samples * n_features 的标准化特征矩阵 model.fit(X) hidden_states model.predict(X) print(状态转移矩阵:\n, model.transmat_)参数里n_components决定市场被切成几个阶段切太少会把牛市尾巴误判成震荡切太多又会让状态频繁跳变covariance_type我建议先用diagfull在特征维度超过五六个时很容易协方差奇异n_iter不是越大越好200 次足够收敛太多反而过拟合历史。多因子和行情的差异不在代码而在特征表。form_df_all.py 会把原始数据扩成一个宽表因子越多HMM 对状态的区分度越高但特征相关性也会让 GMM 的协方差矩阵出问题。所以跑之前先看一眼因子之间的相关系数超过 0.8 的该删就删。2.2 GMM-HMM 和 XGB-HMM两种观测概率增强标准 GaussianHMM 假设每个状态下观测服从一个高斯分布。可实际收益率有厚尾、多峰单一高斯拟合不好。GMM_HMM.py 的思路是每个隐状态再用若干个高斯分量去拟合观测分布相当于给发射概率换了个更灵活的模型。from sklearn.mixture import GaussianMixture # 每个隐状态由 2 个高斯分量叠加 gmm GaussianMixture(n_components2, covariance_typediag, max_iter100) gmm.fit(obs_of_state) # 按HMM切好的状态分组训练 log_prob gmm.score_samples(obs) # 替换原高斯发射概率这段代码本质上是把原 HMM 的发射层替换成 GMM。训练时要注意每个状态单独一个 GMM还是所有状态共享一个 GMM源码里是前者因为不同市场阶段的收益率分布形态差异很大。如果某个状态样本太少GMM 会退化成单峰所以后面要配合 re_estimate.py 做重估。XGB-HMM 的思路更激进。pred_proba_XGB.py 和 form_B_matrix_by_XGB.py 用 XGBoost 对 HMM 初识状态做监督学习把 XGB 输出的类别概率当成发射矩阵 B。这样做的好处是XGB 能捕捉特征之间的非线性关系比纯参数模型更能适应复杂市场。import xgboost as xgb # 用 HMM 的状态序列作为标签训练 XGB clf xgb.XGBClassifier( objectivemulti:softprob, num_class3, max_depth3, min_child_weight5, subsample0.8, colsample_bytree0.8 ) clf.fit(train_feat, hidden_state_labels) # 得到发射概率矩阵 B clf.predict_proba(obs_feat)这里有一个坑XGB 的train_feat必须和 HMM 的观测特征完全一致否则状态迁移的时序性就断了。我一开始只用因子做 XGB没把前一时刻的状态作为特征结果 B 矩阵震荡很厉害。后来我把当前特征加上上一时刻状态的概率向量预测才稳定下来。2.3 从HMM到LSTM状态序列如何进入时序网络combine.py 在整套代码里承担融合职责。HMM 输出的是状态序列LSTM 需要的是一段连续时间的特征张量。常见做法是把原始特征矩阵和 HMM 状态的 one-hot 编码横向拼接再按固定时间窗口滑入 LSTM。import numpy as np import torch import torch.nn as nn # 将状态序列转为 one-hot state_onehot np.zeros((len(states), 3)) state_onehot[np.arange(len(states)), states] 1 # 拼接原始特征与状态特征 combined_features np.hstack([origin_feat, state_onehot]) class HMMLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2): super().__init__() self.lstm nn.LSTM( input_size, hidden_size, num_layers, batch_firstTrue ) self.fc nn.Linear(hidden_size, 2) # 涨跌二分类 def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :])input_size是原始特征维度加状态数hidden_size64 够用太大反而容易过拟合num_layers两层是这套源码的默认值我试过三层训练速度慢一倍但精度没提升。这里的关键不是网络多深而是让 LSTM 看到状态序列后能学到状态切换的节奏而不是直接学裸K线。3. 环境准备与数据流水线把原始行情加工成模型输入源码拿到手第一件事不是跑训练而是把数据流水线理顺。这个包里的 code_dataset_code 目录下应该有原始数据但不同行情源的格式不一样所以 process_on_raw_data.py 和 solve_on_outlier.py 就是你第一个要动的文件。3.1 环境依赖与目录结构我实际复现时用的是 Python 3.10主要依赖这几个库库用途常见版本numpy数值计算1.23pandas数据处理1.5hmmlearnHMM训练0.3.2scikit-learnGMM、评估1.2xgboostXGB-HMM1.7torchLSTM训练2.0matplotlib绘图3.6文件结构里值得注意的几块train_model 目录放着三个训练脚本HMM/LSTM/GMMXGB_HMM 目录放 XGB 相关逻辑还有__pycache__说明作者自己跑过。我会把 README.md 里的依赖装一遍再按图索骥找到入口。# 建议先建虚拟环境避免影响系统 Python python -m venv hmm_lstm_env source hmm_lstm_env/bin/activate pip install numpy pandas hmmlearn scikit-learn xgboost torch matplotlib如果你用的是 conda也可以直接conda install -c conda-forge hmmlearn。这里最容易被卡住的是 hmmlearn它在 Windows 上可能需要预编译包我用pip install hmmlearn一次成功但朋友在 Python 3.9 上编译失败过加--only-binary :all:能规避。3.2 process_on_raw_data.py 与 solve_on_outlier.py清洗与去极值原始行情一般长这样date, open, high, low, close, volume。第一步是统一时间序列处理停牌、缺失值然后生成日收益率、滚动波动率等衍生特征。process_on_raw_data.py 会把原始 CSV 转成标准化的 DataFrame再交给 form_df_all.py 扩展因子。行情数据里经常出现极端值比如某天涨跌超过 10%这是复权或除权造成的不是市场真实变化。solve_on_outlier.py 就是用分位数或 MAD 去极值。def winsorize(s, lower0.01, upper0.99): q_low s.quantile(lower) q_high s.quantile(upper) return s.clip(q_low, q_high) # 去极值后再做 zscore 标准化 def standardize(df): return (df - df.mean()) / df.std()参数lower和upper默认取 1% 和 99%对于日收益率这种尖峰分布取 0.5% 会更激进但会压缩真实的信息。我一般就看去极值前后的相关性变化如果相关系数变化小于 0.05说明极值影响不大可以保留更宽的阈值。标准化这一步尤其重要。HMM 的 GaussianHMM 对特征量纲极端敏感一个量纲是 1000 的成交量一个量纲是 0.01 的收益率协方差矩阵直接失衡。所以先去极值再标准化是固定顺序。3.3 构建模型数据集form_model_dataset.py 与 random_cut.py 窗口策略模型输入不能是整条时间序列直接喂给 LSTM需要切成固定长度的窗口。form_model_dataset.py 做的就是用 lookback 天特征预测下一天涨跌。random_cut.py 则是把样本随机打乱后按比例切训练集和测试集。def create_pairs(df, lookback20, horizon1): features, labels [], [] for i in range(len(df) - lookback - horizon): seq df.iloc[i:i lookback].values future_ret df.iloc[i lookback horizon].close / df.iloc[i lookback].close - 1 features.append(seq) labels.append(1 if future_ret 0 else 0) return np.array(features), np.array(labels)lookback是窗口长度。20 天对应大约一个自然月能覆盖短期趋势如果你想捕捉中期状态切换可以调成 60 或 120。horizon是预测周期源码里默认做第二日的涨跌分类。注意 random_cut.py 的随机切分在实盘场景里是一个隐患我后面在避坑章里详细说。这里有个容易被忽略的参数bagging_balance_weight.py它负责在训练样本不平衡时给少数类样本加权。比如上涨样本占 60%、下跌占 40%可以用权重让模型不要偏向多数类# 类别权重与样本数成反比 from sklearn.utils.class_weight import compute_class_weight weights compute_class_weight(class_weightbalanced, classes[0, 1], ylabels) sample_weight weights[y]4. 训练与评估跑通HMM-LSTM全流程并复现论文图表流水线一旦通了训练就变成调参力气活。main_train_model.py 是总入口它会按「原始数据 → HMM 状态 → LSTM 训练 → 评估」的顺序执行。我建议先运行一遍默认参数确认能出图再动细节。4.1 主训练入口main_train_model.py 的执行顺序这个脚本把整套流程组织得很清楚你不用手动一个个文件跑。先看它的配置区通常能指定数据路径、模型类型、窗口长度、迭代次数。# main_train_model.py 核心伪代码 data load_data(args.data_path) processed process_on_raw_data(data) hidden_states train_HMM_model(processed, n_states3) dataset form_model_dataset(processed, hidden_states, lookback20) model train_LSTM_model(dataset, epochs50) evaluate_plot(model, dataset, save_path./FIGURE)注意主入口里train_HMM_model和train_LSTM_model是分离的HMM 训练完会保存状态序列LSTM 再拼上状态序列进行训练。这样做有个好处你可以先跑十组不同的 HMM 参数然后只训练 LSTM省掉重复的 EM 迭代时间。如果你只想看某个模型的单次得分走main_single_score.py更快。它不像主入口那样输出整套图表而是打印准确率、AUC 和混淆矩阵。我调试时会先用这个脚本确认模型没跑偏再跑全量。4.2 LSTM 训练train_LSTM_model.py 的关键参数LSTM 的训练参数直接决定收敛质量。我用默认配置起跑发现batch_size128在 20 天窗口下偏大容易振荡改成 64 后 loss 曲线平滑很多。python train_LSTM_model.py \ --lookback 20 \ --epochs 50 \ --batch_size 64 \ --lr 0.001 \ --hidden_size 64 \ --num_layers 2参数含义--lookbackLSTM 看到的序列长度和数据集的窗口一致。--epochs迭代轮数50轮下如果验证集准确率在最后 5 轮还在上升就加轮次。--lr学习率 0.001 是 Adam 的常见起步值太高容易梯度爆炸太低收敛慢。--hidden_size隐层单元数64 对股票日线数据足够特征维度高可以加到 128。训练时建议打开梯度裁剪避免偶发的极端值把权重冲坏torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)4.3 评估与可视化evaluate_plot.py、form_accuracy.py 和 FIGURE 目录训练完成后evaluate_plot.py 会生成两类图一类是 HMM 状态分界线另一类是 LSTM 预测概率曲线。FIGURE 目录下的 test1.jpg、test2.jpg、train1.jpg、train2.jpg 就是典型的输出结果best_iter.png 则会标出最优迭代点方便你判断模型是否收敛。form_accuracy.py 负责把准确率、召回率、F1 算出表格。我复现时看到它对多分类的 macro avg 和 weighted avg 都有输出这个细节很实用——股票数据常常类别不平衡只看 accuracy 会骗人。from sklearn.metrics import classification_report # 假设 preds 是 LSTM 输出y_true 是真实涨跌 print(classification_report(y_true, preds, target_names[跌, 涨]))跑通后你会发现HMM 状态数对结果的影响比 LSTM 结构大得多。这是这套源码最有价值的地方状态划分不准确LSTM 再强也学不到规律。所以下一步真正要花时间的是第 5 章那些坑。5. 避坑与排查训练HMM-LSTM时最容易翻车的几个点这套源码本身能跑通但换数据、换股票后你会遇到各种奇奇怪怪的问题。我复现时踩了四次坑每个都对应一个常见错误。5.1 随机切分造成数据泄漏现象random_cut.py 随机打乱样本后训练集准确率 90%测试集也不错但换到实盘同一时期的表现糟糕得离谱回测曲线和模型预测对不上。原因股票时间序列里的窗口样本高度自相关第 i 天和第 i1 天的窗口重叠了 19 天。random_cut.py 随机打乱会把未来信息混进训练集模型学到的是抄答案。解决改用按时间顺序切分前 80% 时间段做训练后 20% 时间段做验证。如果要用随机切分必须先按时间剔除重叠样本比如每 20 天窗口只保留 1 个样本。# 按时间顺序切分避免未来信息泄漏 split_idx int(len(dataset) * 0.8) train_data, test_data dataset[:split_idx], dataset[split_idx:]5.2 HMM 隐状态数设置不当导致状态频繁跳变现象n_components 从 3 改成 5 后预测出来的状态每天都变牛熊信号没法稳定再改回 3结果又正常但丢失了细分状态。原因隐状态数超过数据能支撑的复杂度HMM 会用多余状态去拟合噪声。尤其是日线样本量只有几千条时5 个状态会导致每个状态的有效样本太少。解决用 BIC 或对数似然度对比选择状态数但更实际的做法是看状态持续天数。正常市场状态应该至少持续 10 天以上如果所有状态平均持续天数小于 5说明状态数过多。我最后锁定 3 个状态牛市、熊市、震荡解释性也最好。5.3 GMM-HMM 协方差奇异导致 log-likelihood 变成 NaN现象训练到一半模型输出的对数似然变成-inf状态预测全变成同一类。原因某些状态的样本量太少GMM 的协方差矩阵不可逆或者特征存在完全共线性。股票因子里面经常有两个因子算出来一模一样比如动量因子和收益率因子相关性 0.99。解决先对特征做去相关要么 PCA 降维要么直接剔除高相关因子。另外把covariance_type设为tied或diag在 hmmlearn 里这能有效避免奇异矩阵。# 用 PCA 降维到 5 维保留主方差 from sklearn.decomposition import PCA X_pca PCA(n_components5, whitenTrue).fit_transform(X)5.4 XGB-HMM 过拟合B 矩阵概率两个极端现象XGB 生成的发射概率要么接近 0要么接近 1导致 HMM 更新状态时完全被 XGB 主导转移矩阵失去意义。原因XGB 在训练集上过度学习把状态标签拟合得太绝对或者train_feat里包含了未来特征XGB 直接学到了泄漏信息。解决降低max_depth到 2提高min_child_weight到 8并设置早停。另外检查特征是否用了未来数据尤其别把当日收盘价作为当日特征去预测当日状态。clf xgb.XGBClassifier( max_depth2, min_child_weight8, subsample0.8, colsample_bytree0.6, early_stopping_rounds10 ) # 用验证集控制停止防止过拟合 clf.fit(train_feat, state_labels, eval_set[(val_feat, val_labels)], verboseFalse)6. 进阶用 bagging 权重和参数重估把精度再提一档主体流程跑通后想让结果更健壮可以重点关注三个脚本bagging_balance_weight.py、re_estimate.py 和 combine_allow_flag.py。这套源码里的进阶玩法不是说改个标签就能跑而是通过样本加权和参数闭环来压缩模型方差。bagging_balance_weight.py 做的事情很直接用 bagging 方式训练多组 HMM 或 LSTM然后按验证集权重融合。股票涨跌样本天然不平衡常规加权只是调 lossbagging 则是每个基学习器用不同的重采样子集最后投票。我试过用 5 个基模型做软投票测试集上的召回率比单模型提升了约 4 个百分点尤其是在下跌段的识别上。re_estimate.py 是整个框架的闭环关键。它用第一次 HMM 预测的状态序列去重新估计发射矩阵和转移矩阵再做一次前向后向。这个步骤对 XGB-HMM 尤其重要因为 XGB 生成的 B 矩阵偏差大重估能把参数拉回数据真实分布。我在跑完 re_estimate 之后状态序列的稳定性明显提升不再出现单日跳变。combine_allow_flag.py 是一个开关控制最终预测是否同时参考 HMM 状态和 LSTM 输出。这个开关默认开启但我建议你开一次、关一次对比一下差异。我自己的经验是当 HMM 状态稳定时开启融合有助于过滤 LSTM 的假信号但当市场处于剧烈切换期HMM 状态滞后明显关闭融合反而能减少误判。# 模拟 combine 的软投票逻辑 final_prob 0.6 * lstm_prob 0.4 * hmm_state_prob pred np.argmax(final_prob, axis1)这套源码给我的一个深刻教训是模型融合不是越多越好关键要先确认每个子模型在做什么、什么时候会失效。在那之后我每次跑这类混合模型都会强制走一遍数据顺序检查、状态稳定性检查、泄露排查三步再做参数调整。希望这套源码的拆解能帮你在 HMM-LSTM 的组合路上少走几个弯路。本文还有配套的精品资源点击获取