news 2026/9/30 1:22:42

PyTorch LSTM量化交易全流程:数据清洗、模型训练与策略回测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch LSTM量化交易全流程:数据清洗、模型训练与策略回测

简介:面向量化交易与深度学习爱好者,这是一份系统讲解基于PyTorch搭建LSTM股票价格预测与回测框架的技术文档,旨在帮助读者掌握量化策略开发中从数据处理到模型训练、再到回测评估的完整流程。资源以PDF形式提供,共53页,压缩包内仅1个文件、大小2.33MB,目前已有196人学习下载。内容以PyTorch的动态计算图特性为主线,覆盖LSTM原理、数据获取与清洗、特征工程、模型构建与优化、股票价格预测、回测框架搭建,以及夏普比率等绩效评估指标。文档目录层级完整,支持章节跳转与左侧大纲快速定位,每个章节配有细分子模块,从数据源选择、API获取、技术指标计算,到LSTM层设计、损失函数与优化器配置、回测可视化等均有清晰说明。读者既能借此理解量化交易与LSTM的基础概念,也能获得一套可直接参考的代码思路与框架设计路径,用于搭建自己的量化回测实践环境,整体内容兼顾理论深度与工程落地,适合作为量化交易入门到进阶阶段的参考资料。

1. 把 PyTorch LSTM 接进量化交易:一份能跑通全链路的技术文档

把 PyTorch 和 LSTM 接到量化交易策略开发里,最难的从来不是模型本身,而是数据、训练、回测三段经常割裂:行情拉回来后要清洗、缩放、切窗,LSTM 在 PyTorch 里要处理好张量和训练循环,预测结果还要落到能算夏普比率和最大回撤的回测框架中。这份 53 页文档把链路讲透了——从量化交易概念、LSTM 门控机制,到数据获取与预处理、PyTorch 建模、训练调优、价格预测、回测与策略评估,最后用完整案例分析验证。适合想用深度学习做股票预测但没跑通过全流程的新手,也适合被未来函数和数据泄漏反复坑的从业者。文档我放在下载区了,想判断哪些章节值得细读,可以先翻目录。

2. 数据获取与预处理:从行情拉取到可直接入模的特征集

2.1 数据源怎么选:Tushare、Yahoo Finance 与本地库的取舍

文档把数据源分成三类:专业金融终端、交易所官网、免费开源平台。个人做原型验证,没必要上 Wind 或 Bloomberg,Tushare 和 yfinance 基本够用。A 股日线用 Tushare,注册拿 token 后就能调;美股用 yfinance 更直接,零门槛拉历史日线。文档里还给了爬虫方案,我的建议是能走 API 就别爬网页,维护成本差一个量级。

这里有一个很多人第一天就会踩的坑:Tushare 的pro.daily()返回的是不复权价格,除权除息日会出现价格跳空,直接拿它训练 LSTM,模型会学到一堆假突变。常见做法是拉adj_factor复权因子,算后复权价格再进模型。

import tushare as ts import pandas as pd ts.set_token('你的token') pro = ts.pro_api() # 拉取不复权日线,ts_code 格式:市场代码 + 股票代码 df = pro.daily(ts_code='000001.SZ', start_date='20190101', end_date='20241231') # 拉取复权因子,按交易日期合并 adj = pro.adj_factor(ts_code='000001.SZ', start_date='20190101', end_date='20241231') df = df.merge(adj, on=['ts_code', 'trade_date']) # 后复权:复权因子乘原始价格,让历史价格序列连续可比 for col in ['open', 'high', 'low', 'close']: df[col + '_adj'] = df[col] * df['adj_factor'] df['vol_adj'] = df['vol'] / df['adj_factor'] df = df.sort_values('trade_date').reset_index(drop=True) print(df[['trade_date', 'close', 'close_adj']].head())

start_date和end_date必须用 YYYYMMDD 格式,这是 Tushare 接口的硬性要求。我习惯把复权后的数据存一份 CSV 到本地,后续所有实验都从这份数据读,避免每次重新拉接口被限频。vol_adj用复权因子除法是后复权的标准做法,因为成交量不受价格缩放影响,但股数会因送转变化。

如果你跑的是美股,把上面逻辑换成 yfinance 的Ticker.history()就行,但同样要先查它返回的是不是 adjusted close——yfinance 默认给的是前复权价,和 Tushare 的后复权口径不一样,混用会导致训练集和回测集价格体系不一致。

