
简介一套基于LSTM多因子模型的比特币交易策略Python源码与文档面向有Python基础、对AI量化感兴趣的在校生、研究者或爱好者。策略利用LSTM从历史行情中提取时序特征结合多因子输入降低单一因子风险有助于减小最大回撤和收益波动可适配课程设计、毕业设计或量化入门实践。压缩包共收录12个文件整体仅797KB包括2个Jupyter Notebook主代码、4个CSV行情数据、3个工程配置文件、1份README说明和1张效果图目录简洁便于直接阅读与复现实验。目前已有262位学习者浏览该资源。下载后即可获得完整可运行的策略代码与配套数据代码均测试通过可结合README梳理建模思路并在此基础上扩充特征、调整超参数或进一步尝试集成学习等改进方向适合作为AI金融领域的毕设或课题研究起点。1. LSTM预测比特币价格为什么还要“多因子”如果你把LSTM当成一个“涨跌预测器”拿预测值直接开多开空大概率会在实盘里被来回打脸。我拆过不少基于LSTM的币圈策略发现一个共性问题模型准确率和策略收益率根本不呈线性关系真正决定资金曲线是否平滑的是预测信号之外的因子组合、仓位映射和回撤控制。这份BTC_LSTM-master源码走的正是“先预测、再约束”的路线用PyTorch/Keras搭建LSTM网络预测BTC价格方向同时引入价格以外的辅助因子来过滤交易信号把最大回撤压住、把资金曲线磨平。源码本身是一套可以真正跑通的毕设项目包含了daily_price_btc_cny.csv、btc_nvt.csv等数据文件以及BTC_LSTM.ipynb完整Notebook。适合两类人看一是想做量化课设、毕设的学生想拿到一套能改的完整管线二是已经在做CTA或币圈策略的工程师想看看别人是怎么处理回撤和因子拼接的。不过要提醒一句同样的数据、同样的代码跑出来的收益存在随机性——LSTM的初始化权重和训练顺序天然会带来差异所以源码里也明确提示了加集成学习的改进方向。2. 数据与多因子构造除了收盘价LSTM还能吃什么2.1 原始数据的分布与对齐方式项目里的数据文件名已经暴露了因子来源daily_price_btc_cny.csv是BTC对CNY的日线价格序列btc_nvt.csv提供的是NVT指标Network Value to Transactions网络价值与链上交易量的比值。NVT可以被理解成BTC的“估值温度计”——当NVT过高时说明市值相对于链上实际转账量偏高通常意味着泡沫风险增大过低则可能被低估。拿到数据后第一步不是训练模型而是检查时间轴对齐。常见做法是直接按日期做inner join把价格数据和NVT数据合并成一张宽表import pandas as pd price pd.read_csv(daily_price_btc_cny.csv, parse_dates[date]) nvt pd.read_csv(btc_nvt.csv, parse_dates[date]) df pd.merge(price, nvt, ondate, howinner).sort_values(date) print(df.isnull().sum())这段代码的逻辑有三点第一用parse_dates把日期列转成时间类型方便后续重采样第二inner join只保留两边都有的交易日避免NVT缺失日期产生NaN第三isnull().sum()是必须做的检查——如果合并后NaN过多说明两条数据源的时间覆盖范围不一致需要前向填充或直接裁剪。2.2 构造LSTM能消化的特征窗口LSTM不直接吃“今天的数据”它吃的是“过去N天的数据序列”。所以要把宽表转换成有监督学习的样本用过去60天的特征预测未来第1天或未来第3天的价格方向。特征维度我一般会做这几个特征名计算方式作用return_1close.pct_change(1)短期动量return_5close.pct_change(5)中期动量过滤噪音volatility_20close.pct_change().rolling(20).std()波动率用于仓位控制volume_zscorevolume的20日z-score量能异常放大信号nvt_zscorenvt的20日z-score估值偏离度过滤追高信号构建标签时有个关键选择——用回归还是分类。源码走的是分类路线如果未来3天收益率大于0label设为1否则为0。这样LSTM的输出层用一个sigmoid神经元损失函数用binary_crossentropy训练速度比回归快信号也更稳定。import numpy as np def build_sequences(data, lookback60, horizon3): X, y [], [] for i in range(lookback, len(data) - horizon): X.append(data[i - lookback:i]) y.append(1 if data[i horizon, 0] data[i, 0] else 0) return np.array(X), np.array(y) feature_cols [return_1, return_5, volatility_20, volume_zscore, nvt_zscore] X, y build_sequences(df[feature_cols].values)这段代码里data[i-horizon:i]是样本窗口data[ihorizon, 0]是未来第horizon天的return_1值用来和当前值比较生成方向标签。X的形状是(样本数, 60, 5)60是时间步长5是因子维度。这个形状是LSTM输入层的硬性要求——(batch_size, time_steps, input_dim)。2.3 归一化是所有LSTM项目的生死线LSTM内部用的是tanh和sigmoid激活函数输入值如果分布在0到1000的量级梯度会直接饱和训练根本推不动。这里不能偷懒用MinMaxScaler对整个数据集做变换因为会引入未来信息泄漏。正确做法是只在训练集上fit再用同一组min-max参数去transform验证集和测试集。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_len int(len(X) * 0.8) X_train_raw X[:train_len] X_train np.array([scaler.fit_transform(X_train_raw[i]) for i in range(len(X_train_raw))]) X_test_raw X[train_len:] X_test np.array([scaler.transform(X_test_raw[i]) for i in range(len(X_test_raw))])注意这里是对每个样本窗口单独做归一化而不是对整个特征矩阵做一次归一化。原因在于LSTM关注的是序列内部的相对变化模式而不是绝对数值按窗口归一化能让模型更关注因子在时间轴上的形态而不是被某一段暴涨行情的绝对值带偏。这种方式在源码里没有展开写但实际跑的时候遇到loss不下降第一个要怀疑的就是这一步。3. LSTM模型结构与PyTorch实现细节3.1 模型选型为什么用双向LSTM或堆叠LSTM项目里用的是经典单向LSTM但如果你想让模型同时看到“过去”和“未来”的上下文在时间序列里也就是前后走势的对称性可以改成双向LSTM。不过对交易策略来说双向LSTM有未来函数风险——用t时刻之后的数据预测t时刻回测会虚高。源码里的单向LSTM反而是更诚实的选择。我在拆源码时注意到Notebook中的网络结构是单层LSTM Dropout Dense(1, sigmoid)。这个结构对日线数据足够了但有几个地方值得改一是把LSTM层的return_sequences设为True后再接一层LSTM堆叠结构能捕捉更高阶的时间依赖二是Dropout建议在0.2-0.4之间太低了过拟合太高了欠拟合三是LSTM隐藏单元数设在32-128之间太大在小样本上就是灾难。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(units64, return_sequencesTrue, input_shape(60, 5))) model.add(Dropout(0.3)) model.add(LSTM(units64, return_sequencesFalse)) model.add(Dropout(0.3)) model.add(Dense(units1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()代码逻辑不复杂第一层LSTM保留所有时间步输出return_sequencesTrue给第二层LSTM继续编码第二层只输出最后一个时间步的隐状态接Dropout后进入sigmoid输出层。input_shape的第一个维度是lookback窗口长度60第二维是因子数5这两个数字必须和前面build_sequences里构造的X形状严格一致否则会报维度不匹配错误。3.2 训练策略验证集划分与早停日线数据量本身就少比特币从2013年到现在也就几千个交易日所以不能像CV那样狂训几百个epoch。源码里没有直接用全量数据训练而是预留了20%做验证集。我在这个基础上会加EarlyStopping和ReduceLROnPlateau两个回调分别解决过拟合和训练后期loss震荡的问题。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue) reduce_lr ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) history model.fit( X_train, y_train[:train_len], validation_data(X_test, y_test), epochs100, batch_size32, callbacks[early_stop, reduce_lr], verbose1 )这里y_train[:train_len]是对齐标签索引——因为X_train和X_test已经是按时间顺序拼接后的数组而标签也要做同样的切分。batch_size32在日线尺度上是合理的太小会导致梯度更新方向不稳定太大又会让每个epoch的更新次数太少。patience15表示连续15个epoch验证集loss没有下降就停训避免浪费时间继续跑。3.3 预测结果与准确率解读训练完成后用测试集做预测然后需要做一次反归一化——虽然LSTM输出的方向标签不需要反归一化但如果你把模型改成回归预测价格这一步就省不了。方向预测的评估指标用accuracy不够全面还需要看预测分布。比如模型预测出80%的概率是涨结果只涨了0.1%这种预测的“确定性”和实际幅度不匹配策略上就不该给满仓位。from sklearn.metrics import accuracy_score, confusion_matrix y_pred_prob model.predict(X_test) y_pred (y_pred_prob 0.5).astype(int) print(Accuracy:, accuracy_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))把y_pred_prob直接留在手里是有用的它比0/1硬标签信息量更大可以做后续的仓位权重。confusion_matrix要重点关注两个数字预测“涨”但实际跌的样本数False Positive以及预测“跌”但实际涨的样本数False Negative。前者会让策略开空被套后者会让策略踏空行情。如果FP明显偏高说明模型在震荡行情中偏向看多需要加过滤器。4. 多因子信号合成与回测框架搭建4.1 从LSTM概率到交易信号阈值与方向过滤拿到LSTM预测概率之后直接prob 0.5就开仓是最粗糙的玩法。因为0.5阈值下模型预测的准确率在55%左右单笔交易的胜率并没有压倒性优势。源码里的思路是用多个因子做信号叠加只有“LSTM预测涨”和“技术因子不反对”同时成立才开多。我一般会设计一个复合评分函数将LSTM输出概率、NVT z-score、波动率状态三个信号加权求和:def generate_signal(lstm_prob, nvt_zscore, vol_percentile, threshold0.6): score 0.0 if lstm_prob 0.55: score 0.5 elif lstm_prob 0.45: score - 0.5 if nvt_zscore 1.0: score 0.3 else: score - 0.3 if vol_percentile 0.8: score 0.2 else: score - 0.2 if score threshold: return 1 elif score -threshold: return -1 return 0这里的逻辑是LSTM概率大于0.55时给正向分低于0.45给负向分中间区域不给分避免在模糊区间强行交易NVT z-score小于1时说明估值不离谱允许加分波动率位于历史80分位数以下时才开仓因为高波动期LSTM的预测误差会显著放大。threshold0.6代表必须至少两个因子共振才产生实际信号单因子触发会被过滤掉。4.2 回测引擎实现资金曲线、最大回撤、夏普比率有了信号序列回测就是一个向量化计算的过程。完整回测框架需要包含手续费、滑点、以及持仓周期限制。源码没有给手续费参数但在实盘中这其实是决定策略生死的一环。我搭的回测引擎长这样def backtest(prices, signals, fee_rate0.001, slippage0.0005): position 0 equity 1.0 equity_curve [] prev_price prices.iloc[0] for i in range(1, len(prices)): price prices.iloc[i] if position ! 0: trade_return (price - prev_price) / prev_price * position equity * (1 trade_return - fee_rate - slippage) if signals.iloc[i] 1 and position 0: position 1 equity * (1 - fee_rate - slippage) elif signals.iloc[i] -1 and position 0: position -1 equity * (1 - fee_rate - slippage) elif signals.iloc[i] 0 and position ! 0: position 0 prev_price price equity_curve.append(equity) return equity_curve def max_drawdown(equity_curve): peak equity_curve[0] mdd 0.0 for v in equity_curve: if v peak: peak v mdd max(mdd, (peak - v) / peak) return mdd这个回测实现有几个细节第一position取值是1、-1、0分别代表多头、空头和空仓第二手续费和滑点每次换仓时都扣一次fee_rate0.001是币安这类交易所的taker费率水平滑点0.0005是保守估计第三max_drawdown实时追踪资金曲线峰值回撤是历史最高点到当前点的最大跌幅这个是策略“稳健性”的核心指标。4.3 资金曲线分析为什么这个策略更稳定源码摘要里明确提到这个策略会减少最大回撤、减少波动这一点从因子设计上就能看出原因NVT过滤会在估值过高时强制降仓位波动率过滤会在市场剧烈震荡时暂停交易。两个过滤器叠加后策略实际上只在“趋势比较顺、估值不离谱、波动可接受”的三重条件下满仓运行。对比纯LSTM策略和加因子过滤后的策略资金曲线差异会非常明显。纯LSTM策略在2017年大牛市尾部会回吐大量利润而多因子策略会在NVT z-score超过2时自动减仓躲过大部分回调。这也解释了为什么摘要里强调“收益率与LSTM预测的准确率直接相关”——准确率决定上限但因子过滤器决定下限。5. 集成学习与超参搜索把随机性变成稳定性5.1 为什么同样的数据每次跑结果不一样LSTM训练有三个随机源权重初始化、mini-batch采样顺序、Dropout的随机失活。源码里也说了“同样的数据收益率存在随机性”所以同一个Notebook跑两次测试集准确率可能差1-2个百分点最终资金曲线也会有肉眼可见的差异。这个问题靠调种子值random_seed只能掩盖不能解决本质。更稳的做法是集成学习——训练多个LSTM模型每个模型用不同的随机种子初始化或者用不同长度的lookback窗口比如40、60、80最后把多个模型的预测概率取平均。多个模型的错误模式互不相关平均之后方差会明显收窄。5.2 网格搜索还是贝叶斯搜索源码提示“应该加入更多因子及设置更合适的超参”。超参搜索在这个项目里可以用Keras Tuner或者Optuna来做。以Optuna为例搜索空间包括LSTM层数1到3、隐藏单元数16到128、Dropout比率0.1到0.5、学习率1e-4到1e-2、lookback窗口30到90。import optuna from tensorflow.keras.optimizers import Adam def objective(trial): n_units trial.suggest_int(n_units, 32, 128, step16) dropout trial.suggest_float(dropout, 0.1, 0.5, step0.1) lr trial.suggest_float(lr, 1e-4, 1e-2, logTrue) model Sequential() model.add(LSTM(n_units, return_sequencesTrue, input_shape(60, 5))) model.add(Dropout(dropout)) model.add(LSTM(n_units, return_sequencesFalse)) model.add(Dense(1, activationsigmoid)) model.compile(optimizerAdam(learning_ratelr), lossbinary_crossentropy, metrics[accuracy]) history model.fit(X_train, y_train[:train_len], validation_data(X_test, y_test), epochs50, batch_size32, verbose0) return history.history[val_accuracy][-1] study optuna.create_study(directionmaximize) study.optimize(objective, n_trials20) print(study.best_params)这里有个容易犯的错val_accuracy用的是最后一个epoch的值但EarlyStopping恢复的是最优权重最后几个epoch的准确率可能已经退化了。更稳妥的做法是在回调里用restore_best_weightsTrue并且返回验证集上历史最优的准确率而不是最后一个epoch的准确率。5.3 因子扩展方向链上数据与情绪数据源码里的因子还比较基础价格、成交量、NVT扩展空间很大。我拆过的类似项目中比较有效的是加入链上活跃地址数Active Addresses、交易所净流入量Exchange Netflow、以及资金费率Funding Rate。其中资金费率是永续合约特有的因子——当资金费率持续为正且过高时说明市场多头过度拥挤短期内回调的概率大当资金费率为负时市场偏空可能出现轧空行情。这类因子与LSTM的价格预测信号互补性很强因为LSTM捕捉的是价格形态而链上因子捕捉的是市场微观结构。跑完上面的流程你手里应该已经有一套能输出信号、能回测、能计算回撤的完整方案。接下来最值钱的工作是把回测中表现最好的因子组合拿到样本外数据上验证同时把单次训练的随机性通过多个随机种子的集成办法消除。如果回测收益和资金曲线仍然让你不满意优先检查问题出在训练集与测试集的切分方式上而不是急着堆模型层数。本文还有配套的精品资源点击获取