简介:这套基于长短期记忆网络(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'], utc=True).dt.date gold_df['date'] = pd.to_datetime(gold_df['date'], utc=True).dt.date # 以黄金交易日为基准,比特币做最近邻对齐 aligned = pd.merge_asof( gold_df.sort_values('date'), btc_df.sort_values('date'), on='date', direction='nearest', tolerance=pd.Timedelta('3 days') # 容忍不超过3天的错位 ) aligned = aligned.dropna(subset=['btc_close', 'gold_close']) return aligned这段代码要说明几个点:merge_asof是按时间顺序做最近邻匹配,direction='nearest'允许比特币K线在黄金休市期间前后找最近的一天,tolerance=3天是防止周末长假把完全不相干的数据强行拼在一起。对齐后必须做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_len=15): xs, ys = [], [] for i in range(seq_len, len(data)): # 用前seq_len天预测第seq_len+1天的收盘价 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_len=15) X_test, y_test = create_sequences(test_scaled, seq_len=15) # 转成PyTorch张量:LSTM输入形状是(seq_len, batch, features) X_train_t = torch.tensor(X_train, dtype=torch.float32).permute(1, 0, 2) y_train_t = torch.tensor(y_train, dtype=torch.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_first=True省掉这个麻烦,我习惯在模型定义里设置,数据层保持不变。
3. LSTM模型结构设计:hidden_size、层数与训练参数怎么定
3.1 为什么LSTM是价格预测的默认选择:门控机制与记忆长度
先回答一个问题:既然是预测时间序列,ARIMA、Prophet不也能做吗,为什么是LSTM?ARIMA是线性模型,它对价格序列里的非线性模式——突发拉升、恐慌抛售、横盘后的突破——几乎无能为力。Prophet是趋势分解思路,对节假日和周期性有效,但加密市场没有"周末效应",黄金又受宏观经济数据的脉冲影响,Prophet的节假日参数很难配置。
LSTM的核心是三个门:遗忘门、输入门、输出门。它靠遗忘门决定丢弃多少旧信息,输入门决定记住多少新信息。这意味着它在长期依赖和短期波动之间有一个可学习的平衡——这正好是黄金和比特币价格序列最需要的。黄金的慢变量趋势(实际利率、美元指数)可以被遗忘门保留为长期记忆,比特币的突发波动则通过输入门快速吸收。用同一个LSTM处理这两类信号,等于让模型自己学两个品种的"记忆节奏"。
import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size=2, hidden_size=64, num_layers=2, seq_len=15, output_size=2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) 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_size=2是因为输入特征是比特币收盘价和黄金收盘价两列;output_size=2是同时预测两个品种的下一天收盘价。这里有一个架构选型的细节——两个品种是共享同一个LSTM层还是各自独立LSTM?共享层的优势是参数少、能学习跨品种的联动特征(比如"比特币暴跌时黄金是否避险上涨"这类关系),劣势是如果两个品种的数据分布差异太大,共享层可能互相干扰。我实际跑过的经验是:日线级别、输入特征只有收盘价时,共享层效果更好;如果你加入成交量、资金费率等异质特征,就得分品种建独立LSTM再在决策层融合。
3.2 训练循环:从loss曲线判断模型有没有在认真学习
训练循环看起来简单,但有几个细节直接影响收敛质量。第一是损失函数:价格预测本质是回归问题,用MSELoss。第二是优化器:Adam是默认选择,但学习率要小心,我一般从0.001开始,如果loss震荡剧烈就降到0.0003。第三是早停:价格序列噪声很大,训练轮数过多必然过拟合,验证集loss开始上升时立即停止。
import torch.optim as optim model = PriceLSTM(input_size=2, hidden_size=64, num_layers=2, seq_len=15, output_size=2) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.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:i+64] batch_y = y_train_t[i:i+64] optimizer.zero_grad() pred = model(batch_x) loss = criterion(pred, batch_y) loss.backward() # 梯度裁剪:防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.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(f'epoch {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_size=64、num_layers=2是我在日线级别、两个品种联合预测时的默认起点。为什么不是128或256?因为你的训练数据量就那么多——比特币2015年之前的日线数据质量差,黄金的完整日线也就几十年的量,实际洗干净后可能只有几千条样本。hidden_size越大,参数量呈平方级增长,几千条样本撑不起百万级参数的网络。这是价格预测LSTM最常见的过拟合来源,不是模型不够强,而是模型太强了。
num_layers=2比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, threshold=0.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.5,MACD信号权重0.3,RSI信号权重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, low=30, high=70): if rsi_value < low: return 1 # 超卖,反弹预期 elif rsi_value > high: return -1 # 超买,回调预期 return 0 def composite_score(lstm_score, macd_score, rsi_score, w_lstm=0.5, w_macd=0.3, w_rsi=0.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, samples=20): 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(axis=0) std_pred = preds.std(axis=0) 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_size=128时的训练loss是0.1,验证loss是2.5
现象:训练集loss一路降到0.1左右,验证集loss在0.4附近震荡后开始上升,最终到2.5。模型对训练数据倒背如流,对新数据基本靠猜。
原因:日线级价格数据量本来就少(几千条),hidden_size和num_layers设置过大,参数量远超样本量,模型直接记住了训练集的噪声。另一个原因是训练轮数过多,early stopping的patience设太大。
解决:把hidden_size降到32或64,num_layers保持1到2层,dropout设到0.3,patience设为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%)从训练集里单独抽出来做压力测试样本,模型的表现才算真实可信。希望帮到你——这个方案的方向没错,但验证的严谨程度,决定了它是研究玩具还是生产工具。
本文还有配套的精品资源,点击获取