2.2 缺失值与异常值:先判断成因,再决定删还是填

股票数据里的缺失值大多来自停牌,不是随机丢失。停牌期间价格没有交易,如果直接用均值填充,等于在序列里塞入一段假价格,LSTM 会学到「停牌后价格跳回均值」这种根本不存在的规律。我一般用前向填充,让价格保持停牌前最后一个值,走势曲线不会出现断层。

import numpy as np df = df.set_index('trade_date') # 缺失值:按日频重采样后前向填充 df = df.asfreq('D').ffill() # 异常值:用滚动窗口 Z-score 检测成交量突变 window_mean = df['vol_adj'].rolling(20).mean() window_std = df['vol_adj'].rolling(20).std() zscore = (df['vol_adj'] - window_mean) / window_std # |z| > 4 的成交量用 20 日均值替换,而不是直接删行 df.loc[zscore.abs() > 4, 'vol_adj'] = window_mean

这里有个容易被忽略的细节:asfreq('D')会把非交易日也补进来,周六周日变成缺失值,所以必须接ffill()。如果你只要交易日序列,就别做这一步,直接对原始交易日期索引处理。

异常值处理我建议优先用滚动窗口而不是全局统计。一只股票从 10 元涨到 100 元,成交量的正常波动区间完全不同,全局 Z-score 会把上涨期的正常放量误判成异常。滚动 20 日窗口能自适应均值和标准差,这也是文档里「基于统计方法的异常值检测」在金融数据上的正确打开方式。至于涨跌停导致的极端价格,那不是异常值——那是市场真实信息,删了反而丢数据。

2.3 特征缩放与时间划分:MinMax 归一化为什么是价格预测的默认选项

LSTM 的激活函数默认是 tanh,输出范围在 [-1, 1],输入特征如果尺度差异太大——价格是几十到几百,成交量是几百万到几千万——梯度更新会被大数值特征主导。文档里同时讲了归一化和标准化,我自己的经验是:纯价格序列用 MinMax 归一化到 [0, 1] 最稳,因为价格本身有上下界特性;如果特征里混入了类似 RSI、波动率这种已经是比率尺度的指标,再加一层标准化通常问题不大。但不要混着用,一个模型里统一一个口径。

from sklearn.preprocessing import MinMaxScaler feature_cols = ['open_adj', 'high_adj', 'low_adj', 'close_adj', 'vol_adj'] # 按时间顺序切分:前 70% 训练,中间 15% 验证,最后 15% 测试 train_size = int(len(df) * 0.7) val_size = int(len(df) * 0.15) train = df.iloc[:train_size] val = df.iloc[train_size:train_size + val_size] test = df.iloc[train_size + val_size:] # 关键:scaler 只 fit 训练集,验证集和测试集只 transform scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train[feature_cols]) train_scaled = scaler.transform(train[feature_cols]) val_scaled = scaler.transform(val[feature_cols]) test_scaled = scaler.transform(test[feature_cols])

这段代码里最重要的是scaler.fit(train)这一行。如果对全量数据做 fit,测试集的 min/max 信息就已经被模型看到了,这种数据泄漏会让回测结果虚高,实盘一上线就现原形。后面在回测章节我会再展开讲这个坑。

数据划分也必须严格按时间顺序。股票价格是强非平稳序列,随机打乱训练集和测试集等于让模型「偷看未来」,这种错误在初学阶段极其常见——很多人习惯了图像分类的train_test_split(random_state=42),直接套到时间序列上,回测出的曲线再漂亮也没有任何参考价值。

技术指标方面,文档里提到相关性分析和特征重要性排序。我的习惯是在 OHLCV 基础上加三类特征:收益率pct_change()、均线差、滚动波动率。均线不要加太多条高度相关的,MA5 和 MA10 相关性极高,同时进模型只会增加冗余。加特征之前先跑一遍df.corr(),把和目标变量相关系数绝对值低于 0.05 的特征砍掉,能减少不少噪声。

# 在缩放之前构造特征 df['ret'] = df['close_adj'].pct_change() df['ma5'] = df['close_adj'].rolling(5).mean() df['ma20'] = df['close_adj'].rolling(20).mean() df['volatility'] = df['ret'].rolling(20).std() # 特征列顺序固定,后续训练测试保持一致 feature_cols = ['open_adj', 'high_adj', 'low_adj', 'close_adj', 'vol_adj', 'ret', 'ma5', 'ma20', 'volatility']

