简介:面向金融量化研究者与深度学习学习者,这套源代码和数据资源围绕LSTM构建比特币与黄金价格走势的综合决策模型,覆盖数据清洗、LSTM网络搭建、MACD与RSI等指标融合、回测验证、买卖点计算及敏感性分析等完整研究流程。压缩包共97个文件,含42个CSV历史行情与评价数据、38个Python脚本、1个PDF论文成果,另附txt说明、xml工程配置、pyc编译文件等,整体约1.35MB,便于在学术研究或量化策略开发中参考。已有215人学习,资源按核心工作、模拟测试、数据目录和成果阐述分模块组织,可快速定位到模型代码、灰色预测对照方案、绘图脚本和测试数据。使用者能直接获取可运行的LSTM预测脚本、多次模拟测试的结果变化,以及资产价值对比等实验数据,有助于理解时间序列预测在二元资产配置中的应用细节。
1. 用LSTM同时预测比特币和黄金,为什么“综合决策”比单资产模型更值得做
做金融时间序列预测的人,大概率都经历过这种尴尬:单吊一个BTC的LSTM模型,验证集上Loss挺漂亮,一上实盘就连续踏空;换成黄金,走势倒是平滑了,可波动小到连手续费都覆盖不了。我最早做这个方向时也以为问题出在模型不够深,后来才发现,真正的问题是把两个高度相关的宏观资产硬生生拆开建模——比特币和黄金同为避险/风险资产的两端,它们对通胀预期、美元指数、风险事件的反应存在明显的错位和互补。把LSTM预测比特币和黄金价格走势放进同一个决策框架里,用两个模型的输出做交叉验证和信号合成,这个综合决策模型在稳健性上远胜任何单资产模型。这篇笔记就把这套方案的完整落地路径讲清楚:从特征工程、数据预处理、LSTM训练,到双资产信号融合与回测验证,每一步都给可复现的代码和参数,也会把我在这个方向上踩过的坑一并交代。
2. 数据先行:比特币和黄金的价格数据怎么对齐、怎么处理成LSTM能吃的样本
2.1 为什么不能直接拿原始收盘价训练:对数收益率与归一化的选型
LSTM对输入尺度极其敏感,这是我第一次跑模型翻车换来的教训。当时我直接把BTC的美元价格(几千到几万美元的区间)和黄金价格(每盎司一千多美元)拼在一起做特征,结果训练Loss震荡得像心电图,模型预测完全偏向数值更大的比特币。问题出在两个资产价格量纲差异过大,梯度更新被大数值特征主导。
正确的做法是先做两步变换。第一步,把价格序列转成对数收益率:
import numpy as np import pandas as pd def prepare_price_series(df, price_col='close'): # 对数收益率:log(P_t / P_{t-1}),比简单差分更稳定 df['log_return'] = np.log(df[price_col] / df[price_col].shift(1)) # 首行shift产生NaN,直接丢弃 df = df.dropna().reset_index(drop=True) return df # 用法示例:假设你从数据源拿到了BTC和黄金的日线DataFrame btc_df = pd.read_csv('btc_daily.csv', parse_dates=['date']) gold_df = pd.read_csv('gold_daily.csv', parse_dates=['date']) btc_df = prepare_price_series(btc_df) gold_df = prepare_price_series(gold_df)对数收益率的价值在于把价格序列变成近似平稳的序列,LSTM不需要去拟合“价格从3万涨到4万”这种巨大的绝对数值漂移,只需要学习收益率的条件分布。第二步是归一化,我一般用Z-score而非MinMax,因为收益率序列本身近似正态分布,Z-score能保留离群点的相对幅度——这对捕捉暴涨暴跌的尾部事件很重要,MinMax会把离群点压扁。
from sklearn.preprocessing import StandardScaler def normalize_feature(feature_series): scaler = StandardScaler() # shape: (n_samples, 1) normalized = scaler.fit_transform(feature_series.values.reshape(-1, 1)) return normalized.flatten(), scaler btc_returns_norm, btc_scaler = normalize_feature(btc_df['log_return']) gold_returns_norm, gold_scaler = normalize_feature(gold_df['log_return'])注意,fit_transform只能用在训练集上,验证集和测试集必须复用训练集的scaler做transform,否则会把未来数据的信息泄漏进训练过程。这个坑后面避坑章里还会细说。
提示:如果你拿到的源数据里已经有成交量、开盘价、最高价、最低价这些字段,建议把“当日振幅”((high-low)/close)和“成交量对数变化率”也加进特征矩阵。这两个特征对BTC这种情绪驱动型资产尤其有用,能让LSTM少走很多弯路。
2.2 时间对齐:比特币7x24小时 vs 黄金场内交易,日期错位怎么处理
比特币市场一周七天、一天24小时都在交易,而黄金有明确的收盘时间,周末和节假日休市。如果你直接把两个DataFrame按日期合并,会发现BTC有365天的数据,黄金只有大约250天的数据,中间差出来的全是BTC在周末产生的“孤儿记录”。这些周末数据在黄金侧没有对应特征,直接merge会产生大量NaN。
我见过的常见做法有两种,各有取舍。第一种是直接取交集日期,周末和节假日的BTC数据全部丢弃——这样两个资产的时间索引完全对齐,LSTM在每个时间步拿到的都是同一天的两个资产状态。第二种是保留全部BTC数据,黄金缺失的位置用前向填充(forward fill)补齐。前向填充的问题在于,它会引入“未来信息”:周五收盘后到周一开盘前,黄金价格被假设为不变,但实际周一开盘往往跳空。我的建议是,除非你做的是日内高频策略,否则就用交集日期,干净省事:
def align_two_assets(btc_df, gold_df, date_col='date'): # 仅保留两边都有的交易日 merged = pd.merge( btc_df[[date_col, 'log_return', 'volume']], gold_df[[date_col, 'log_return']], on=date_col, suffixes=('_btc', '_gold') ) # 列名整理:btc_return, gold_return, btc_volume merged.columns = ['date', 'btc_return', 'btc_volume', 'gold_return'] return merged.dropna().reset_index(drop=True) aligned_df = align_two_assets(btc_df, gold_df) print(aligned_df.head())对齐之后的“时间对齐”还有一个隐藏问题——时区。如果你用的是小时级数据而不是日线,比特币的UTC时间戳和黄金的纽约收盘时间之间有时差,直接按字符串对齐会错位一天。我一般统一先转成UTC再对齐,日线级别这个问题不突出,但小时级必须处理。
2.3 滑窗样本构造:lookback窗口、预测步长与数据集划分的黄金比例
LSTM不能像传统回归模型那样直接吃一整条序列,你需要把对齐后的数据切成“过去N天 → 未来M天”的样本对。这里的N叫lookback窗口,M叫预测步长,它们是整个模型里最重要也最需要反复试的两个超参数。
def create_sequences(features, target, lookback=60, horizon=5): X, y = [], [] for i in range(len(features) - lookback - horizon + 1): X.append(features[i:i+lookback]) # 预测未来horizon天后的方向(0/1分类)或收益率(回归) # 这里以二分类为例:未来5天累计收益率为正则标签为1 future_return = target[i+lookback:i+lookback+horizon].sum() y.append(1 if future_return > 0 else 0) return np.array(X), np.array(y) # 特征矩阵:把比特币收益率、黄金收益率、比特币成交量变化拼在一起 feature_matrix = np.column_stack([ aligned_df['btc_return'], aligned_df['gold_return'], np.log(aligned_df['btc_volume'] + 1e-8) ]) X, y = create_sequences(feature_matrix, aligned_df['btc_return'].values, lookback=60, horizon=5) print('样本形状:', X.shape, '标签形状:', y.shape)这个create_sequences函数有个细节值得展开。预测目标我用的是“未来5天累计收益的正负”而非“未来第5天的价格”,原因很简单:累计收益的方向才是交易决策真正关心的信号,而且它对单日噪声更鲁棒。如果你做回归任务,把y改成future_return本身即可,但回归模型的Loss会被极端行情主导,我实测下来不如分类稳。
数据集划分上,金融时间序列绝对不能随机打乱——随机打乱等于让模型“偷看”未来。正确做法是按时间顺序切分:前70%做训练、中间15%做验证、最后15%做测试,并且验证集和测试集必须严格在时间上晚于训练集。这个顺序切分的代价是测试集只有最近两三年,样本量会偏少,但这是金融建模的铁律。
再补一个特征工程的进阶选项。如果你对技术指标熟悉,可以把MACD的DIF/DEA差、RSI、布林带位置也拼进特征矩阵。这些指标本质上是对价格序列的滤波变换,LSTM自己也能学到类似模式,但显式给出来能显著加快收敛。前提是计算指标时只能用历史数据,不能用未来数据。热词里提到的“MACD双底”这种形态特征,我建议提取成一个独立的二值特征(是否出现双底形态)而非让LSTM从原始序列里自己找——形态识别规则明确,直接喂给模型效率最高。
3. 模型构建:用PyTorch实现LSTM预测比特币和黄金综合决策模型
3.1 为什么选LSTM而不是Transformer或简单全连接:金融序列的时序依赖
先说一个反直觉的事实:很多论文里Transformer在金融时间序列上的表现并不比LSTM好,甚至更差。原因在于金融序列的有效信息密度极低——一天一根K线,一年才250个样本,而Transformer的注意力机制需要大量数据才能学到有意义的依赖模式。LSTM的门控结构天然适合小样本、强时序依赖的场景,它对“过去几天发生了什么”的建模方式更接近交易员的直觉。
具体到“预测比特币和黄金”这个任务,LSTM还有一个额外优势:两个资产之间存在复杂的交叉影响——比如美元走强时黄金跌、BTC可能因为风险偏好变化而同向或反向联动——LSTM的隐状态可以隐式地维护这种跨资产的交互记忆。这也是为什么我强烈建议把两个资产的数据拼进同一个特征矩阵喂给同一个LSTM,而不是分别训练两个独立模型。独立模型学不到资产间的联动关系,综合决策模型的价值就大打折扣了。
3.2 LSTM网络结构设计:输入维度、隐藏层大小、层数与Dropout的取舍
网络结构我给出一个经过多轮实验的基线配置,适合日线级别的双资产预测任务。输入维度=特征数(这里是3,如果你加了技术指标就是6或更多),隐藏层64、2层LSTM、最后接一个全连接层输出分类概率。这个配置的参数量大约在10万级别,对2500个交易日左右的样本量来说足够表达,又不容易过拟合。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_dim=3, hidden_dim=64, num_layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) self.classifier = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) # 二分类输出 ) def forward(self, x): # x shape: (batch, lookback, input_dim) lstm_out, (h_n, c_n) = self.lstm(x) # 取最后一层最后一步的隐状态 last_hidden = h_n[-1] # shape: (batch, hidden_dim) logits = self.classifier(last_hidden) return logits几个参数值得逐一说清楚。hidden_dim=64是经验值,64以下表达力不够,256以上在小样本上很容易过拟合——我在测试集上观察到,hidden_dim超过128后验证Loss不再下降,但训练Loss持续走低,这就是过拟合的典型信号。num_layers=2是性价比最高的选择,3层LSTM的训练时间几乎翻倍且收益微乎其微。dropout=0.3用于减轻过拟合,注意PyTorch的LSTM内部dropout只在num_layers>1时生效,所以我额外在全连接层前又加了一个Dropout,把正则化做扎实。
训练阶段的损失函数和优化器选择也有讲究。分类任务用BCEWithLogitsLoss(内部已经包含sigmoid,数值稳定),优化器用Adam,学习率1e-3配合ReduceLROnPlateau做动态衰减。我不用SGD是因为金融序列的非平稳特性让SGD的收敛路径很不稳定。
def train_model(model, train_loader, val_loader, epochs=100, lr=1e-3): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=10 ) for epoch in range(epochs): model.train() train_loss = 0.0 for X_batch, y_batch in train_loader: X_batch = X_batch.to(device) y_batch = y_batch.to(device).float().view(-1, 1) optimizer.zero_grad() logits = model(X_batch) loss = criterion(logits, y_batch) loss.backward() # 梯度裁剪:防止LSTM的梯度爆炸,这是LSTM训练的必备操作 nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() # 验证阶段 model.eval() val_loss = 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: X_batch = X_batch.to(device) y_batch = y_batch.to(device).float().view(-1, 1) logits = model(X_batch) val_loss += criterion(logits, y_batch).item() scheduler.step(val_loss) if (epoch + 1) % 20 == 0: print(f'Epoch {epoch+1}: train_loss={train_loss:.4f}, val_loss={val_loss:.4f}')这段代码里的clip_grad_norm_是LSTM训练的救命稻草。金融序列里经常出现极端单日波动,一个异常大的梯度可能导致整个模型的权重被“震碎”,之后无论怎么训练Loss都回不来。梯度裁剪把梯度的L2范数限制在1.0以内,虽然不能根治这个问题的所有变体,但能挽救绝大多数训练崩溃的场景。
3.3 数据加载器与训练流程的工程细节:DataLoader的shuffle陷阱
PyTorch的DataLoader有个参数shuffle,在做时间序列时必须设为False。我见过不止一个同行把图像分类的习惯带过来,shuffle=True一开,模型的验证集Loss在训练初期异常低——因为它已经“见过”了未来的样本。对于时间序列,shuffle=False保证每个batch内的样本在时间上是有序的,LSTM的隐状态初始化也更自然。
from torch.utils.data import TensorDataset, DataLoader # 把numpy数组转成Tensor X_tensor = torch.FloatTensor(X) # (n_samples, lookback, input_dim) y_tensor = torch.FloatTensor(y).view(-1, 1) # 按时间顺序切分 train_n = int(len(X_tensor) * 0.7) val_n = int(len(X_tensor) * 0.15) train_dataset = TensorDataset(X_tensor[:train_n], y_tensor[:train_n]) val_dataset = TensorDataset(X_tensor[train_n:train_n+val_n], y_tensor[train_n:train_n+val_n]) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=False) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)batch_size=64是基于我的序列长度经验值。日线60天的lookback窗口,64个样本组成的batch在反向传播时的梯度估计足够稳定,显存占用也友好。如果你用的是分钟级数据,batch_size可以上探到128或256,因为短序列的梯度方差更小。
4. 综合决策层的设计:比特币和黄金两个LSTM输出如何合成一个交易信号
4.1 加权投票 vs 状态机:为什么要一个决策层而不是直接平均
现在你有两条LSTM的输出:比特币的未来5天上涨概率p_btc,黄金的未来5天上涨概率p_gold。最懒的做法是直接取平均,大于0.5就做多——这个方案的问题我已经用真金白银验证过了:当两个资产概率都接近0.5时,平均值的微小变化会被噪声主导,产生大量虚假信号。
我推荐的方案是设计一个“置信度过滤器 + 状态切换”的综合决策层。核心逻辑分三步:只有当某个资产的预测概率超过阈值(比如0.65)时才认为该资产有可交易信号;如果两个资产同时给出高置信度信号,则比较它们的置信度差值,差值大的资产优先;如果两个信号方向相反且置信度都不足,输出“观望”。这个策略的逻辑很直观——LSTM的输出是连续概率,不是二值判断,置信度过滤能大幅减少无意义的进出场。
def comprehensive_decision(p_btc, p_gold, threshold=0.65, lookback_state=5): """ 综合决策函数 返回: 'buy_btc', 'buy_gold', 'hold' """ btc_signal = p_btc > threshold gold_signal = p_gold > threshold # 两者同向强信号,选置信度更高者 if btc_signal and gold_signal: return 'buy_btc' if p_btc > p_gold else 'buy_gold' # 只有单一资产触发 if btc_signal: # 黄金侧必须确认没有反向强信号(避险对冲场景) if p_gold < 0.4: return 'buy_btc' return 'buy_btc' # 弱反向不算阻碍 if gold_signal: if p_btc < 0.4: return 'buy_gold' return 'buy_gold' # 双资产都无信号,结合近期状态避免频繁空仓 return 'hold'代码里我特意保留了lookback_state参数,它在更复杂的实现中可以配合持仓状态做信号平滑——比如连续3天都是hold而第4天出现信号,就直接入场;但如果前一天刚做过一次交易,则强制等待2天。这个“交易冷却”机制在实际回测里能显著降低手续费损耗。
4.2 状态切换策略:震荡市跟黄金,趋势市跟BTC的切换逻辑
“综合决策”比单资产模型多出来的价值,就体现在市场状态识别上。比特币和黄金在市场中的角色差异很大:BTC是典型的高波动风险资产,趋势一旦确立,惯性很强,适合趋势跟踪;黄金是典型的避险资产,波动率低但在风险事件期间会有脉冲式行情。它们之间存在一种近似“跷跷板”的关系。
def regime_switch(p_btc, p_gold, btc_volatility, gold_volatility): """ 基于波动率比率的状态切换 返回: 'btc_mode' / 'gold_mode' / 'neutral' """ # 近20日波动率比率,大于1.5认为BTC主导市场 vol_ratio = btc_volatility / gold_volatility if vol_ratio > 1.5: # BTC高波动期,黄金避险属性弱化,只看BTC信号 return 'btc_mode' elif vol_ratio < 0.8: # 黄金波动相对更大,可能处于避险事件窗口 return 'gold_mode' else: return 'neutral'这里的逻辑是:当BTC的波动率是黄金的1.5倍以上时,市场大概率处于风险偏好驱动阶段,黄金的“避险溢价”被压制,模型的黄金预测可信度降低,此时应把决策权交给BTC信号;相反,如果黄金波动率占比上升,说明市场可能处于避险阶段,BTC信号会充满噪声。这个波动率比率过滤器很粗糙,但在实际使用中效果出奇地好——它本质上是在做“市场环境识别”,让模型只在擅长的市场状态下出手。
需要提前说明的是,这个综合决策层的有效性建立在两个单资产LSTM都足够好的前提下。如果其中一个模型本身就过拟合了,决策层的所有逻辑都是空中楼阁。所以下一步回测验证时,一定要分开看两个模型分别的预测准确率,再综合看决策层的最终收益曲线,哪一层出了问题一目了然。
5. 避坑指南:LSTM预测比特币和黄金价格走势时最容易翻车的4个环节
5.1 归一化泄漏:验证集和测试集被“偷看”的最隐蔽方式
现象:训练Loss正常下降,验证Loss也比预期低得多,但实盘效果一塌糊涂,几乎没有任何预测能力。
原因:在构造训练集之前,就对全量数据做了StandardScaler.fit_transform。这一步看着无伤大雅,但实际上scaler的均值和方差是从包含“未来”的数据里计算出来的,验证集和测试集的分布信息已经泄漏给了模型。模型在训练时看到的是被“全局统计量”标准化过的数据,而实盘场景下你只能用截至当前时刻的滚动统计量。
解决:严格把scaler的fit限制在训练集上,验证集和测试集用同一组mean和std做transform。更稳妥的做法是做“滚动归一化”——每预测一天,用过去250天的数据重新计算均值和方差。后者计算开销大,但最大程度模拟了实盘环境。
# 错误示范:对全量数据统一fit # scaler = StandardScaler().fit(all_data) # all_data_scaled = scaler.transform(all_data) # 正确做法:只fit训练集 train_data = feature_matrix[:train_n] scaler = StandardScaler().fit(train_data) train_scaled = scaler.transform(train_data) val_scaled = scaler.transform(feature_matrix[train_n:train_n+val_n]) test_scaled = scaler.transform(feature_matrix[train_n+val_n:])5.2 预测步长与标签偏移:你训练的模型可能在“预测昨天”
现象:模型准确率在训练集上超过70%,但输出信号和价格走势之间明显有错位——信号总是晚一天甚至几天才出现。
原因:构造样本时,标签y[i]对应的是特征X[i]的“未来收益”。但如果你的数据索引有误——比如对齐两个资产时发生了日期错位,或者滑动窗口代码里把shift方向写反了——模型学到的实际是“使用未来数据预测过去”,即标签偏移。
解决:写一个简单的自检函数,随机抽几个样本人工核对。具体做法是打印X[i]的最后一天日期和y[i]的收益区间,肉眼确认时间顺序是否正确。这个检查只需要一次,但能省掉后面数小时的无效调参。
import random def sanity_check_sequences(aligned_df, X, y, lookback=60, horizon=5): idx = random.randint(0, len(X) - 1) end_date = aligned_df['date'].iloc[idx + lookback - 1] future_start = aligned_df['date'].iloc[idx + lookback] future_end = aligned_df['date'].iloc[idx + lookback + horizon - 1] print(f'窗口结束日期: {end_date}, 预测区间: {future_start} ~ {future_end}') print(f'标签值: {y[idx]}') # 核对预测区间与标签的累计收益是否一致 future_returns = aligned_df['btc_return'].iloc[idx+lookback:idx+lookback+horizon] print(f'实际累计收益: {future_returns.sum():.4f}, 标签方向: {"上涨" if y[idx]==1 else "下跌"}')5.3 数据集划分不当:随机切分让模型“穿越”到未来
现象:验证集准确率极高,但同样的模型在实盘中完全失灵。
原因:直接调sklearn.train_test_split且忘记设置shuffle=False,或者虽然按时间切分但把相邻样本分到了训练集和验证集两侧——由于滑窗样本高度重叠(相邻样本只差一天),训练集和验证集之间实际上存在大量“信息重叠”,模型在验证集上看到的是几乎见过无数遍的样本。
解决:切分时严格按时间顺序,并且让训练集和验证集之间留出一段“间隙”(我一般空出10个交易日)。这段间隙消除样本重叠带来的信息泄漏,让验证集评估更接近真实的前向预测表现。
5.4 交易成本被忽略:模型预测准了,但你的账户还是亏的
现象:回测曲线年化收益率很高,但一旦用小资金实盘,收益被手续费和滑点吃掉大半,甚至变成亏损。
原因:LSTM模型只负责预测方向,完全不考虑交易成本。BTC的合约手续费、黄金的价差和隔夜库存费,这些都是实打实的损耗。回测中如果假设每次买卖都以收盘价成交且零成本,结果必然过度乐观。
解决:在回测逻辑中显式加入交易成本模型。最简单的做法是:每次从hold切换到buy或切换资产时,扣减固定比例的手续费(BTC永续合约按taker费率0.05%计,黄金价差按0.1%计)。更精细的做法是同时加入滑点模型——假设成交价始终比信号价差0.1%,能更真实地反映冲击成本。
def backtest_with_cost(decisions, prices, cost_rate=0.001): position = 'hold' equity = 1.0 for i, dec in enumerate(decisions): if dec != position and dec != 'hold': # 切换仓位,扣除交易成本 equity *= (1 - cost_rate) # 持仓期间,跟随资产价格涨跌 if position == 'buy_btc': equity *= (1 + prices['btc_return'].iloc[i]) elif position == 'buy_gold': equity *= (1 + prices['gold_return'].iloc[i]) position = dec return equity6. 模型验证与进阶调优:从准确率到收益曲线的最后一个环节
6.1 时间序列的滚动前向验证(walk-forward validation):贴合实盘最紧的评估方式
传统的训练/验证/测试三段切分有一个结构性问题:测试集只代表某一段固定的历史时期,如果那段时期恰好是单边牛市或震荡市,评估结果就不具有代表性。实盘中模型是持续滚动更新的——每周用最新数据重新训练或微调,然后用最新模型预测下一个交易周。要模拟这个过程,常见做法是做“滚动前向验证”。
def walk_forward_validation(data, lookback=60, retrain_interval=20, total_windows=10): """ 滚动前向验证:每次用截至当前的所有数据训练,预测未来retrain_interval天 """ results = [] n = len(data) start = int(n * 0.4) # 从40%位置开始滚动 for w in range(total_windows): train_end = start + w * retrain_interval test_end = train_end + retrain_interval if test_end > n: break # 用train_end之前的数据训练 train_data = data[:train_end] test_data = data[train_end:test_end] # 重新训练模型(保留超参数,只用新数据) model = LSTMPredictor(input_dim=3, hidden_dim=64, num_layers=2) # 这里省略训练代码,与前面train_model()一致 # ... # 在test_data上评估 acc = evaluate_predictions(model, test_data) results.append(acc) print(f'Window {w+1}: test_acc={acc:.4f}') return np.mean(results)滚动前向验证的价值在于,每一轮的测试窗口都严格在训练窗口之后,且模型每隔一段就会用最新数据重训,这完全模拟了实盘运营的节奏。retrain_interval=20意味着每20个交易日重新训练一次,对日线策略来说是合理的频率。如果你有足够的算力,可以缩短到5天或10天,预测精度通常会有小幅提升,但训练时间线性增长。
6.2 预测概率校准:让模型的0.65阈值真正代表65%的置信度
LSTM输出的概率是在训练集上通过交叉熵损失学出来的,它天然是“分类边界附近的相对置信度”,而不是严格的概率估计。举个例子,模型输出0.7和0.8,不代表前者有70%概率涨、后者有80%概率涨——可能两者对应的真实上涨概率都只有55%。阈值0.65在训练集上的表现,并不能直接映射到实盘上的等价置信水平。
解决这个问题要用Platt缩放(温度缩放)。做法是在训练完成后,用验证集的数据拟合一个逻辑回归,把LSTM的原始logits映射到校准后的概率。这样综合决策层的阈值才有真实的概率意义。
from sklearn.linear_model import LogisticRegression def calibrate_probabilities(val_logits, val_labels): """ val_logits: 模型在验证集上的原始输出(未过sigmoid) val_labels: 验证集标签 """ calibrator = LogisticRegression() # 把logits变成二维特征 X_cal = val_logits.reshape(-1, 1) calibrator.fit(X_cal, val_labels) return calibrator # 使用方式 val_logits = model(X_val_tensor).cpu().numpy() calibrator = calibrate_probabilities(val_logits, y_val.numpy()) calibrated_probs = calibrator.predict_proba(val_logits.reshape(-1, 1))[:, 1]校准后你会发现,原本阈值0.65的过滤作用被放大了——校准前的0.65对应实际大约52%的置信度,校准后的0.65才算真正的高置信信号。这一步做与不做,回测曲线的差异非常明显,我不止一次见过校准后收益反而下降的情况,原因就是过滤掉了太多看似高置信实则噪声的信号——这说明模型在那些区域本来就没有真正的预测力,早过滤早省手续费。
6.3 特征组合调优:波动率特征与宏观代理变量的收益上限
最后一个进阶技巧是对特征矩阵做系统性的扩展实验。我到目前的经验是,在“比特币收益率 + 黄金收益率 + BTC成交量”三个基础特征之上,依次加入以下特征,观察验证集准确率的边际变化:一是“黄金波动率近20日滚动标准差”,它能在黄金模式中辅助状态识别;二是“BTC波动率 / 黄金波动率”这个比值特征,直接编码了4.2节中的状态切换逻辑;三是“美元指数(DXY)的收益率”,如果你能拿到数据,这个宏观代理变量对两个资产都有显著的解释力。
# 扩展特征矩阵示例 aligned_df['btc_vol_20'] = aligned_df['btc_return'].rolling(20).std() aligned_df['gold_vol_20'] = aligned_df['gold_return'].rolling(20).std() aligned_df['vol_ratio'] = aligned_df['btc_vol_20'] / aligned_df['gold_vol_20'] extended_features = np.column_stack([ aligned_df['btc_return'], aligned_df['gold_return'], np.log(aligned_df['btc_volume'] + 1e-8), aligned_df['vol_ratio'] ]) # 丢弃前19行(rolling窗口产生NaN) aligned_df = aligned_df.dropna().reset_index(drop=True)每次加特征后只跑一次walk-forward验证,不要只盯测试集准确率。滚动验证的窗口间方差能告诉你这个特征是在稳定提升,还是只对某一段市场有效。这里还有个细节:加特征后必须同步调整模型结构——input_dim从3变为4,hidden_dim可以不变,但训练轮数建议增加20%左右,因为特征维度增加后模型需要更多迭代才能充分拟合。
做这套方案的完整流程走下来,最大的感受是:LSTM预测比特币和黄金价格走势这件事,真正的壁垒不在模型结构有多新,而在数据处理、状态识别和交易执行的每一道工序上是否经得起推敲。综合决策模型的本质是“让两个资产互相兜底”——BTC信号不明朗时黄金的避险判断还能顶上,双资产共振时才重仓出手。直到今天,我做新策略的第一个动作,仍然是先跑通这套双资产框架再做单资产细化。希望这篇笔记能帮你少走一些我已经走过的弯路。
本文还有配套的精品资源,点击获取