
简介一份基于 HMM-LSTM 融合的股票市场趋势分析 Python 源码项目内置四种模型实现与配套说明文档适合希望掌握时序预测与隐马尔可夫建模的研究者或量化学习者。资源包共61个文件以28个 Python 脚本和26个编译后的 pyc 文件为主另含结果图、论文原文及 README 说明压缩包仅约2.54MB轻巧且目录清晰便于快速查阅与二次开发。目前已有327人学习/下载作为中等规模的完整工程可承担课程设计、论文复现或实盘策略验证等多类任务。项目流程覆盖原始行情数据清洗、异常值处理、多因子构建以及 HMM 市场行情识别、LSTM 回归预测、GMM-HMM 与 XGBoost 融合等环节并提供模型训练、概率预测、评价对比与可视化绘图等脚本帮助读者端到端理解不同模型在股票趋势分析中的组合协作方式。此外包内还附带了模型训练过程中的最优迭代图、测试集对比图等可视化结果以及原始论文 PDF便于深入理解算法细节与实现背景。1. 用混合模型拆解股票趋势为什么单靠 LSTM 不够先给一个反直觉的结论在 A 股日线上直接拿 LSTM 预测涨跌测试集准确率长期在 48% 到 52% 之间徘徊甚至不如“昨天涨今天就买”的朴素规则。这不是网络结构不够深而是 LSTM 本质上在拟合一个连续函数它默认市场在每个时刻都处在同一个“状态”里。但股票走势明显不是这样拉升、洗盘、恐慌下跌、横盘这几种行情下的量价统计规律差异极大用同一套参数去拟合等于让一个模型同时做四种完全不同的任务。HMM隐马尔可夫模型恰好擅长做另一件事把可观测的量价序列反解成离散的隐藏状态序列。把 HMM 输出的状态信息喂给 LSTM让 LSTM 在“知道当前是趋势还是震荡”的前提下做预测效果会明显不同。本文要做的就是用四种模型做对比实验纯 LSTM 基线、HMM 状态标签辅助、HMM 后验概率作为特征以及 HMM 先切状态再分别建模。完整源码和项目说明在压缩包里下面把每一步拆开讲。2. HMM 和 LSTM 各自解决什么问题组合点在哪2.1 HMM 的观测独立性假设是缺点也是切入点HMM 的核心假设有两条第一当前隐藏状态 ( z_t ) 只由前一个状态 ( z_{t-1} ) 决定齐次马尔可夫性第二当前观测 ( x_t ) 只由当前状态 ( z_t ) 生成与其它时刻无关观测独立性。第二条在金融时序上几乎肯定是错的——今天的涨跌显然受昨天影响。但换个角度想正因为这个假设“过于简单”HMM 才能在有限样本下稳定地估计出状态转移矩阵不会像深度模型那样过拟合。用 GaussianHMM 拟合股票日线数据时观测变量通常选对数收益率和成交量变化率隐藏状态数设为 4。训练完成后模型给出的是每个时刻属于某个状态的后验概率。这个概率序列比硬分类的状态编号更有信息量比如某个时刻有 70% 概率处于“趋势上涨”20% 概率处于“震荡”这个分布本身就可以作为 LSTM 的额外特征。2.2 LSTM 擅长建模条件概率但缺乏状态感知LSTM 通过门控机制在时间维度上传递信息理论上能捕捉长距离依赖。但它在训练时看到的是“裸”的量价序列需要自己从数据里学出“现在是趋势还是震荡”这个抽象概念。问题是这种隐含状态没有显式监督信号模型很可能学到的是某种模糊的中间态而不是清晰的市场状态。把 HMM 和 LSTM 接起来本质上是把“状态识别”这件事从 LSTM 里拆出去交给一个概率模型显式完成。LSTM 的输入从 ( x_t ) 变成 ( [x_t, \gamma_t] )其中 ( \gamma_t ) 是 HMM 在时刻 ( t ) 的后验状态分布。这样 LSTM 只需要学“在给定状态下未来走势的条件分布”学习难度显著降低。2.3 四种模型的定位差异模型输入特征状态信息使用方式适用场景Model A 纯 LSTM量价特征无基线对照Model B HMM 状态标签量价特征 状态编号硬标签拼为 one-hot状态边界清晰时Model C HMM 后验概率量价特征 状态概率软概率直接拼特征状态模糊、重叠度高时Model D HMM 分治 LSTM按状态分组后的量价特征每个状态训练独立 LSTM状态区分度好、样本充足Model D 的思路更容易理解先用 HMM 把整个训练集切成 4 段每段对应一种市场状态然后针对每段分别训练一个 LSTM。预测时先算当前时刻属于哪个状态再调用对应的 LSTM。这个方案对数据量要求高但解释性最强——每个子模型只负责一种行情行为可预期。3. 用 Python 实现 HMM-LSTM 混合模型的最小可行版本3.1 数据预处理特征构造与数据集划分假设已经有了日线 OHLCV 数据常见的做法是用akshare或tushare拉取没有现成数据的情况下也可以用yfinance下载美股日线。特征列一般包括对数收益率、5 日动量、成交量变化率、最高价与收盘价之比。以下代码完成特征工程和标签构造import numpy as np import pandas as pd def make_features(df): df df.copy() # 对数收益率比简单涨跌幅更接近正态分布HMM 拟合效果更好 df[log_ret] np.log(df[close] / df[close].shift(1)) # 5 日动量捕捉短期趋势强度 df[mom_5] df[close] / df[close].shift(5) - 1 # 成交量变化率量能突变往往是状态切换的前兆 df[vol_chg] df[volume] / df[volume].rolling(5).mean() - 1 # 当日振幅反映多空分歧程度 df[amplitude] (df[high] - df[low]) / df[close] # 标签未来 5 日收益为正为 1否则为 0 df[future_ret] df[close].shift(-5) / df[close] - 1 df[label] (df[future_ret] 0).astype(int) return df.dropna()参数说明shift(-5)表示用未来第 5 天的收盘价计算收益这是为了匹配持仓周期。如果换成shift(1)标签会变成预测次日涨跌噪声极大模型几乎学不到有效信号。vol_chg用 5 日均量做分母是为了平滑单日极端成交量。注意这里不能直接用原始close作为 LSTM 输入必须做标准化。HMM 对输入尺度极其敏感GaussianHMM 假设观测服从高斯分布如果某个特征方差远大于其它特征模型会被该特征主导。from sklearn.preprocessing import StandardScaler feature_cols [log_ret, mom_5, vol_chg, amplitude] scaler StandardScaler() scaled scaler.fit_transform(df[feature_cols]) scaled_df pd.DataFrame(scaled, columnsfeature_cols, indexdf.index) # 按时间顺序切分严禁随机打乱 split_idx int(len(scaled_df) * 0.8) train_x scaled_df.iloc[:split_idx] train_y df[label].iloc[:split_idx] test_x scaled_df.iloc[split_idx:] test_y df[label].iloc[split_idx:]这里用时间顺序切分而不是随机切分原因在于金融时序存在时间依赖性随机打乱会让模型“偷看”未来数据导致回测结果虚高。3.2 训练 HMM 并提取状态后验概率HMM 用hmmlearn库的GaussianHMM。需要调的参数主要是n_components状态数和covariance_type协方差类型。from hmmlearn import hmm # 状态数 4 对应趋势上涨、趋势下跌、震荡、高波动回调 model_hmm hmm.GaussianHMM( n_components4, covariance_typefull, n_iter100, random_state42 ) model_hmm.fit(train_x.values) # 训练集和测试集都要转换注意 HMM 是有状态的序列必须按时间顺序传入 train_state_proba model_hmm.predict_proba(train_x.values) test_state_proba model_hmm.predict_proba(test_x.values)predict_proba返回的是形状为(n_samples, n_components)的矩阵每一行是某个时刻属于每个状态的概率行和为 1。这里用的是全协方差full意思是每个状态都有自己的协方差矩阵能刻画不同状态下的波动聚集性。如果你发现训练速度慢或者过拟合可以改成diag只保留对角线方差。HMM 有个值得注意的问题状态编号本身没有语义第一次运行完可能编号 0 代表上涨第二次运行编号 1 才代表上涨。这跟初始化有关。解决办法是训练后根据每个状态的平均收益率重命名状态或者直接用后验概率矩阵就不用关心编号顺序。3.3 LSTM 模型结构把 HMM 概率拼进输入LSTM 用 PyTorch 实现输入维度是4原始特征 4HMM 状态概率 8。模型结构采用两层 LSTM 加一个全连接输出层import torch import torch.nn as nn class HmmLstm(nn.Module): def __init__(self, input_size8, hidden_size128, num_layers2, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 1) ) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一层所有时刻的隐状态 out out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(out).squeeze(-1)训练时最关键的一步是把 HMM 概率拼到每个时间步上。不能只在序列末尾拼一次那样 LSTM 无法在早期时间步感知状态。正确做法是对每个时间步的输入向量都拼接上该时刻的状态概率。def create_sequences(features, proba, labels, seq_len20): xs, ys [], [] for i in range(seq_len, len(features)): feat_seq features[i - seq_len:i] prob_seq proba[i - seq_len:i] # 时间步维度的拼接每个时间步都是 4 维特征 4 维概率 combined np.concatenate([feat_seq, prob_seq], axis1) xs.append(combined) ys.append(labels[i]) return np.array(xs), np.array(ys)seq_len设为 20 个交易日约等于一个自然月能覆盖大多数短期趋势的完整周期。如果设太长比如 60早期信息会被遗忘门稀释太短比如 5则捕捉不到状态切换。3.4 训练循环与早停def train_model(model, train_loader, val_loader, epochs50, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-4) criterion nn.BCEWithLogitsLoss() best_val_loss float(inf) patience 0 for epoch in range(epochs): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() logits model(xb) loss criterion(logits, yb.float()) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() # 验证集早停patience 连续 5 轮不下降就终止 model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: logits model(xb) val_loss criterion(logits, yb.float()).item() if val_loss best_val_loss: best_val_loss val_loss patience 0 torch.save(model.state_dict(), best_model.pt) else: patience 1 if patience 5: breakclip_grad_norm_这行容易被忽略但对 LSTM 至关重要。时间序列模型在反向传播时容易出现梯度爆炸尤其是在序列长度较长时。限制梯度范数在 1.0能稳定训练过程。weight_decay加 L2 正则也能防止过拟合。4. 四种模型的对比实验设计与参数调优4.1 基线设定不能只比准确率对比实验最容易犯的错是只看准确率。股票涨跌预测中负样本下跌通常略多于正样本模型只要全预测下跌就能获得不错准确率但这毫无交易价值。更合理的评估指标是 F1-score、AUC以及模拟交易后的年化收益率和最大回撤。评估方式用 walk-forward 而非单次切分。把数据按时间分成 5 折每折用前 80% 训练、后 20% 验证然后滚动。这样能验证模型在不同市场阶段牛市、熊市、震荡市下的稳定性也更容易暴露过拟合问题。4.2 四种模型的具体实现差异Model A 是纯 LSTM输入只有feature_cols不加 HMM 信息。Model B 在 HMM 训练完后把predict得到的硬标签做 one-hot 编码拼接到每个时间步。Model C 用predict_proba的软概率拼接这是最推荐的方案因为硬标签会丢失不确定性信息——当模型对某个时刻的状态只有 50% 把握时硬编码成 0 或 1 都是在制造噪声。Model D 则完全不同先按训练集的 HMM 状态把样本分成 4 组每组训练一个独立的单层 LSTM输入只有原始量价特征。需要强调的是Model D 的 LSTM 输入序列应该来自同一状态的连续片段而不是把同一状态的所有样本强行拼在一起。举例来说如果训练集中状态 0 出现的时间段是第 100-150 天和第 300-320 天那就应该对这两段分别构造序列而不是把两段数据首尾相连。否则 LSTM 会学到一段不存在的跨状态依赖。4.3 关键参数参考表参数推荐值调整方向HMM 状态数4状态数太多会导致某些状态样本过少LSTM 学不动太少则无法区分震荡与趋势LSTM 层数2超过 2 层容易过拟合日线数据量不足以支撑深层网络隐层维度128数据量大可加 256小样本 64 就够Dropout0.3训练集准确率远高于验证集时调大到 0.5学习率1e-3训练震荡时降到 5e-4序列长度20长周期策略可加 30日内策略减到 104.4 实验结果解读什么样的提升才算有效实验做下来纯 LSTM 的测试集准确率通常在 50% 上下浮动F1 值在 0.45 到 0.55 之间。加入 HMM 后验概率的 Model CF1 通常能提升 5 到 8 个百分点主要提升在“下跌状态”的召回率上——HMM 对高波动状态识别得比较准LSTM 在这个状态下更容易学会回避。Model B 的提升幅度略低于 Model C原因就是硬标签丢失了概率信息。Model D 的结果则分两种情况如果 HMM 切出的 4 个状态在样本量上相对均衡每个状态至少占 15% 以上单状态 LSTM 的准确率会明显高于统一模型但如果某个状态样本极少比如“高波动下跌”只有几百条独立模型会严重过拟合效果反而不如 Model C。这里要提醒一句任何模型在测试集上的提升都不直接等于能赚钱。预测准确率 55%、F1 值 0.6 的水平扣掉交易成本后可能刚好打平。HMM-LSTM 的价值在于提供比随机更好的方向判断真正的收益还需要靠仓位管理和止损逻辑来放大。5. 涨跌停过滤与滚动回测验证 HMM-LSTM 是否真的有用最后一个值得单列的技巧是训练和回测时对涨跌停样本的处理。A 股有 10% 涨跌幅限制涨停时买不进、跌停时卖不出如果模型预测某天涨停并给出买入信号实际成交根本做不到。正确的做法是把这些样本从训练集和测试集里都去掉或者在标签生成时把包含涨跌停的样本权重降为零。def filter_limit_up_down(df): # 去掉当天涨停或跌停的样本避免不可成交信号干扰训练 limit_up df[close] df[close].shift(1) * 1.095 limit_down df[close] df[close].shift(1) * 0.905 return df[~(limit_up | limit_down)].copy()阈值 1.095 和 0.905 留了 0.5% 的容差因为实际交易中收盘价可能正好卡在 9.96% 或 10.04% 这种边界上。过滤后重新训练 Model C你会发现测试集的 AUC 可能不升反降但模拟交易的净值曲线更真实——之前的高分里混着不少“纸面富贵”。验证模型价值时不要只输出一张准确率表。写一个简单的滚动回测按模型预测的上涨概率排序只在概率超过 0.6 时做多、低于 0.4 时做空每次开仓用固定仓位同时记录最大回撤。这一步能暴露前文所有实验里藏着的问题比如模型在牛市阶段表现很好但在震荡市连续亏损。回测代码只需要几十行就能完成实现过程中你会自然理解“预测概率并不等于胜率”这句话的份量。def backtest(proba, labels, threshold0.6): position 0 equity [1.0] for p, y in zip(proba, labels): if p threshold and position 0: position 1 elif p (1 - threshold) and position 0: position -1 # 简化收益预测对赚 1%预测错亏 1% daily_pnl position * (1 if y 1 else -1) * 0.01 equity.append(equity[-1] * (1 daily_pnl)) return equity把回测曲线的最大回撤计算出来如果超过 20%说明模型的预测信号在市场风格切换时失效过快这时应该检查 HMM 是否把新行情误判成了旧状态。实践中我通常会打印 HMM 各个状态在最近一段时间的平均收益率对比历史值。如果状态含义发生了漂移即使 LSTM 的预测概率没有变化也要对信号打个折扣。本文还有配套的精品资源点击获取