pct_change()会产生第一个 NaN,rolling()也会有前 N 行是 NaN,统一用dropna()清掉再切分。特征列顺序一定要固定下来,别今天 A 顺序训练、明天 B 顺序测试,PyTorch 不关心列名,它只认位置索引。

3. 基于 PyTorch 搭建 LSTM 模型:从时间窗构造到完整训练循环

3.1 nn.LSTM 的关键参数与输入输出形状

PyTorch 的nn.LSTM有四个参数必须搞清楚:input_size是每个时间步的特征维度;hidden_size是隐藏状态维度,决定模型的记忆容量;num_layers是堆叠层数,层数越多模型越深;batch_first控制输入张量的维度顺序。很多人第一次用 LSTM 就被 shape 搞懵了,问题多半出在没设batch_first=True。

import torch import torch.nn as nn # 单层 LSTM 测试,体会一下输出形状 lstm = nn.LSTM(input_size=5, hidden_size=64, num_layers=2, batch_first=True, dropout=0.2) x = torch.randn(32, 30, 5) # (batch_size, lookback, input_size) output, (h_n, c_n) = lstm(x) print(output.shape) # 期望输出 (32, 30, 64) print(h_n.shape) # (2, 32, 64)

按batch_first=True约定,输入形状是 (batch, seq_len, input_size),输出也是 (batch, seq_len, hidden_size),非常直观。h_n的形状是 (num_layers, batch, hidden_size),c_n同理。还有一个容易忽视的点:dropout参数只有在num_layers > 1时才会生效,单层 LSTM 传 dropout 不报错但也没作用。

hidden_size的选择我一般从 64 起步,股价预测这种单输出回归任务,64 已经能捕捉大部分序列依赖;到 128 以上对数据量的要求会陡增,小样本下反而容易过拟合。

3.2 滑动窗口构建与 DataLoader

LSTM 不是把整段历史价格一次性吞进去,而是按固定长度的窗口滑动切分。比如用过去 30 个交易日预测下一个交易日的收盘价,窗口每滑一天就生成一条样本。文档里讲的数据格式转换,本质就是干这件事。

from torch.utils.data import Dataset, DataLoader class StockDataset(Dataset): def __init__(self, data, lookback=30): self.data = torch.tensor(data, dtype=torch.float32) self.lookback = lookback def __len__(self): # 窗口需要 lookback 行,最后 lookback 行无法构成完整样本 return len(self.data) - self.lookback def __getitem__(self, idx): x = self.data[idx: idx + self.lookback] # 过去 30 天特征 y = self.data[idx + self.lookback, 3] # 预测第 31 天的 close_adj return x, y train_dataset = StockDataset(train_scaled, lookback=30) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_dataset = StockDataset(val_scaled, lookback=30) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)

注意y取的是close_adj所在列。如果你在特征列表里调整了列顺序,这个索引必须跟着改,我建议把目标列固定放第一位或最后一位,减少出错概率。训练集shuffle=True可以让梯度更新更稳定,但验证集和测试集必须shuffle=False,保持时间顺序。

3.3 模型结构与前向传播

LSTM 每个时间步都会输出一个隐藏状态,我们需要的是最后一个时间步的隐状态,再接一个线性层压缩成价格预测值。这是 LSTM 做价格预测最标准的回归头结构。

