基于LSTM的比特币与黄金双资产综合决策模型
简介一套基于LSTM预测比特币和黄金价格走势的综合决策模型项目源码、数据与项目说明齐备。项目出自大三课程作业经导师指导评审获得99分代码完整可运行面对计算机相关专业毕设、课程设计和期末大作业场景均适用也适合想要上手金融时间序列预测的Python学习者。压缩包共97个文件涵盖42个CSV数据文件与38个Python脚本另有XML配置、TXT说明、PDF报告及Markdown笔记等整体仅1.39MB。Python脚本覆盖数据预处理、MACD/RSI/乖离率与灰色预测等指标计算、LSTM模型训练、价格走势预测、买卖时机回测以及多维度可视化绘图CSV数据包含比特币和黄金原始行情以及资产值变化、相对误差、评价权重等中间结果便于完整复现决策流程。目前已有303人浏览学习。配套PDF项目报告和文档有助于理解实验设计思路可作为课程设计或大作业的完整参考。1. 为什么把比特币和黄金放进同一个 LSTM 模型比特币的日收益波动率常年是黄金的 8 到 10 倍两者在市场避险情绪升温时又经常同向联动。单资产 LSTM 会把这种联动当成噪声去拟合等震荡行情一来模型输出的置信度与实际胜率就脱节了。标题里这个项目的核心不是「谁预测得更准」而是用同一套 LSTM 骨架读两种资产在输出端合成一个综合决策信号源码、数据和项目说明只是工程外壳真正难的是样本构造、数据防泄漏和决策规则设计。适合两类人刚从图像或分类任务转来做时间序列的人需要先扭转「随机打乱数据集」的直觉已经在写技术指标或规则策略的工程师想看看 LSTM 原文里的门机制落到双资产场景时哪些环节必须自己补。记住一个反直觉结论预测准确率最高并不等于决策最好收益来自两个输出头之间的联动惩罚。2. 数据准备时间对齐、缺失值处理与按时间的三段划分多资产 LSTM 的第一个坑通常不在模型在数据。比特币 24 小时交易黄金期货只在芝商所CME交易周末和节假日休市。直接下载两条序列索引长度和日期分布完全不一样必须取两个交易日历的交集再按日期做 inner join 成一张宽表统一时区后排序。常见做法是用 yfinance 一次性拉取 BTC-USD 和 GCFimport yfinance as yf import pandas as pd btc yf.download(BTC-USD, start2018-01-01, end2024-12-31, interval1d, auto_adjustTrue) gold yf.download(GCF, start2018-01-01, end2024-12-31, interval1d, auto_adjustTrue) btc btc[[Open, High, Low, Close, Volume]].rename(columnslambda c: fbtc_{c.lower()}) gold gold[[Open, High, Low, Close, Volume]].rename(columnslambda c: fgold_{c.lower()}) df btc.join(gold, howinner) df.index pd.to_datetime(df.index).tz_localize(None) df df.sort_index() print(df.shape, df.isna().sum().sum())auto_adjustTrue会自动处理拆股和分红造成的前复权价格。黄金期货没有股息影响但主力连续合约GCF做过展期拼接相邻两天的跳价里含着展期成本特征工程时不能把它当作纯市场价格波动。比特币这边要额外检查分叉资产在历史价位上的连续性。这类数据不一致的原因通常有三类不同数据源对 OHLC 的定义不同、时区基准不同UTC 还是纽约时间、成交量单位不同BTC 是币数黄金是合约数。清洗后把连续日期索引补全缺的量价直接丢弃不要靠 LSTM 的遗忘门去「脑补」缺失值。数据落到data/raw/后立刻打一个带日期标记的压缩快照这是整个项目里成本最低的一次数据备份与恢复投入。原始 CSV 别用 Excel 双击打开再另存Excel 会把长数字改成科学计数法、把时间列悄悄转成本地时区这类「文件看着对、模型跑着乱」的问题最难查。这类体量的数据也没必要接 MySQL 管理CSV 加 Parquet 足够列名顺序和日期格式固定下来就行。2.1 构建 LSTM 的三维输入seq_len 与 feature_dimLSTM 的输入是(batch_size, seq_len, feature_dim)的三维张量。seq_len是回溯窗口长度feature_dim是每个时间步上同时喂进去的特征数量。把两个资产的特征按列堆叠假设每天有 6 列两个资产的 close 和 volume 变换feature_dim6窗口 60 天一个样本的形状就是(60, 6)。这里解释一个高频疑问LSTM 遗忘门的输入到底是什么数据。读 LSTM 原文会发现门控机制读的是「上一时刻隐藏状态 h_{t-1} 与当前时刻输入 x_t 拼接后的向量」而不是某列特征单独进门。PyTorch 的nn.LSTM把 batch 放在第一维源码内部会转成(seq_len, batch, input_size)逐步展开。理解这一点对排查 shape 报错很有帮助。2.2 数据集划分不能随机 shuffle也不能按整年切训练、验证、测试三段必须按时间顺序切7:1.5:1.5测试集放在最后 20% 日期上。和 MNIST、yolov8 训练自己的数据集那种随机抽样完全不同时间序列里第 t 天和第 t1 天的窗口共享 59 天数据一旦 shuffle训练集里就混入验证集时间段的信息这是最隐蔽的时间泄漏。KITTI 这类视觉数据集相邻帧也高度相关但金融日线的相关性衰减更慢影响也更大。train_end int(len(df) * 0.70) val_end int(len(df) * 0.85) df_train df.iloc[:train_end] df_val df.iloc[train_end:val_end] df_test df.iloc[val_end:]切完后把三个日期边界写进项目说明。后面调模型就会发现导致结果无法复现的往往不是权重初始化而是某次手滑对全量数据重新算了归一化参数。下载下来的开源项目凡是「源码数据项目说明」三件套齐全的跑不通的原因十有八九是路径写死或数据版本不一致而不是模型代码本身。3. 特征与序列构造窗口长度、多资产堆叠与归一化边界特征设计这一步别急着堆技术指标。市面上流传的「量能饱和度」「分时点火」这类指标公式源码本质是把量价行为编码成规则信号LSTM 的强项恰恰是从原始量价里直接学这类规则人工编码的指标未必比遗忘门自己组合出的特征更优。第一版建议只保留裸量价两个资产的 log 收益、log 成交量变化、20 日滚动波动率共 6 到 8 个特征列。3.1 窗口长度怎么选自相关图加对照实验窗口长度用经验值不如用数据说话。先算收益序列的自相关看相关性衰减到接近 0 需要多少天再把窗口按 30、60、90 三档做对比实验。日线数据上 60 天是合理起点覆盖一个季度且不引入过多噪声250 天年窗口会大幅削减样本量LSTM 在数据不够时反而学不出长期依赖。from statsmodels.graphics.tsaplots import plot_acf import numpy as np log_ret np.log(df[btc_close] / df[btc_close].shift(1)).dropna() plot_acf(log_ret, lags120)ACF 图前几根显著的 lag 代表短期动量缓慢衰减的部分提示需要更长上下文。最后以验证集 loss 为准不要迷信任何「经典窗口」。特征列顺序必须固定两个资产谁在前谁在后会影响feature_dim的排列但不会改变 LSTM 的拟合能力工程上建议按资产名排序避免换数据源时列错位。3.2 归一化只 fit 训练集边界锚定在训练期多资产时序预测最隐蔽的坑在这。用StandardScaler时必须只fit训练集再把同一组 mean 和 scale 应用到验证集和测试集并保存成独立文件。若直接对全量数据做fit_transform验证集和测试集的分布信息提前进入模型回测收益会系统性虚高这是典型的锚定缩放问题。import numpy as np from sklearn.preprocessing import StandardScaler feat_cols [c for c in df.columns if c.endswith((_close, _volume))] scaler StandardScaler() scaler.fit(df_train[feat_cols]) np.save(data/processed/train_mean.npy, scaler.mean_) np.save(data/processed/train_scale.npy, scaler.scale_) def make_windows(df_, seq_len60): X, y_btc, y_gold [], [], [] feats (df_[feat_cols] - scaler.mean_) / scaler.scale_ for i in range(seq_len, len(df_)): X.append(feats.iloc[i - seq_len:i].values) # (seq_len, feat_dim) y_btc.append(df_[btc_close].iloc[i]) y_gold.append(df_[gold_close].iloc[i]) return np.array(X), np.array(y_btc), np.array(y_gold)feat_dim就是feat_cols的列数。预测目标应该是未来 k 日的 log 收益而不是当日收盘价本身——直接预测价格会让模型退化成「记住上一个价格」的复制器。如果想搜「lstm时间序列预测python」找参考多数开源实现也是这个套路堆特征列、切窗口、统一归一化。提示慎用自带未来函数的指标公式。通达信、同花顺里不少指标源码隐含前视引用比如REF(X, -1)这类写法放进训练集等于把未来价格搬进特征。加指标前逐行核对是否引用了未来 K 线。4. LSTM 模型实现与训练从单资产基线到双资产多头模型结构不过度设计。输入nn.LSTM后接一个全连接层再分出两个输出头分别预测比特币和黄金的 log 收益。这里有个反直觉点不要用双向 LSTM。预测未来序列时bidirectional 每个时间步都会看到「未来的隐藏状态」训练时验证集 loss 非常漂亮实盘一跑就崩这是时序模型坚持单向 LSTM 的根本原因。4.1 模型结构与损失选择import torch import torch.nn as nn class MultiAssetLSTM(nn.Module): def __init__(self, feat_dim, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM(feat_dim, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.head_btc nn.Linear(hidden_size, 1) self.head_gold nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden) last out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.head_btc(last).squeeze(-1), self.head_gold(last).squeeze(-1)损失用 HuberLossdelta1.0而不是 MSE。比特币日 log 收益偶尔出现单日 20% 以上的极端值MSE 会把梯度焦点全压在极端样本上Huber 残差超过 delta 后退化为 L1训练更稳定。两个输出头的损失直接相加权重默认相等如果发现某一资产拟合明显偏弱可以给损失加权但第一版不建议调这个比例。为什么共用 LSTM 底层而不是训练两个独立模型共享隐藏层不算玄学黄金和比特币虽然波动率差一个数量级但驱动它们的中期因子高度重叠共享 backbone 能让底层学到更稳定的状态表示两个输出头各自保留资产的收益尺度。这是多任务学习里最轻量的结构参数增加很少防过拟合效果优于两个独立 LSTM。4.2 训练循环、早停与学习率衰减def train_one_epoch(model, loader, opt, device): model.train() total_loss, n 0.0, 0 for xb, yb, yg in loader: xb, yb, yg xb.to(device), yb.to(device), yg.to(device) opt.zero_grad() pred_b, pred_g model(xb) loss nn.functional.huber_loss(pred_b, yb) \ nn.functional.huber_loss(pred_g, yg) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() total_loss loss.item() * len(xb) n len(xb) return total_loss / n scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(opt, modemin, factor0.5, patience5) patient, best_val 0, float(inf) for epoch in range(200): train_loss train_one_epoch(model, train_loader, opt, device) val_loss evaluate(model, val_loader, device) scheduler.step(val_loss) if val_loss best_val: best_val val_loss torch.save(model.state_dict(), best_model.pt) patient 0 else: patient 1 if patient 10: break训练前固定随机种子训练中每次验证后都要重置 DataLoader 的 shuffle 顺序否则数据加载顺序变化会掩盖真实收敛效果。梯度裁剪设 1.0 是防特征列出现 inf 时权重直接飞到 NaN价格序列做 log-diff 后梯度一般不爆炸但脏数据防不胜防。4.3 关键超参数表与失败观察参数取值说明seq_len60日线看一个季度样本不足时降到 30hidden_size64特征维度小于 15 时 64 已够再大容易过拟合num_layers1 到 2数据量不足 10 万样本时两层是上限dropout0.2仅对多层 LSTM 层间隐藏状态生效batch_size64按显存调整验证集不需要 drop_lastlr1e-3第一个 epoch loss 跳高就降到 5e-4验证 loss 出现锯齿状震荡时先检查特征里是否混入 NaN。很多脚本用fillna(0)处理缺失量价缺几天就填 0LSTM 会把 0 当成真实价格水平学进去。这类问题在 Excel 里表现为「此值与此单元格定义的数据验证限制不匹配」但 CSV 会静默吞掉模型却在 loss 曲线里诚实反映出来。5. 综合决策模型从预测值到可执行的仓位信号综合决策不是把两个资产的预测分数简单相加。常见做法是先用 LSTM 输出未来 5 日 log 收益预测再用阈值映射成三档信号多、空、观望最后按两个信号的组合给出仓位建议。预测次日收益噪声太大模型几乎等于随机5 日窗口平滑噪声也更贴近实际调仓频率。5.1 预测目标与信号转换threshold_btc 0.005 threshold_gold 0.003 def to_signal(pred, th): if pred th: return 1 elif pred -th: return -1 return 0 sig_btc to_signal(pred_btc, threshold_btc) sig_gold to_signal(pred_gold, threshold_gold)阈值是综合决策模型里最敏感的参数比特币和黄金波动率量级不同同一阈值对两者并不公平。建议在验证集上各取其收益标准差的一定倍数比如 0.2 倍而不是拍脑袋定值。网格搜索阈值时评估指标用「决策后组合收益」而不是预测准确率——这直接决定了后续仓位质量。5.2 双资产联动与决策矩阵基于两个信号各取 -1、0、1共 9 种组合工程上可以合并成 5 档动作。这张决策矩阵是策略层的核心输出比特币信号黄金信号建议动作11风险与避险资产同涨半仓做多比特币1-1风险偏好明确满仓做多比特币-11避险模式仅持有黄金或清空比特币-1-1系统性下跌清仓观望00无明确方向维持最低仓位这组规则本身就是「综合决策」的落点。比特币量化社区里常见的误用是对两个资产分别建策略、再按资金比例叠加这样会漏掉两个信号同向时的系统性风险。决策矩阵的价值是把联动显式编码同号时降杠杆异号时才敢加仓。更进阶的做法是计算两资产滚动 20 日收益率相关性相关性突变超过阈值时把整体仓位惩罚到 60%这一步不需要重新训练模型。5.3 滚动再训练别把测试集一次性用完固定划分的测试集只能验证一次反复看测试集调参会变成隐式训练。常见做法是 walk-forward 滚动验证从训练集末端出发每次往前推 20 个交易日重新训练模型再预测下一个 20 天窗口滚动前进直到覆盖全部测试期。这样每个预测都严格使用当时可得的数据和实盘流程一致。代码实现不复杂外层套一个按日期滑动的循环内部复用第 4 章的train_one_epoch。滚动验证的结果会比单次测试更接近真实表现综合决策模型的价值也只有在滚动测试里才能看出稳定性。6. 回测与排错三个让结果可复现的小技巧回测是最后一道闸门。常见做法是写一个逐 bar 的循环按时间顺序推进每个时点只用当时已训练好的模型做预测前视窗口、未来函数、锚定缩放三个问题任何一个存在回测曲线都会漂亮得不真实。下面的三个技巧是我在跑这类项目时一定会固化的动作。6.1 先凝固数据版本再谈模型复现把训练、验证、测试的日期边界、归一化参数、特征列顺序、窗口长度全部写进一份config.yaml每次跑实验前重新加载而不是重新计算。至少包含seq_len、threshold、corr_window、scaler_anchor_date四个字段。原始 CSV 一旦被后续脚本覆盖再强的模型也无法复现数据快照和配置文件的优先级高于模型权重。6.2 反向时间对照验证是否真学到时序依赖把测试集的日期顺序倒过来再用同一模型做预测。如果倒序后的误差和正序差不多说明模型只是在记忆价格水平没有学到序列依赖这时候先别上仓位。这个检查跑一遍只需十分钟能筛掉一半无效模型。类似地把训练集和验证集的时间边界互换做一次对照能暴露归一化参数泄漏。6.3 用决策日志定位收益偏差每次回测保存当天两个输出头的预测值、阈值、最终仓位落一份decision_log.csv。日后复盘或做绩效归因时这份日志比模型权重更有说服力。实际排查会发现90% 的收益偏差来自信号阈值变动、归一化参数没加载、日期边界切错而不是模型结构本身。复现结果的第一步不是重训模型而是先恢复数据快照和决策配置。本文还有配套的精品资源点击获取