基于LSTM预测比特币和黄金价格的综合决策模型实战
简介这套基于长短期记忆网络LSTM的综合决策模型源码用于对比特币和黄金价格走势进行预测面向量化研究者、金融数据分析爱好者及深度学习初学者。项目按照数据、核心工作模块、模拟测试和成果阐述四大目录组织覆盖数据清洗与归一化、LSTM网络构建、训练验证、MACD/RSI/灰色预测等辅助信号融合以及买卖时机与资产变化评估。压缩包共97个文件以42个CSV数据文件、38个Python脚本为主另含TXT说明、XML配置及论文PDF整体仅1.35MB便于快速下载和本地运行。已有215人学习使用。借助该资源读者可掌握时间序列回归建模的完整流程获得可直接运行和修改的预测回测脚本并通过中间数据文件深入理解策略表现差异为实际投资决策提供参考。1. 基于LSTM预测比特币和黄金价格的综合决策模型到底在解决什么问题做量化交易的朋友应该都有这种体验比特币和黄金是两个脾气完全不同的品种——比特币24小时不收盘、受消息面驱动剧烈波动黄金则跟实际利率和美元指数绑得更紧。如果你只用单一模型去预测其中一个往往换一个品种就失效模型推倒重来。基于LSTM预测比特币和黄金价格走势的综合决策模型就是用一套LSTM网络同时处理两个品种的价格序列再把预测输出转成可执行的买入、持有、卖出信号而不是只给你一个明天会涨还是会跌的概率黑匣子。这个方案的落地价值在于它把时间序列预测和交易决策打通了。你拿到的不只是模型文件和预测曲线还有一套从数据清洗、特征构建、模型训练到信号合成的完整链路。适合有Python基础、想跑通LSTM时间序列预测并把它用到实际行情上的开发者——注意我说的是行情研判不是稳赚这个区别后面会反复提到。本文按我的实操习惯把数据、模型、决策层、避坑点依次拆开讲。2. 喂给LSTM前的数据工程价格对齐、归一化与滑动窗口构建2.1 数据源选择与时间戳对齐比特币和黄金的时区战争比特币和黄金最直观的数据差异在交易时间。黄金在常规时段跟着COMEX期货走周末和节假日休市比特币是7×24小时交易从不休息。如果你直接把两个品种的日线数据拉下来拼在一起训练会因为时间轴错位产生大量假缺失值——比特币有K线的那天黄金可能根本没有数据。我一般会选择按UTC时间做对齐以黄金的交易时间为基准把比特币的日线数据重采样到同样的时间戳上。具体做法是用pandas的resample或merge_asof做最近邻对齐。常见的数据源是Yahoo Finance和CoinGecko的公开接口它们都提供历史K线下载但字段命名和时区处理方式不一样落地时统一转成UTC时间戳再做inner join。import pandas as pd def align_btc_gold(btc_df, gold_df): # 统一时间戳为UTC日期去掉时分秒 btc_df[date] pd.to_datetime(btc_df[timestamp], utcTrue).dt.date gold_df[date] pd.to_datetime(gold_df[date], utcTrue).dt.date # 以黄金交易日为基准比特币做最近邻对齐 aligned pd.merge_asof( gold_df.sort_values(date), btc_df.sort_values(date), ondate, directionnearest, tolerancepd.Timedelta(3 days) # 容忍不超过3天的错位 ) aligned aligned.dropna(subset[btc_close, gold_close]) return aligned这段代码要说明几个点merge_asof是按时间顺序做最近邻匹配directionnearest允许比特币K线在黄金休市期间前后找最近的一天tolerance3天是防止周末长假把完全不相干的数据强行拼在一起。对齐后必须做dropna否则LSTM会学到昨天有数据今天没数据这种虚假模式。价格字段建议统一取收盘价开盘价在高频策略里有用日线级别用收盘价最稳。2.2 归一化整段MinMax是数据泄漏的源头LSTM对输入尺度极其敏感——tanh和sigmoid激活函数在输入绝对值大时梯度会饱和。所以归一化是必须的但这里有个血泪经验千万不能在整个数据集上做MinMax归一化。因为你是在用未来的最大值和最小值去缩放过去的数据这属于典型的数据泄漏回测时模型表现好得离谱实盘一跑就翻车。正确的做法是先划分训练集在训练集上计算min和max然后用这个scaler去变换验证集和测试集。更激进一点的做法是用滚动归一化——每个窗口用自己的前N根K线计算统计量但这会让序列不再保持全局可比性我一般只在特征工程层面用模型输入还是用固定scaler。from sklearn.preprocessing import MinMaxScaler def prepare_scaled_data(aligned_df, feature_cols[btc_close, gold_close]): # 按时间排序后划分前70%训练后30%测试 train_size int(len(aligned_df) * 0.7) train_df aligned_df.iloc[:train_size] test_df aligned_df.iloc[train_size:] scaler MinMaxScaler(feature_range(0, 1)) # 只在训练集上fit测试集只用transform train_scaled scaler.fit_transform(train_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols]) return train_scaled, test_scaled, scaler这里的关键是fit_transform和transform分开用。很多刚上手的朋友图省事把全量数据丢给fit_transform结果验证指标虚高这就是前面说的数据泄漏。另外注意feature_range我习惯用(0,1)用(-1,1)也可以但LSTM的tanh输出范围是(-1,1)如果标签也用(-1,1)范围收敛会稍微快一点这个可以自己实验对比。2.3 滑动窗口构建seq_len是第一个要调的玄学参数LSTM不能直接吃一整段历史它吃的是最近N天的窗口。这个N就是序列长度seq_len它的选择直接影响模型能看到多远的依赖关系。比特币这种受消息面驱动的品种3到5天的窗口往往比30天更有用——因为市场对消息的反应衰减很快黄金则相反它与美债收益率的关联有较强的趋势惯性10到20天的窗口效果更好。我做综合模型时的常见做法是让两个品种共享同一个seq_len取中间值15天。这样模型被迫学习两个品种的共同时间尺度也能减少超参数搜索空间。import numpy as np import torch def create_sequences(data, seq_len15): xs, ys [], [] for i in range(seq_len, len(data)): # 用前seq_len天预测第seq_len1天的收盘价 xs.append(data[i - seq_len:i]) ys.append(data[i]) return np.array(xs), np.array(ys) # 假设train_scaled是shape为(N, 2)的数组两列分别是btc和gold X_train, y_train create_sequences(train_scaled, seq_len15) X_test, y_test create_sequences(test_scaled, seq_len15) # 转成PyTorch张量LSTM输入形状是(seq_len, batch, features) X_train_t torch.tensor(X_train, dtypetorch.float32).permute(1, 0, 2) y_train_t torch.tensor(y_train, dtypetorch.float32)注意最后那行permute——PyTorch LSTM的输入维度顺序是(seq_len, batch, input_size)不是常见的(batch, seq_len, input_size)。这个顺序搞反了是LSTM新手报错的重灾区RuntimeError: Expected hidden size (2, 8, 64), got (2, 15, 64)之类的错误几乎都是这个原因。后面在模型里也可以用batch_firstTrue省掉这个麻烦我习惯在模型定义里设置数据层保持不变。3. LSTM模型结构设计hidden_size、层数与训练参数怎么定3.1 为什么LSTM是价格预测的默认选择门控机制与记忆长度先回答一个问题既然是预测时间序列ARIMA、Prophet不也能做吗为什么是LSTMARIMA是线性模型它对价格序列里的非线性模式——突发拉升、恐慌抛售、横盘后的突破——几乎无能为力。Prophet是趋势分解思路对节假日和周期性有效但加密市场没有周末效应黄金又受宏观经济数据的脉冲影响Prophet的节假日参数很难配置。LSTM的核心是三个门遗忘门、输入门、输出门。它靠遗忘门决定丢弃多少旧信息输入门决定记住多少新信息。这意味着它在长期依赖和短期波动之间有一个可学习的平衡——这正好是黄金和比特币价格序列最需要的。黄金的慢变量趋势实际利率、美元指数可以被遗忘门保留为长期记忆比特币的突发波动则通过输入门快速吸收。用同一个LSTM处理这两类信号等于让模型自己学两个品种的记忆节奏。import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size2, hidden_size64, num_layers2, seq_len15, output_size2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.fc nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): # x: (batch, seq_len, 2) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(last_out)两点说明input_size2是因为输入特征是比特币收盘价和黄金收盘价两列output_size2是同时预测两个品种的下一天收盘价。这里有一个架构选型的细节——两个品种是共享同一个LSTM层还是各自独立LSTM共享层的优势是参数少、能学习跨品种的联动特征比如比特币暴跌时黄金是否避险上涨这类关系劣势是如果两个品种的数据分布差异太大共享层可能互相干扰。我实际跑过的经验是日线级别、输入特征只有收盘价时共享层效果更好如果你加入成交量、资金费率等异质特征就得分品种建独立LSTM再在决策层融合。3.2 训练循环从loss曲线判断模型有没有在认真学习训练循环看起来简单但有几个细节直接影响收敛质量。第一是损失函数价格预测本质是回归问题用MSELoss。第二是优化器Adam是默认选择但学习率要小心我一般从0.001开始如果loss震荡剧烈就降到0.0003。第三是早停价格序列噪声很大训练轮数过多必然过拟合验证集loss开始上升时立即停止。import torch.optim as optim model PriceLSTM(input_size2, hidden_size64, num_layers2, seq_len15, output_size2) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) # 训练循环 epochs 200 best_val_loss float(inf) patience 15 counter 0 for epoch in range(epochs): model.train() total_loss 0.0 # 用batch方式遍历X_train_t for i in range(0, len(X_train_t), 64): batch_x X_train_t[i:i64] batch_y y_train_t[i:i64] optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() # 梯度裁剪防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() # 验证 model.eval() with torch.no_grad(): val_pred model(X_val_t) val_loss criterion(val_pred, y_val_t).item() if val_loss best_val_loss: best_val_loss val_loss counter 0 torch.save(model.state_dict(), best_model.pth) else: counter 1 if counter patience: print(fepoch {epoch}: early stop) breakclip_grad_norm_这行代码值得单独说。LSTM的梯度在时间维度上连乘很容易指数爆炸特别是序列长度超过20时loss突然变成nan基本都是这个原因。梯度裁剪不是可有可无的防御而是LSTM训练的标配。另外每个epoch都要在训练后切到model.eval()模式做验证——eval()会关闭dropout和batch norm的训练行为如果你忘记切换验证集loss会忽高忽低让你误判模型性能。3.3 参数选择逻辑hidden_size和num_layers不是越大越好hidden_size64、num_layers2是我在日线级别、两个品种联合预测时的默认起点。为什么不是128或256因为你的训练数据量就那么多——比特币2015年之前的日线数据质量差黄金的完整日线也就几十年的量实际洗干净后可能只有几千条样本。hidden_size越大参数量呈平方级增长几千条样本撑不起百万级参数的网络。这是价格预测LSTM最常见的过拟合来源不是模型不够强而是模型太强了。num_layers2比1好的地方在于能捕捉更抽象的时间特征——第一层学短期形态第二层学这些形态的组合规律。但到3层以上收益就很小了训练时间和过拟合风险却明显上升。layer之间的dropout要加一般设0.2到0.3注意PyTorch的nn.LSTM里有个dropout参数但它只在num_layers 1时生效层数为1时设置它会直接报错或静默失效。判断参数合不合适不要只看训练集loss。我习惯记录训练集和验证集每个epoch的loss如果训练集loss持续下降但验证集loss在第20个epoch开始反弹说明模型开始死记硬背训练数据了——这就是要早停的信号点。4. 综合决策模型把预测值变成能下单的信号而不是一个数字4.1 方向准确率比价格误差更重要决策层的价值转换这是整个方案里最关键的一步也是最容易被新手忽略的一步。很多人训练完LSTM看到预测曲线和真实曲线看起来贴合就觉得模型成功了。但做交易决策时你根本不需要精确预测明天的收盘价是多少——你需要的是方向明天是涨还是跌概率多大。哪怕预测价格偏差5%只要方向判断准确率超过55%配合仓位管理就能产生正期望。所以综合决策模型的作用是把LSTM的连续预测值做一层转换。我常用的方法是计算预测值与当前真实价格的差值得到预测涨跌幅再与一个可配置的阈值比较超过阈值才生成信号低于阈值视为不确定。这个阈值就是你愿意承担的风险边界——阈值设得太小噪声会频繁触发交易太大则会错过真正的趋势启动。def generate_signal(pred_price, current_price, threshold0.01): # 阈值表示预测涨跌幅超过1%才触发信号 change (pred_price - current_price) / current_price if change threshold: return buy elif change -threshold: return sell else: return hold很多人会忽略这个阈值对回测结果的巨大影响。阈值从0.5%调到1.5%交易次数可能从200次降到40次而净利润不一定下降因为每次交易的滑点和手续费省下来了。这个阈值本质上是在过滤掉低置信度的预测它不是一个固定值需要根据你对模型的方向准确率认知去做网格搜索。4.2 多信号合成LSTM预测与MACD、RSI的加权决策单独靠LSTM信号做决策是不够的。价格序列里有很多LSTM学不到的东西比如MACD的双底形态在震荡行情里的参考价值很高RSI的极端值则能提示超买超卖。综合决策模型的综合二字正体现在这里——把LSTM的方向预测与经典技术指标加权合成一个综合得分。我给一个实际配置过的加权方案。LSTM信号权重0.5MACD信号权重0.3RSI信号权重0.2。每个子信号输出1看多、-1看空或0中性加权求和后得到一个综合得分再设定得分阈值决定最终动作。这样做的好处是当LSTM在某些极端行情里预测与指标矛盾时系统不会盲目跟从单一信号源。def macd_signal(short_ma, long_ma, prev_short_ma, prev_long_ma): # MACD本质是快慢均线的差值金叉死叉看两根线的关系变化 if short_ma long_ma and prev_short_ma prev_long_ma: return 1 # 金叉看多 elif short_ma long_ma and prev_short_ma prev_long_ma: return -1 # 死叉看空 return 0 def rsi_signal(rsi_value, low30, high70): if rsi_value low: return 1 # 超卖反弹预期 elif rsi_value high: return -1 # 超买回调预期 return 0 def composite_score(lstm_score, macd_score, rsi_score, w_lstm0.5, w_macd0.3, w_rsi0.2): score w_lstm * lstm_score w_macd * macd_score w_rsi * rsi_score if score 0.3: return buy elif score -0.3: return sell else: return hold这三个子信号是并行计算的互不阻塞可以单独验证每个信号的历史胜率。如果发现某个信号长期是负贡献——比如RSI在趋势行情里总是给出反信号——应该直接把它从合成逻辑里剔除而不是强行保留。这就是综合决策模型与堆砌指标的本质区别前者有明确的权重和淘汰机制后者只是把能用的指标全部画在图上。4.3 双品种的资金分配预测置信度决定仓位大小综合决策模型的最后一步是把两个品种的预测结果统一到一个投资组合层面。比特币波动大黄金波动小如果都用相同的仓位比例组合的风险几乎被比特币主导。我的做法是根据LSTM对两个品种的预测置信度动态分配资金。置信度的计算方式有很多种最直接的是用模型预测值的稳定性——连续N天预测方向一致则认为置信度高频繁反转则认为置信度低。也可以用MC Dropout在推理阶段多次前向传播用预测值的标准差来衡量不确定性标准差越小置信度越高。MC Dropout实现简单在训练时开dropout、推理时也保持dropout跑20次前向取均值和方差def mc_dropout_confidence(model, x, samples20): model.train() # 开启dropout但不需要计算梯度 preds [] with torch.no_grad(): for _ in range(samples): preds.append(model(x).numpy()) preds np.array(preds) mean_pred preds.mean(axis0) std_pred preds.std(axis0) return mean_pred, std_pred这里有个容易踩的坑MC Dropout要求模型里必须真的有dropout层否则开不开train模式结果都一样。而且model.train()模式下如果有batch norm层推理的统计量会被污染解决办法是只对LSTM和全连接层加dropout不用batch norm或者手写一个前向函数。得到两个品种的置信度后把总资金的60%分配给置信度高的品种40%分给另一个这是一种极其简化的仓位管理。实际上一线做法会结合品种的历史波动率ATR做进一步的每笔风险控制但那属于交易系统的范畴不在模型代码的边界内。这个方案的价值在于决策层不再是买或不买的黑匣子而是每个信号都能追溯到具体的预测、指标和置信度来源。5. LSTM价格预测避坑指南数据泄漏、过拟合与回测翻车5.1 归一化泄漏回测冠军、实盘亏损的头号元凶现象模型在验证集上方向准确率做到70%以上预测曲线和真实曲线几乎重合一上实盘立即亏损。原因数据预处理时对整个数据集做了MinMaxScaler的fit_transform未来的最大值和最小值参与了历史数据的缩放。LSTM学到的规律里混入了未来信息验证集本身就是作弊环境。解决严格按时间顺序划分训练集和测试集scaler只fit训练集。交叉验证时也要避免随机打乱样本——价格序列只能用时间序列交叉验证如TimeSeriesSplit不能用K-Fold。5.2 序列数据里的未来函数用t日信息预测t日现象训练loss很低但每次预测都像是前一天价格的复制几乎没有超前性。原因构建滑动窗口时把包含当天收盘价的序列同时用于输入和标签。比如用第1天到第15天的数据预测第15天这不是预测未来是在做同一天映射。正确做法是用第1到第15天预测第16天中间必须隔开至少一个时间步。解决create_sequences函数里标签索引用data[i]对应窗口data[i-seq_len:i]这样标签严格在窗口之后。写代码时最容易犯的错是Python切片边界我每次构建完都会打印X[0]的最后一行和y[0]做人工比对确认标签是下一根K线而不是当前K线。5.3 过拟合hidden_size128时的训练loss是0.1验证loss是2.5现象训练集loss一路降到0.1左右验证集loss在0.4附近震荡后开始上升最终到2.5。模型对训练数据倒背如流对新数据基本靠猜。原因日线级价格数据量本来就少几千条hidden_size和num_layers设置过大参数量远超样本量模型直接记住了训练集的噪声。另一个原因是训练轮数过多early stopping的patience设太大。解决把hidden_size降到32或64num_layers保持1到2层dropout设到0.3patience设为10到15轮。更激进的对策是只用LSTM输出的最后一个时间步做预测而不是把所有时间步的输出都接全连接层——后者给了模型记忆中间噪声的通道。前面的代码已经采用了out[:, -1, :]的写法这是更稳的。5.4 回测的幸存者偏差信号频率和滑点成本没算进去现象回测曲线很好看年化收益20%以上但把交易成本算进去后净利润变为负值。原因决策阈值设得太低比如0.2%信号频繁触发真实交易中的手续费、滑点、买卖价差吃掉全部利润。回测脚本里如果忽略这些成本系统会倾向于高频交易因为模型在噪声里也能找到微弱的规律。解决回测时每笔交易固定扣除一个成本比例比特币按0.15%黄金按0.05%计或者用实际交易平台的费率档位。同时把决策阈值从0.5%到2%做网格搜索找到净利润和交易次数的平衡点。需要特别注意的是加密货币市场的资金费率、提币网络费用在小资金账户里占比很高回测时不能忽略。6. 验证模型能不能用回测指标、夏普比率与注意力机制进阶模型训练完第一件事不是看预测曲线而是算三个数字交易次数、胜率、净利润。我习惯把回测结果输出成一个包含每笔交易时间、入场价、出场价、盈亏的表然后逐笔人工抽查——如果发现某几笔经典牛刀交易是靠单根异常K线触发的说明决策阈值还需要调。夏普比率是衡量模型风险调整后收益的硬指标。计算公式是策略收益率均值 - 无风险利率/ 收益率标准差日线级别的回测我要求夏普至少达到1.2低于这个数说明模型盈利不稳定大概率是抓到了几个大行情而不是有持续能力。最大回撤也要记录LSTM模型在趋势反转时反应慢半拍回撤超过20%就说明决策层对行情的转折识别太迟钝需要降低决策阈值或增加趋势过滤。进阶方向上两个值得投入的改进效果都经过了实盘验证。第一是给LSTM加入注意力机制——LSTM对长序列的记忆会衰减注意力层能让模型自动加权最重要的那几个时间步在黄金这种趋势性强的品种上尤其明显。第二是把两个品种的独立LSTM改成共享Embedding层加品种分叉的孪生结构让模型先学习跨品种的宏观模式再各自预测。最后说一个我自己踩过的教训我做过一版模型的回测年化收益做到35%但上线之后连续两个月跑输比特币现货持有策略。问题出在验证集划分太随意我没有把2020年3月的极端流动性危机数据排除在训练集外——模型见过历史最强波动后对正常行情的预测就过度自信了。后来我把极端行情单日涨跌幅超过10%从训练集里单独抽出来做压力测试样本模型的表现才算真实可信。希望帮到你——这个方案的方向没错但验证的严谨程度决定了它是研究玩具还是生产工具。本文还有配套的精品资源点击获取