class PricePredictor(nn.Module): def __init__(self, input_size=5, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # (batch, seq_len, hidden_size) out = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(out).squeeze(-1) # (batch,) model = PricePredictor(input_size=9, hidden_size=64, num_layers=2)

out[:, -1, :]是取序列最后一个时间步的输出,等价于直接用h_n[-1],两种写法都行。squeeze(-1)是把线性层输出的 (batch, 1) 压成 (batch,),方便和标签算 loss。如果你的input_size是 9(前面构造了 9 个特征列),这里要对应传 9。

3.4 损失函数、优化器与训练循环

价格预测是回归任务,最常用的损失函数是 MSE,但我会建议你备选 Huber Loss——它对异常值不那么敏感,股价偶发暴涨暴跌时,Huber 不会让单个样本主导梯度。优化器首选 Adam,默认学习率 1e-3,对 LSTM 这种中等深度的模型通常够用。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = PricePredictor(input_size=9).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss = 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() # 梯度清零,否则会累加 pred = model(x) loss = criterion(pred, y) loss.backward() optimizer.step() # 更新参数 total_loss += loss.item() * len(y) return total_loss / len(loader.dataset)

optimizer.zero_grad()每步都要调用,这是 PyTorch 和 TensorFlow 的惯性差异——PyTorch 不会自动清空梯度,漏掉这行 loss 会越训越乱。total_loss按 batch 内样本数加权,避免最后一个不完整 batch 扭曲平均值。训练回合里如果把x直接unsqueeze(0)变成 (1, lookback, features),那就是单样本预测模式,后面第六章会用到。

4. 训练与调参:学习率、批大小、早停与监控的落地配置

4.1 PyTorch 环境与版本匹配

开始调参之前,先把环境弄干净。PyTorch 和 Python 版本有对应关系,Python 3.10 或 3.11 配 PyTorch 2.x 是当前最省心的组合,网上搜 pytorch 安装教程一抓一大把,但版本对应关系才是大多数人翻车的地方——装完 import 报错,多半是 Python 版本太新或太旧。我建议直接用 Anaconda 建独立环境,别往系统 Python 里塞。

conda create -n quant python=3.11 -y conda activate quant conda install pytorch torchvision torchaudio cpuonly -c pytorch

如果你有 NVIDIA GPU 且想用 CUDA 加速,把cpuonly换成对应的pytorch-cuda=12.1之类的版本号,具体看你的驱动支持。装完先跑一句诊断:

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

没有 GPU 也不用慌,CPU 训练 30 万条以内的日线数据,100 个 epoch 也就十几分钟,完全扛得住。金融日线数据量本身不大,GPU 更多是心理安慰。

4.2 学习率、批大小与学习率调度

学习率是 LSTM 训练里最玄学的超参数。固定学习率 1e-3 起步没问题,但训练后期 loss 容易在某个平台期来回震荡。我现在的标准配置是:Adam 初始 1e-3,配合ReduceLROnPlateau,验证损失连续 5 个 epoch 不降就自动减半。这个组合比手动调学习率省心得多。

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5 ) # 每个 epoch 结束后调用一次 scheduler.step(val_loss)

factor=0.5表示每次衰减为原来的一半,patience=5表示容忍 5 个 epoch 不改善。对金融数据这种噪声大的序列,patience 别给太小,3 个 epoch 没降就减半会把学习率过早打到地板。批大小一般取 32 或 64,日线数据一两个周期就够;如果 loss 曲线震荡得厉害,优先加大 batch 而不是改学习率。

4.3 早停、正则化与验证集监控

LSTM 在股价预测上过拟合的速度比想象中快——训练集 loss 降到 0.001,验证集却在 0.01 附近飘。解决办法就是早停加正则化双管齐下。文档里提到 L1、L2 和早停策略,我的主力是早停 + dropout,L2 权重衰减也会顺手加一点。

best_loss = float('inf') patience = 10 no_improve = 0 for epoch in range(200): train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss = evaluate(model, val_loader, criterion, device) scheduler.step(val_loss) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'best_model.pt') no_improve = 0 else: no_improve += 1 if no_improve >= patience: print(f'epoch {epoch}: early stop') break # 加载验证集上最优的那份权重,而不是最后一个 epoch model.load_state_dict(torch.load('best_model.pt'))

这里有个重要的工程习惯:只保存验证集最优权重,训练结束后加载它再去做预测。最后一个 epoch 的权重不一定是验证集上最好的,直接拿它做测试集预测会低估模型真实水平。持久化用state_dict而不是整个 model 对象,跨环境迁移更省事。

4.4 训练日志与 loss 曲线判读

训练时要盯两条曲线:训练 loss 和验证 loss。我一般把每一轮的数值打印到控制台,同时存一份 CSV,方便事后画图。

现象判断处理
训练 loss 降,验证 loss 先降后升过拟合提高 dropout,减小 hidden_size,增大权重衰减
两条曲线都平稳不降学习率太低或模型容量不足学习率调至 3e-3,hidden_size 翻倍
两条曲线剧烈震荡学习率太高或 batch 太小学习率降到 1e-4,batch 提到 128
训练 loss 比验证 loss 高很多训练集噪声大或标签异常检查标签对齐,确认没用错列

