news 2026/9/10 18:50:08

HMM与LSTM混合模型:提升股票趋势预测准确率的实战方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HMM与LSTM混合模型:提升股票趋势预测准确率的实战方法

简介:一份基于 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 数据,常见的做法是用aksharetushare拉取,没有现成数据的情况下也可以用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, columns=feature_cols, index=df.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_components=4, covariance_type='full', n_iter=100, random_state=42 ) 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(原始特征) + 4(HMM 状态概率) = 8。模型结构采用两层 LSTM 加一个全连接输出层:

import torch import torch.nn as nn class HmmLstm(nn.Module): def __init__(self, input_size=8, hidden_size=128, num_layers=2, dropout=0.3): super().__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.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_len=20): 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], axis=1) 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, epochs=50, lr=1e-3): optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-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_norm=1.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: break

clip_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,日内策略减到 10
4.4 实验结果解读:什么样的提升才算有效

实验做下来,纯 LSTM 的测试集准确率通常在 50% 上下浮动,F1 值在 0.45 到 0.55 之间。加入 HMM 后验概率的 Model C,F1 通常能提升 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, threshold=0.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 的预测概率没有变化,也要对信号打个折扣。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:50:00

Android音频开发:AudioSessionId原理与实战应用

1. 项目背景与核心价值在Android音视频开发领域&#xff0c;AudioSessionId是一个容易被忽视但极其重要的概念。作为一位经历过多个大型音频项目的老兵&#xff0c;我深刻理解这个看似简单的标识符背后所承载的技术重量。记得在开发某直播应用时&#xff0c;我们团队曾因为对Au…

作者头像 李华
网站建设 2026/9/10 18:49:38

R-precision详解:文本生成图像评估的核心排序指标

简介&#xff1a;本资源是一套面向文本生成图像&#xff08;T2I&#xff09;领域研究者与算法工程师的R-precision定量评估工程实现&#xff0c;聚焦于文本-图像跨模态对齐性的精准度量&#xff0c;适用于AttnGAN、CLIP-based生成模型等主流架构的性能验证与对比实验。压缩包共…

作者头像 李华
网站建设 2026/9/10 18:48:11

8款精选AI论文写作工具横向实测,本硕博避坑选型手册

前言&#xff1a;AI 写论文乱象频发&#xff0c;实测 8 款工具理清适配边界 每到毕业季&#xff0c;本科生、硕博生都会集中寻找 AI 论文辅助工具&#xff0c;市面各类写作软件层出不穷&#xff0c;但普遍存在几类硬伤&#xff1a;虚假参考文献、无法匹配本校格式、不支持公式代…

作者头像 李华
网站建设 2026/9/10 18:48:05

分布式日志系统实战:从ELK到Kafka的搭建与排坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:46:32

Python控制流详解:if条件、for/while循环与性能优化

1. 控制流基础概念解析 程序执行顺序的控制是编程中最基础也最重要的概念之一。控制流&#xff08;Control Flow&#xff09;决定了代码的执行路径&#xff0c;就像交通信号灯指挥车辆行驶方向一样。在Python中&#xff0c;if条件判断、for循环和while循环构成了最基本的控制流…

作者头像 李华
网站建设 2026/9/10 18:45:32

Python代码质量检查:Pylint与Flake8实战指南

1. 为什么需要代码质量检查工具 刚入行时我总觉得"能跑就行"&#xff0c;直到有次凌晨三点被叫起来修复生产环境Bug——就因为有人写了 if x 1 这种低级错误。这种经历让我明白&#xff0c;代码质量不是玄学&#xff0c;而是直接影响项目成败的关键因素。Python作…

作者头像 李华