
简介这份资源是比特币基于LSTM的多因子交易策略Python实现面向计算机、人工智能、金融工程等方向的在校学生与量化交易初学者可用于课程设计、毕业设计或策略研究入门。项目通过LSTM对多因子行情数据进行建模预测在控制最大回撤与降低波动方面表现较为稳健同时保留了收益率与预测准确率强相关的特性便于读者进一步加入更多因子、调整超参数或尝试集成学习等改进方向。压缩包共12个文件约797KB包含4个CSV行情数据文件、2个Jupyter Notebook源码、3个XML与1个IML工程配置、1张效果图及1份README说明结构清晰开箱即可运行。目前已有263人学习下载。代码均经测试运行成功答辩评审平均分达96分读者可据此掌握数据预处理、因子构建、模型训练与回测评估的完整流程并在此基础上修改扩展用于毕设、课设或项目立项演示。1. 多因子 LSTM 交易策略为什么你的模型在回测里封神实盘却翻车很多人第一次接触「比特币基于 LSTM 的多因子交易策略」都是被一个漂亮到不真实的回测曲线吸引进来的年化几百个点最大回撤个位数夏普比率高得离谱。然后兴冲冲把模型接到实盘第一周就被市场教做人。问题几乎从来不在 LSTM 本身而在于「多因子」这三个字被当成了摆设——大多数人只喂了收盘价序列却号称自己在做多因子。这个方向真正要解决的是把链上数据、市场微观结构、技术指标、情绪代理这几类异构信号对齐到同一个时间轴上用 LSTM 去学它们之间的非线性时序依赖最后输出一个可执行的仓位信号。它适合两类人一类是已经会写 Python、懂一点 pandas 和 PyTorch但不知道怎么把金融数据组织成模型能吃的形状的开发者另一类是做传统量化、想把手写规则换成序列模型的从业者。下面这套东西我按能直接复现的粒度拆开讲参数、坑、验证方法都给到。2. 多因子的选型与对齐别把 OHLCV 当多因子2.1 因子分四类先想清楚每类解决什么问题所谓多因子不是把几十个技术指标一股脑塞进去。指标之间高度共线喂多了只会让 LSTM 学到噪声。我一般按四类来组织每类挑 2 到 4 个代表类别代表因子作用数据频率价格动量对数收益率、RSI、动量因子捕捉趋势与超买超卖与 K 线一致波动结构已实现波动率、ATR、布林带宽度刻画风险状态做仓位缩放与 K 线一致市场微观结构成交量变化率、买卖量比代理、换手率反映资金进出强度与 K 线一致情绪/链上代理资金费率、活跃地址变化率、搜索热度代理提供价格之外的领先或滞后信息需重采样对齐关键点在于价格类因子和 K 线同频但链上、情绪类因子往往是低频的日频甚至更低。直接 merge 会产生大量前向填充模型会误以为这些因子在盘中是恒定的。常见做法是把低频因子做前向填充后再叠加一个「距上次更新已过多少根 K 线」的衰减特征让模型自己学衰减速度。2.2 时间对齐用 merge_asof 而不是 merge这是最容易翻车的一步。K 线是 1 小时一根链上数据是每天 0 点更新如果你用普通 merge会把当天还没发生的链上数据泄漏到当天早上的 K 线上回测直接虚高。正确做法是用merge_asof做向后对齐只允许用「已经发生」的低频值。import pandas as pd import numpy as np # kline: 索引为 UTC 时间戳1H 频率含 open/high/low/close/volume # onchain: 索引为 UTC 时间戳1D 频率含 active_addr_chg / fund_rate kline kline.sort_index() onchain onchain.sort_index() # 关键directionbackward 表示每根 K 线只取它之前最近一次已更新的低频值 merged pd.merge_asof( kline, onchain, left_indexTrue, right_indexTrue, directionbackward, # 绝不能用 nearest 或 forward tolerancepd.Timedelta(3D) # 超过 3 天没更新就置 NaN避免用过期数据 ) # 衰减特征距离上次低频更新的小时数 merged[onchain_age_h] ( merged.index - merged.index.where(onchain.index.isin(merged.index)) ).total_seconds() / 3600 merged[onchain_age_h] merged[onchain_age_h].fillna(methodffill).clip(0, 72)逻辑说明directionbackward是防泄漏的核心它保证第 t 根 K 线只能看到时间戳小于等于 t 的低频值。tolerance参数防止用几个月前的陈旧数据超过阈值直接置 NaN后续在特征工程里统一填充。onchain_age_h这个衰减特征很关键它把「数据有多旧」这个信息显式交给模型比单纯前向填充有效得多。参数上tolerance我一般设成低频周期的 2 到 3 倍日频数据用 3D小时频用 3H。设太小会导致大量 NaN设太大等于没防泄漏。2.3 因子标准化滚动窗口别用全样本另一个隐蔽的泄漏点是标准化。如果你用整段数据的均值和方差做 z-score测试集的信息就漏进了训练集。必须用滚动窗口且窗口只覆盖当前时刻之前的数据。def rolling_zscore(series, window168): # window168 对应 1H K 线的 7 天 mean series.rolling(window, min_periodswindow // 2).mean() std series.rolling(window, min_periodswindow // 2).std() # shift(1) 保证当前 bar 不参与自己的标准化 return (series - mean.shift(1)) / (std.shift(1) 1e-8) factor_cols [log_ret, rsi_14, realized_vol, vol_chg, fund_rate] for col in factor_cols: merged[f{col}_z] rolling_zscore(merged[col], window168)shift(1)这一步很多人会漏导致当前 bar 的极值把自己标准化成 0模型学到虚假的规律。min_periods设成窗口一半是为了在数据起步阶段不至于全是 NaN但起步阶段的因子质量确实差训练时可以考虑丢掉前 500 根。3. 用 PyTorch 搭一个能跑通的 LSTM 多因子模型3.1 样本构造滑动窗口与标签的三种定义LSTM 吃的是序列所以要把因子矩阵切成一个个定长窗口。窗口长度我一般取 48 到 96对应 1H K 线的 2 到 4 天太短学不到中期依赖太长梯度容易衰减且样本数骤减。标签定义直接决定策略性质常见三种回归标签未来 N 根 K 线的对数收益率模型输出连续值适合做仓位大小。三分类标签涨/平/跌用阈值切分适合做方向判断。分位数标签按未来收益的分位数切保证类别平衡。我一般先用回归标签跑通因为它对阈值不敏感调起来直观。import torch from torch.utils.data import Dataset class FactorSeqDataset(Dataset): def __init__(self, df, feat_cols, seq_len72, horizon4): self.seq_len seq_len self.horizon horizon self.feats df[feat_cols].values.astype(np.float32) # 标签未来 horizon 根的累计对数收益 self.labels ( np.log(df[close].shift(-horizon) / df[close]) ).values.astype(np.float32) # 有效起点前面要有 seq_len后面要有 horizon self.valid np.arange(seq_len, len(df) - horizon) def __len__(self): return len(self.valid) def __getitem__(self, idx): end self.valid[idx] x self.feats[end - self.seq_len:end] # [seq_len, n_feat] y self.labels[end - 1] # 用窗口最后一根预测未来 return torch.from_numpy(x), torch.tensor(y)逻辑说明valid数组保证每个样本都有完整的输入窗口和未来标签避免边界越界。标签用shift(-horizon)计算未来收益注意这里用的是窗口最后一根 K 线的收盘价作为基准和实盘中「当前时刻决策」的语义一致。horizon参数控制预测周期做日内可以设 1 到 4做波段设 12 到 24。3.2 模型结构单层 LSTM 加一个注意力池化就够很多人一上来堆三四层 LSTM参数量爆炸在小样本金融数据上必然过拟合。我的经验是单层 LSTMhidden 64 到 128加一个时间维度的注意力池化效果往往比深层网络好而且训练快。import torch.nn as nn class LSTMAttn(nn.Module): def __init__(self, n_feat, hidden96, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_feat, hidden_sizehidden, num_layers1, # 单层别贪深 batch_firstTrue, dropout0.0 # 单层时 dropout 无效靠后面的 dropout 层 ) # 注意力池化给每个时间步学一个权重 self.attn nn.Sequential( nn.Linear(hidden, 32), nn.Tanh(), nn.Linear(32, 1) ) self.head nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden, 1) ) def forward(self, x): out, _ self.lstm(x) # [B, T, H] w torch.softmax(self.attn(out), dim1) # [B, T, 1] ctx (out * w).sum(dim1) # [B, H] return self.head(ctx).squeeze(-1) # [B]逻辑说明注意力池化让模型自己决定窗口里哪些时间步更重要比直接取最后一个 hidden state 更稳。dropout放在 head 之前而不是 LSTM 内部是因为单层 LSTM 内部 dropout 不生效。hidden设 96 是个折中因子数在 10 到 20 之间时够用因子特别多可以加到 128。3.3 训练循环时序切分与早停金融数据绝不能随机切分训练测试集必须按时间顺序切。我一般用 70% 训练、15% 验证、15% 测试且验证集用于早停。from torch.utils.data import DataLoader def train_model(model, train_ds, val_ds, epochs50, lr1e-3, patience6): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) opt torch.optim.AdamW(model.parameters(), lrlr, weight_decay1e-4) loss_fn nn.HuberLoss(delta0.01) # 金融收益有厚尾Huber 比 MSE 稳 train_loader DataLoader(train_ds, batch_size128, shuffleTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse) best_val, wait, best_state float(inf), 0, None for ep in range(epochs): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) opt.zero_grad() loss loss_fn(model(xb), yb) loss.backward() # 梯度裁剪LSTM 必备 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) val_loss loss_fn(model(xb), yb).item() * len(xb) val_loss / len(val_ds) if val_loss best_val: best_val, wait val_loss, 0 best_state {k: v.cpu().clone() for k, v in model.state_dict().items()} else: wait 1 if wait patience: break model.load_state_dict(best_state) return model逻辑说明HuberLoss的delta设 0.01 是因为加密货币单根 K 线收益大多在 1% 以内超过这个范围的极端值用线性损失避免被少数暴涨暴跌样本带偏。clip_grad_norm_是 LSTM 训练的标配不裁剪很容易梯度爆炸。早停的patience设 6配合weight_decay1e-4能压住大部分过拟合。4. 回测与实盘的避坑清单五个血泪教训4.1 坑一用未来数据做标准化回测夏普虚高两倍现象回测夏普 3.5实盘第一周就亏。原因标准化用了全样本均值方差测试集信息泄漏。解决所有滚动统计量必须shift(1)且窗口只覆盖历史。检查方法很简单把测试集最后 10% 数据删掉重跑如果回测指标大幅变化说明有泄漏。4.2 坑二忽略交易成本高频调仓把利润吃光现象模型每天调仓好几次回测扣费前年化 200%扣费后剩 30%。原因加密货币现货手续费加滑点单边常在 0.1% 到 0.2%。解决回测里显式建模手续费和滑点且给仓位变化加一个死区比如目标仓位变化小于 5% 就不动。def apply_cost_and_deadzone(position, target, fee0.001, deadzone0.05): # position/target 均为 [-1, 1] 的目标仓位序列 new_pos position.copy() for t in range(1, len(target)): if abs(target[t] - new_pos[t-1]) deadzone: turnover abs(target[t] - new_pos[t-1]) new_pos[t] target[t] - np.sign(target[t] - new_pos[t-1]) * fee / 2 else: new_pos[t] new_pos[t-1] return new_posfee要按双边算deadzone设 0.05 到 0.1 之间太小起不到过滤作用太大又跟不上趋势。4.3 坑三标签用了未来收益但特征里混入了未来 bar现象训练 loss 降得飞快验证 loss 也低但样本外一塌糊涂。原因构造因子时用了rolling().mean()但没shift或者用了centerTrue。解决所有滚动窗口默认centerFalse且因子计算完统一检查一遍把因子和未来收益做相关性如果某个因子和未来收益相关性异常高比如超过 0.3大概率有泄漏。4.4 坑四LSTM 输入没做序列长度对齐batch 里混了不同长度现象训练报错或者结果随机。原因不同样本的序列长度不一致DataLoader 拼不成张量。解决在 Dataset 里保证每个样本都是固定seq_len不足的用前面补齐或直接丢弃。我一般直接丢弃金融数据不缺这点样本。4.5 坑五实盘用训练时的标准化参数但市场分布已经漂移现象模型上线一个月后逐渐失效。原因因子分布随时间漂移训练时的标准化参数不再适用。解决实盘用滚动窗口实时计算标准化参数且定期比如每季度用新数据微调模型。监控因子分布用 PSI 指标超过 0.2 就报警。5. 让策略真正可用的两个进阶技巧5.1 用分位数回归替代点预测直接输出仓位点预测的问题是对极端值敏感且没法表达不确定性。一个更稳的做法是把输出改成未来收益的分位数比如预测 10%、50%、90% 分位然后用「预测收益 / 预测波动」作为仓位信号。这样模型自己就带了风险缩放。class QuantileHead(nn.Module): def __init__(self, hidden, quantiles(0.1, 0.5, 0.9)): super().__init__() self.quantiles quantiles self.linears nn.ModuleList([nn.Linear(hidden, 1) for _ in quantiles]) def forward(self, ctx): return torch.cat([l(ctx) for l in self.linears], dim-1) # [B, 3] def quantile_loss(pred, target, quantiles(0.1, 0.5, 0.9)): losses [] for i, q in enumerate(quantiles): e target - pred[:, i] losses.append(torch.maximum(q * e, (q - 1) * e)) return torch.stack(losses, dim-1).mean()逻辑说明分位数损失对每个分位单独算q*e和(q-1)*e取最大等价于对高估和低估施加不对称惩罚。训练完后用(pred_0.9 - pred_0.1)作为不确定性代理仓位 pred_0.5 / (pred_0.9 - pred_0.1 eps)再 clip 到 [-1, 1]。这个做法比直接回归收益稳得多尤其在行情剧烈切换时。5.2 滚动重训与模型集成的落地节奏单模型上线必然衰减我的习惯是滚动重训加轻量集成。具体节奏每周用最近 12 个月数据重训一次保留最近 3 个版本的模型实盘信号取三个模型的中位数。这样既跟得上市场变化又不会被单次训练的随机性带偏。验证方法上除了常规的样本外回测我强烈建议做一次「时间倒置测试」把数据按时间倒序排列重新训练和回测。如果倒序也能跑出漂亮曲线说明模型学的是统计假象而不是真实规律。这个测试帮我筛掉过好几个看似完美的方案。最后说个我自己的教训早期我总想一步到位因子堆到 50 多个模型层数加到 4 层结果调参调到怀疑人生实盘还不如一个简单的动量策略。后来把因子砍到 12 个、模型压到单层 LSTM反而稳定了。这个方向值不值得做值得但前提是你愿意在数据对齐和防泄漏上花 80% 的精力模型本身只占 20%。希望帮到你。本文还有配套的精品资源点击获取