
做时序预测的人应该都见过这种场面把美股日线数据原样丢给LSTM训练集上的loss曲线漂亮得可以当教科书配图准确率一度逼近90%可一换到测试集就原形毕露甚至赶不上“昨天涨了今天接着猜涨”的无脑策略。我一开始以为是模型结构配错了层数、hidden size、dropout轮着调了小一个月后来才意识到问题大头根本不在神经网络而在数据准备和特征构造上。这篇文章是我基于雅虎财经数据接口做美股历史数据预测的全流程复盘核心就围绕两个关键词时序特征处理和LSTM。我会把从原始K线到模型输入特征、再到标签设计和最终回测评估的每个环节都拆开细讲该给代码给代码该给坑位给坑位。适合正在入门金融时序预测的人也适合那些跑通了demo但结果不稳定、想知道自己哪里做错的朋友。1. 项目定位预测涨跌方向比预测具体价格更有意义1.1 为什么我选了雅虎财经数据接口做实验场做美股历史数据研究数据源是第一道关卡。雅虎财经的数据接口几乎算是个人研究者的默认选择免费、日频覆盖时间够长、字段完整Open、High、Low、Close、Volume一应俱全还自带复权后的调整价格。用Python生态里的开源封装库拉数据几行代码就能把十几年的历史数据整理成DataFrame比自己去爬行情网站或者申请商业数据要省太多事。当然免费的代价是偶尔会有接口限流和延迟但作为构建预测模型的原料它的结构和稳定性完全够用。更重要的是日线级别的数据天然适合LSTM这类序列模型来实验——样本量不会少到没法训练又保留了明显的时序依赖结构。我在这类项目里养成的习惯是先用雅虎数据把整个特征工程和模型框架跑通确认信号有效之后再考虑要不要换更高频或者更干净的数据源。1.2 LSTM在这个项目里的真实定位很多人一听到“用LSTM做股票预测”第一反应就是“AI要跑赢市场了”。这个预期必须先校准。金融时序数据是出了名的低信噪比有效信号可能就藏在几个百分点的统计优势里任何模型都不可能稳定地“预测明天涨还是跌”到很高的准确率。LSTM在这里的角色不是点石成金的神器而是一个比传统时序模型更能吸收多特征、更能捕捉长期依赖的特征抽取器。举个例子ARIMA这类线性模型的强项是单变量时间序列的自回归关系面对多变量交叉作用和突变型的非线性模式非常吃力。LSTM通过门控机制选择性地记住和遗忘信息可以把过去十几天甚至几十天的波动状态、动量变化揉进当前预测里这是它比传统模型更适配这个场景的核心原因。所以整条技术路线可以表述为用特征工程把原始价格数据变成有金融含义的输入再用LSTM去挖掘其中跨时间的非线性规律最后以方向预测的胜率作为真实评价标准。1.3 项目完整流程速览整个项目大致分成六个环节环环相扣任何一环偷懒都会在最终结果上现形数据获取与清洗拉取日线数据处理复权、时区、缺失值。特征构造从原始价格转向收益率、滚动统计、技术指标、相对强弱等特征。标签设计定义未来N日的涨跌方向标签。训练集/验证集/测试集切分严格按时间顺序禁止随机打乱。LSTM模型搭建窗口样本生成、归一化、网络结构设计和训练。评测与回测方向准确率、AUC、模拟持仓策略并重点排查预测滞后问题。下面按这个顺序把每个环节里我实际踩过的坑和有效做法一一说出来。2. 拉数据与清洗复权、时区和停牌才是第一道坎2.1 字段里最容易被忽略的是复权价格用雅虎财经的数据接口拉数据时最基础的代码长这样import yfinance as yf df yf.download( XXX, # 这里填股票代码 start2015-01-01, end2025-01-01, auto_adjustTrue, ) print(df.columns)很多人拿到数据以后只看Close列忽略了一个致命细节当上市公司分红或者拆股时股价会出现一个不自然的跳空缺口。如果没有做复权处理模型会把这个因为分红引起的价格跳空当成一次真实的暴跌或者暴涨特征序列里就会长出一堆假信号。常见的做法是打开自动复权参数这样拿到的Close实际上是“后复权/前复权处理过的价格”或者单独使用数据里的复权收盘价列。我强烈建议在任何历史回测里面都使用复权价格否则股息除权日前后模型的预测错误率会高得离谱而且这类错误往往很隐蔽因为累计收益曲线看起来只是多了一两次小回撤。2.2 时区问题别让“今天”的数据跑进昨天的窗口美股交易是美东时区北京时间跟它有12到13个小时的时差。雅虎财经返回的日线数据默认是美东日历日对齐的也就是说每个交易日只产出一根K线这一根K线代表的是美东当天完整交易时段。这里有个隐藏的坑如果你习惯在盘前用“当天”数据做预测拿到的可能是一个未收盘的不完整序列而如果你把它当成已收盘数据来训练特征窗口和标签之间就等于混入了未来信息。从实践角度最稳妥的做法是训练和预测统一使用“收盘后”的数据时间戳并且明确把特征序列截断在交易日T标签对应TN的未来收益。不要使用盘中实时数据去套用收盘模型也不要因为时差把当前这根不完整的K线纳入特征。这个问题在构建任何真实策略时都很致命但在不少教程里都被一笔带过。2.3 停牌、长假和缺失值的处理方案美股虽然交易日密集但依然有假日休市和各种突发停牌。如果直接用数据接口返回的结果可能某一天就是凭空消失的而不是空值。这会带来两个问题一是LSTM的样本必须是连续时间步中间缺一天序列结构就断了二是如果使用重采样把非交易日强行补进去等于塞入大量无意义的填充数据。我习惯的做法是这样的先拉取目标时段的完整交易日列表用这个列表去做数据对齐缺失的交易日直接剔除保留连续的交易日序列如果数据里出现NaN再细分处理缺失场景推荐处理方式原因单日停牌导致个别字段缺失前向填充并增加一个“是否缺失”标记特征保持序列连续同时让模型知道当天没有成交连续多日无交易数据剔除或单独标记不做后向填充后向填充等于偷看未来信息指标计算时产生的开头缺失直接删除这些不完整窗口窗口数量足够不值得冒险补值补上“是否缺失”这个标记特征是我觉得很有用的小技巧。这样一来LSTM能主动学习“某天没有交易”这个事件本身的影响力而不是单纯把它当成一个噪声。3. 特征构造把K线变成LSTM真正看得懂的信号3.1 第一层从原始价格到收益率序列原始价格序列有一个天然缺陷非平稳。十年前某股票的价格是5美元现在是200美元模型如果直接在价格数字上学规律就需要同时适应完全不同的数值量级还得额外处理长期通胀和公司成长带来的漂移。一个更自然的选择是把价格转换成收益率序列也就是每天的变化比例或者取对数差。收益序列在统计性质上平稳得多也更加接近金融资产交易的本质——我们赚的本来就不是“价格数字”而是“价格变化”。我实际使用的收益率有两类单日收益率ret_1 close.pct_change()描述隔夜到当日的边际变化。多周期动量ret_5 close.pct_change(5)、ret_20 close.pct_change(20)分别代表一周和一个月区间的累计涨跌幅。多周期收益率的意义在于给模型提供了不同时间尺度的“动量状态”日线级别的短期趋势往往噪声大而20日收益能反映更稳定的中期方向。LSTM虽然有记忆能力但让模型自己从原始价格里硬学出“20日动量”这个概念需要相当多的数据和参数直接作为特征喂进去等于把人类已有的金融常识预置给模型训练负担小得多效果也更稳。3.2 第二层滚动统计特征与技术指标第二层特征是我花时间最多的地方也是这个项目里“时序特征处理”的核心部分。光有收益率还不够市场状态不仅体现在涨跌幅本身还体现在波动率、价格所处区间、短期动能是否衰竭这些维度。我常用的是下面这几类滚动波动率rolling_std ret_1.rolling(20).std()。金融数据有明显的波动聚集现象高波动后面往往还是高波动低波动后面往往还是低波动。这个特征对判断“当前市场是否处于剧烈分化期”非常有效。均线偏离度(close - close.rolling(20).mean()) / close.rolling(20).mean()。价格偏离中期均线的程度可以近似表示短期情绪是不是过热或者过度悲观。布林带位置把价格在布林带中的相对位置算出来即(close - 下轨) / (上轨 - 下轨)。这个指标本质上是把波动率和趋势融合在一起数值在0到1之间可以直观表达“价格处在统计区间的什么位置”。RSI类动能指标经典的相对强弱指标可以自己用滚动平均实现用来刻画一段时间内上涨力量和下跌力量的对比。我通常取14日窗口。这些特征不需要引入额外的金融数据库纯pandas就能算完。关键是理解它们背后的直觉LSTM本质上是一个函数逼近器你喂给它的特征里如果已经包含了“波动率聚集”“价格偏离均值”这些被市场反复验证的规律它学起来会比从零开始摸索高效得多。3.3 第三层相对强弱与周期编码这个项目里还有一个很容易被忽略的维度个股和市场整体的关系。在系统性行情里大盘涨的时候很多股票跟着涨大盘跌的时候再好的个股也难独善其身。所以我会把标的的收益率和同期大盘指数的收益率放在一起构造一个相对收益特征相对收益ret_1 - 大盘ret_1。这个特征过滤掉市场整体的beta成分留下股票自己跑赢或跑输大盘的部分。它对捕捉个股的alpha很有帮助。除此之外还有日历效应。虽然很多人觉得“星期一效应”已经消失了但作为特征星期几和月份这些信息几乎没有成本。我推荐用周期性编码而不是直接填整数比如把星期几映射成两个周期函数sin(2 * pi * weekday / 7)和cos(2 * pi * weekday / 7)。这样设计的原因很简单星期一是1星期日是0如果用整数编码模型会莫名其妙学到“数字越大越怎么样”的无意义顺序而用sin/cos编码星期一和星期日之间的距离在数值上又很接近符合“周末相邻”的真实含义。3.4 特征工程的减法原则与泄漏边界我见过不少项目把特征堆到三四十个结果训练集表现尚可测试集直接崩盘。核心原因之一就是特征太多、太冗余LSTM的参数量会被迫膨胀过拟合风险急速上升。以日线级别的单标的预测来说我个人经验是15到20个特征已经非常充裕关键是特征之间的互补性和信息增量而不是数量。还有一个所有做时序特征的人都必须警惕的雷区滚动特征计算时的未来信息泄漏。比如要计算当天的20日均线只能用截止到当天收盘的数据也就是必须用rolling(20).mean().shift(0)——这个没问题但如果操作不当用了包含未来几天数据的窗口去算指标模型就等于在训练时偷看了“未来的收盘价”测试时自然被打回原形。注意凡是涉及滚动窗口的特征一定要在心里画一条时间轴反复确认“当前时刻T这个特征值是否只依赖T以及T以前的真实数据”。别让任何未来数据偷偷爬进你的训练特征。4. 标签设计与数据切分时序泄漏比模型错误更容易毁掉结果4.1 回归连续价格还是分类涨跌方向刚开始做这个项目时我试过直接预测“下一天的收盘价”损失函数用MSE。结果是训练过程看起来很顺畅预测曲线跟真实价格曲线简直贴在一起准确率感人。但我后来意识到这完全是错觉价格序列的自相关极强模型只要把预测值做到“非常接近昨天的价格”MSE就已经很小了。这本质上是在做一个复制昨天的任务而不是在预测未来。这也是我后来转而做“方向分类”的根本原因。更合理的标签不是连续价格而是未来h个交易日后的涨跌方向df[label] (df[close].shift(-h) df[close]).astype(int)h 1代表预测明天涨不涨h 5代表预测未来一周的方向是否向上。二分类是最简洁的设计也可以扩展成三分类或五分类大涨、小涨、横盘、小跌、大跌但类别越多样本平衡和模型收敛就越难。我的建议是先从二分类入手把整个流程跑通再逐步增加标签粒度。4.2 训练集/验证集/测试集必须按时间顺序切分做过普通机器学习的人一开始很可能会习惯性调用随机切分函数这是时序预测项目里最毒的一个错误。随机切分会把2021年的样本和2023年的样本混在一起训练和验证造成一种“模型见过未来”的幻觉评估结果严重虚高。正确的做法是完全按时间顺序切分前70%作为训练集中间15%作为验证集用来调超参数和做早停最后15%作为测试集只允许最后评估一次。更严格一点可以采用walk-forward验证把窗口往前滚动重复“训练一段→验证预测下一段→把验证段纳入训练”的过程。这样能更真实地模拟模型在实战中的更新节奏。对于起步项目简单的时间比例切分已经能避免大部分自欺欺人的评估。4.3 三个最容易忽略的泄漏源数据泄漏在时序项目里是无孔不入的我至少真实翻车过三次总结下来最常见的三个来源第一归一化在完整数据集上fit。合理的做法是只用训练集的统计量拟合标准化器然后对验证集和测试集只做transform。如果对整个数据集求最小值和最大值再做归一化测试集的数值范围信息就提前混进了训练过程。第二特征里混入了未来数据。前面提到滚动窗口计算时如果没用对偏移量“未来几天的价格”就会进到当天的特征里。这个坑往往非常隐蔽因为你只看单行数据根本发现不了必须写代码去抽样核查特征和时间标签的对应关系。第三缺失值填充用了后向填充。例如某一天停牌为了“让序列连续”而用后一天的价格来填空这就是实打实的未来数据泄漏。正确的做法是用前向填充或者干脆删除该样本。这三件事每一项都是“看起来没什么大不了实际上能把结果从50%抬到60%甚至更高”的隐患。评估模型之前先把它们全部排查干净。5. LSTM模型搭建窗口、门控和训练配置的组合逻辑5.1 为什么LSTM在这里确实能学到东西回到最核心的问题特征和标签都准备好了为什么用LSTM而不是普通全连接神经网络答案在于金融时序的“状态依赖”特性。今天的市场行为往往跟过去几个星期甚至几个月的上下文有关——比如一个连续上涨后突然放量下跌的形态和一个长期横盘后的放量突破背后代表的状态完全不同。如果只看今天的特征向量MLP根本区分不了这两种情况。LSTM的核心是引入了细胞状态和三个门控遗忘门、输入门、输出门。它可以在长序列中决定哪些旧信息需要保留哪些新信息需要写入。这就意味着模型有机会自己学会“注意过去的波动率状态”“记住过去一段时间的动量”而不是把每个时间步孤立地看待。从工程角度说LSTM在具有长期依赖的多维时间序列上确实比MLP和普通RNN都更可靠。5.2 样本窗口与三维输入的数据结构LSTM的输入是三维张量(样本数量, 时间步数, 特征数量)。也就是说模型不只看“今天”的特征还要看过去连续T天的特征组合。这个T就是滑动窗口长度。窗口长度的选择需要平衡两个方向太短的窗口比如5天学不到中期背景太长的窗口比如120天一方面会让样本数量大幅减少另一方面会把太遥远的、跟当前关系不大的噪声也装进来。以日线数据为例我实际操作下来感觉30到60个交易日是比较合理的区间既能覆盖一个半月的市场背景又不至于让序列过于笨重。构造样本的典型代码这样写import numpy as np def make_sequences(features, labels, window40): X, y [], [] for i in range(len(features) - window): X.append(features[i : i window]) y.append(labels[i window]) return np.array(X), np.array(y)注意这里的对齐逻辑第i个样本的输入是i到i window - 1的数据对应的标签是i window那一天的未来涨跌方向。这种错位是刻意为之的保证模型在预测时“看不到”自己需要预测的那一天。5.3 归一化、网络结构与损失函数的选择特征进入LSTM之前必须先做归一化否则不同特征之间的数值尺度差异比如价格是几百收益率只有0.01会让门控计算失衡。我通常对每个特征列分别做MinMax归一化而且严格限定在训练集上拟合。网络结构方面我踩过很多次坑之后形成了一个感觉比较稳的模板import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.classifier nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1), ) def forward(self, x): out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步的输出 return self.classifier(out)这是一个两层的LSTM加一个轻量分类头。hidden_size取64在日线场景下通常是够用的增大到128未必带来收益反而会明显拖慢训练并增加过拟合风险。最后一层用sigmoid输出配合二元交叉熵损失。优化器用Adam初始学习率1e-3附近比较合适。5.4 训练时容易遇到的三个实际问题训练LSTM的过程里面有几个小问题几乎人人都会遇到提前说清楚能省很多精神内耗。一是随机种子问题。同一个模型配置不同随机种子跑出来的准确率可能波动两三个百分点。这不是代码写错了是深度学习在小数据集上的正常现象。应对方式是固定所有随机种子同时对一个配置跑3到5个不同种子取平均结果再下结论。二是类别不平衡问题。虽然涨跌天数通常接近五五开但特定行情阶段可能出现明显的偏斜。如果发现训练集里上涨样本占60%以上可以在损失函数里给少数类加上比重大于1的权重或者用分层抽样来构造训练批次。三是loss在初期纹丝不动的情况。如果训练了十几个epochloss曲线几乎水平先不要急着加大学习率。先检查数据是不是有问题比如标签列是不是全是同一个值、特征是不是全为NaN、归一化是不是做反了。这些基础问题排查完再考虑调整模型结构。6. 评测与回测别让LSTM的“滞后抄作业”骗了你6.1 预测滞后现象的识别与根因用LSTM做股票方向预测时最容易出现的一种“虚假成功”就是预测结果比真实涨跌滞后一拍。举个例子假如把模型预测的概率值和真实累计收益画在一起很多人会看到两条曲线形状高度类似只是预测曲线整体晚了一两天。这时候如果只看相关性会觉得模型很厉害但实际上模型学到的是“今天的涨跌和明天的涨跌高度相关”这个简单的惯性规律本质上只是在复制当天的收益方向。怎么识别这个问题一个简单有效的办法是看模型在“反转日”的表现假如今天是上涨日模型给出的明天上涨概率是多少假如今天是下跌日模型给出的明天上涨概率又是多少。如果模型对这两类情况的预测几乎完全相反而且准确率主要来自顺延行情基本可以断定它陷入了滞后抄作业。缓解滞后现象的思路有三个一是把预测标签改为未来5日方向强迫模型不能只靠今天推断明天二是在特征中增加更多与当日收益方向正交的信息比如量价关系、波动率变化三是增大dropout降低模型对单日自相关模式的依赖。从我的经验看改标签周期效果最明显代价是训练难度上升。6.2 用方向准确率、AUC和策略回测一起评估模型的最终价值不应该是“图好看”而应该是能不能在实际策略里产生正收益。我建立了一套三层评估体系第一层看分类指标。方向准确率超过55%在日线预测模型里已经算可圈可点同时要关注AUCAUC能更好地衡量模型在不同阈值下的区分能力。第二层看预测概率的累计分布模型给出的上涨概率如果集中在0.4到0.6之间说明模型整体很犹豫信号价值有限如果能相对分散说明模型在不同市场状态下有区分度。第三层也是最关键的一层做策略回测。一个最简单的回测方案是这样当预测上涨概率超过0.55时持有多头仓位当低于0.45时空仓或做空每天根据信号调仓。计算累计收益时要考虑手续费和滑点# signal: 1做多, -1做空, 0空仓 strategy_daily_return signal.shift(1) * asset_daily_return strategy_equity (1 strategy_daily_return.fillna(0)).cumprod() benchmark_equity (1 asset_daily_return.fillna(0)).cumprod()信号用shift(1)是因为当天的预测信号最早也只能在下一个交易日执行这个处理能避免前视偏差。对比策略净值和“买入持有”基准的差距才能真正说明模型预测有没有转化为赚钱能力。6.3 我实测下来最值得优化的三个方向做完整个项目之后如果让我重新做一遍我会把时间优先投入这三个方向第一从单标的预测转向多标的联合训练。把同一时间段内多只股票的数据放在一起训练能显著提升模型的泛化能力。不同股票之间共享的交易模式和波动特征相当于天然的“数据增强”而且模型还能学到一些跨标的的相对现象。第二不要迷信LSTM本身把特征验证放在调参之前。每新增一个特征先在同样的模型配置下回测对比如果策略收益没有提升就果断拿掉。这个流程比盲目堆叠网络层数有意义得多。第三多试几个标签周期和阈值策略。预测未来1日、5日、10日的信号规律往往完全不同不要默认某一种就一定更好。用回测结果反推标签设计是金融时序预测里非常实用的一招。做了一圈实验下来我最大的感受是在这个项目里数据意识和时序敬畏感比任何花哨的模型结构都重要。只要训练特征里混入了一点点未来信息再好的模型也会给你一个漂亮的假象而只有老老实实地把特征、标签、切分和评估每一环都做好LSTM的价值才真正显现出来。顺带分享一个我自己的小习惯每次跑完一组实验我都会写一行代码把预测结果画成散点图标出连续三日以上的预测正确区间——图上看一眼模型是不是只会顺着昨天的方向说废话比看一百行指标报表都直观。这个技巧看着简单真的能帮你避开很多自我感动。