判读曲线的时候记住一点:验证 loss 的绝对值不重要,训练-验证 gap 才是过拟合的核心信号。gap 持续拉大,早停就会触发;gap 很小但两条线都平,说明模型容量不够,要先加层或加 hidden 而不是盲目加大 epochs。文档里「训练过程监控」一节讲的损失函数监控,落地就是这套日志和判读习惯。

5. 回测框架与评估指标:五个高频踩坑点排查

5.1 最小可用回测循环:从信号到净值曲线

预测做完只是第一步,模型给出的价格预测必须转成持仓信号,再按历史行情算出这笔策略赚不赚钱。文档里回测框架包含数据模块、策略模块、交易模块、评估模块,落到代码上,一个最小可用的回测循环其实很短。我之前也折腾过 backtrader 这类现成框架,但教学和验证阶段,自己写几十行循环反而更可控。

def simple_backtest(close, pred, cost=0.001, threshold=0.005): position = 0 equity = [1.0] for i in range(len(close) - 1): # 当日收盘时根据预测判次日仓位:预测涨幅超阈值才持仓 target = 1 if pred[i] > close[i] * (1 + threshold) else 0 if target != position: # 换仓,扣一次交易成本 equity.append(equity[-1] * (close[i + 1] / close[i]) * (1 - cost)) position = target else: equity.append(equity[-1] * (close[i + 1] / close[i])) return np.array(equity)

threshold=0.005意味着预测涨幅超过 0.5% 才开仓,低于这个值就空仓,避免被模型输出的微小噪声反复触发买卖。cost是单次交易成本,含手续费和滑点,我一般按千分之一到千分之二估。pred[i]是用第 i 天及之前的数据预测出的结果,对应的是第 i 天的收盘价预测,所以信号在第 i+1 天才生效——这里不能写成当天买入,否则就是标准的未来函数。

5.2 评估指标计算:夏普比率、最大回撤与年化收益

文档用了一整章讲策略评估指标体系,从总收益率到信息比率。实际验证一个策略,我必看的五个指标是:总收益率、年化收益率、最大回撤、夏普比率、索提诺比率。它们的计算都不复杂,但含义要分清。

def sharpe_ratio(returns, rf=0.0, periods=252): excess = returns - rf / periods return excess.mean() / returns.std() * np.sqrt(periods) def max_drawdown(equity): peak = np.maximum.accumulate(equity) return ((equity - peak) / peak).min() def annual_return(equity, periods=252): years = len(equity) / periods return (equity[-1] / equity[0]) ** (1 / years) - 1
指标作用使用注意
年化收益率衡量盈利能力样本时间太短时年化会失真,至少覆盖一个完整市场周期
最大回撤衡量极端风险只看数值不够,还要看回撤持续了多少天
夏普比率风险调整后收益分母用的是总波动率,上涨波动也算风险
索提诺比率只惩罚下行波动更适合非对称收益分布的策略
信息比率相对基准的超额收益需要选对基准指数,A 股常用沪深 300

年化收益率的公式里periods=252是 A 股每年的交易日数量,如果你的数据是周线,这里要改成 52。最大回撤是净值曲线从峰值到谷底的最大跌幅,它是负值,数值越接近 0 越好。

5.3 交易成本与滑点:让回测贴近实盘的第一个修正

很多第一次写回测的人完全不扣交易成本,跑出来的曲线平滑得像教科书。A 股双边手续费加印花税,一次完整买卖的成本接近千分之一到千分之二,高频策略一个月换手 20 次,光成本就能吃掉 4% 的收益。文档里交易模块讲了交易规则定义和模拟交易实现,成本是其中不能忽略的一环。我的习惯是成本参数集中定义,回测结束做一次敏感性分析:成本从 0 到 0.005 各跑一遍,看策略收益对成本的敏感度——如果成本涨一倍收益就由正转负,这个策略没有实盘价值。

5.4 高频踩坑点排查

以下四个问题是我在复现这套框架时真实踩过的,每条都按现象、原因、解决记录。

坑 1:回测曲线稳步上涨,实盘完全对不上

现象:回测年化收益 30%,实盘跑了两个月亏损。原因:数据预处理时把 scaler 对全量数据做了 fit,测试集的 min/max 泄漏到训练过程,模型相当于提前知道了测试集的价格区间。解决:scaler 只 fit 训练集,验证集和测试集只调用 transform,代码见 2.3 节。

坑 2:除权日价格「凭空」跳空,策略在除权日大量误触发

现象:净值曲线在每个除权日出现异常回撤,信号频繁翻转。原因:用的是不复权价格,除权日股价跳空低开被模型识别为下跌信号。解决:统一用后复权价格训练和回测,真实收益计算再考虑现金分红。

坑 3:网格搜索选出的最优参数,样本外一塌糊涂

现象:验证集上 200 组参数跑完,选了夏普最高的那组,测试集直接腰斩。原因:参数寻优过程实际上是在验证集上做隐式拟合,来回试参数等于把验证集信息偷进模型。解决:网格搜索只基于训练集和验证集,测试集全流程只跑一次。参数寻优结果必须检查是否落在参数空间的平滑区域,孤立尖峰点的参数不可信。

坑 4:训练集 loss 正常,验证集 loss 持续偏高且不稳

现象:训练 loss 降到 0.001,验证集一直在 0.01 附近震荡。原因:一是验证集没有按时间顺序切分,随机混入了未来信息;二是模型过拟合,验证集分布和训练集差异被放大。解决:按时间顺序切分数据集,验证集 DataLoader 关闭 shuffle,同时检查 dropout 和早停是否已生效。

6. 让预测真正可用:滚动预测、逆变换与信号过滤

6.1 滚动预测:把新数据接进窗口

训练时我们用的是固定窗口,实际部署时每天都有新数据进来,窗口要跟着滚动。每次预测时,把包括最新一天在内的过去 30 天特征喂给模型,而不是重新训练。这里有一个容易踩的细节:预测结束后,要用当天真实收盘价更新窗口,而不是用预测值继续往后滚——用预测值喂预测值,误差会像滚雪球一样累积,三五天后预测曲线就飘了。

def rolling_predict(model, df, scaler, lookback=30): model.eval() preds = [] for i in range(lookback, len(df)): window = df.iloc[i - lookback:i][feature_cols] window_scaled = scaler.transform(window) # 注意:只 transform,不 fit x = torch.tensor(window_scaled, dtype=torch.float32).unsqueeze(0).to(device) with torch.no_grad(): pred = model(x).item() # 逆变换还原成价格 inverse = scaler.inverse_transform([[0, 0, 0, pred, 0]])[0] preds.append(inverse[3]) return preds

6.2 归一化逆变换:还原成真实价格

scaler 是五列特征一起 fit 的,逆变换时也要传五维向量,不能只传一个价格进去。所以我构造[[0, 0, 0, pred, 0]]占位,只把第 3 列(close_adj 所在位置)填成预测值,逆变换后取inverse[3]。如果你调整了特征列顺序,这里的位置索引务必同步改。这个写法看起来有点 hack,但比手动还原pred * (max - min) + min少一步出错机会,scaler 内部逻辑如果有变化也能自动适配。

6.3 信号过滤:把预测值变成可执行的动作

预测价格出来后直接做多,是新手最常犯的错。模型输出的是条件期望,噪声很大,预测涨幅 0.1% 就开仓,手续费就能把利润吃光。我推荐的做法是把预测涨幅和交易成本绑在一起算最小触发阈值:单笔交易成本是千分之一,那预测涨幅至少要覆盖两倍成本才值得动手,所以阈值应该设置在 0.2% 到 0.5% 之间。文档里策略模块提到的双均线、动量策略,本质也是在做信号过滤,只是过滤依据从模型预测换成了技术指标。

有一回我把过滤阈值从 0.5% 调到 2%,结果模型整整三个月没发出任何买入信号,回测净值变成一条直线,dry run 时间全白费;反过来完全不设阈值,回测曲线被手续费啃成锯齿,换手率高得吓人。从那以后,我每次调信号过滤都把阈值和 cost 绑在一起算——先算单笔交易的期望净收益,再反向推导最小触发涨幅,而不是拍脑袋定一个数。这套习惯帮我挡掉了很多看起来很美、实际跑不动的参数组合。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:22:39

手把手写RT-Thread hwtimer驱动:先楫BSP完整实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:22:39

毫米波雷达目标识别全解析:点云生成、4D雷达与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:22:28

同步与互斥:并发编程中不可绕过的底层生存法则

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:21:06

AI芯片到底是什么?架构、算力指标与工程选型实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:20:37

C++友元机制详解:从封装破坏到精准授权的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华