☰
时序特征处理与LSTM:美股历史数据预测全流程复盘
2026/10/22 22:08:30 网站建设 项目流程

做时序预测的人应该都见过这种场面:把美股日线数据原样丢给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 项目完整流程速览

整个项目大致分成六个环节,环环相扣,任何一环偷懒都会在最终结果上现形:

  1. 数据获取与清洗:拉取日线数据,处理复权、时区、缺失值。
  2. 特征构造:从原始价格转向收益率、滚动统计、技术指标、相对强弱等特征。
  3. 标签设计:定义未来N日的涨跌方向标签。
  4. 训练集/验证集/测试集切分:严格按时间顺序,禁止随机打乱。
  5. LSTM模型搭建:窗口样本生成、归一化、网络结构设计和训练。
  6. 评测与回测:方向准确率、AUC、模拟持仓策略,并重点排查预测滞后问题。

下面按这个顺序,把每个环节里我实际踩过的坑和有效做法一一说出来。

2. 拉数据与清洗:复权、时区和停牌才是第一道坎

2.1 字段里最容易被忽略的是复权价格

用雅虎财经的数据接口拉数据时,最基础的代码长这样:

import yfinance as yf df = yf.download( "XXX", # 这里填股票代码 start="2015-01-01", end="2025-01-01", auto_adjust=True, ) print(df.columns)

很多人拿到数据以后只看Close列,忽略了一个致命细节:当上市公司分红或者拆股时,股价会出现一个不自然的跳空缺口。如果没有做复权处理,模型会把这个因为分红引起的价格跳空当成一次真实的暴跌或者暴涨,特征序列里就会长出一堆假信号。

常见的做法是打开自动复权参数,这样拿到的Close实际上是“后复权/前复权处理过的价格”,或者单独使用数据里的复权收盘价列。我强烈建议在任何历史回测里面都使用复权价格,否则股息除权日前后模型的预测错误率会高得离谱,而且这类错误往往很隐蔽,因为累计收益曲线看起来只是多了一两次小回撤。

2.2 时区问题:别让“今天”的数据跑进昨天的窗口

美股交易是美东时区,北京时间跟它有12到13个小时的时差。雅虎财经返回的日线数据,默认是美东日历日对齐的,也就是说每个交易日只产出一根K线,这一根K线代表的是美东当天完整交易时段。这里有个隐藏的坑:如果你习惯在盘前用“当天”数据做预测,拿到的可能是一个未收盘的不完整序列;而如果你把它当成已收盘数据来训练,特征窗口和标签之间就等于混入了未来信息。

从实践角度,最稳妥的做法是:训练和预测统一使用“收盘后”的数据时间戳,并且明确把特征序列截断在交易日T,标签对应T+N的未来收益。不要使用盘中实时数据去套用收盘模型,也不要因为时差把当前这根不完整的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, window=40): 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_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.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在初期纹丝不动的情况。如果训练了十几个epoch,loss曲线几乎水平,先不要急着加大学习率。先检查数据是不是有问题,比如标签列是不是全是同一个值、特征是不是全为NaN、归一化是不是做反了。这些基础问题排查完,再考虑调整模型结构。

6. 评测与回测:别让LSTM的“滞后抄作业”骗了你

6.1 预测滞后现象的识别与根因

用LSTM做股票方向预测时,最容易出现的一种“虚假成功”就是预测结果比真实涨跌滞后一拍。举个例子,假如把模型预测的概率值和真实累计收益画在一起,很多人会看到两条曲线形状高度类似,只是预测曲线整体晚了一两天。这时候如果只看相关性,会觉得模型很厉害;但实际上模型学到的是“今天的涨跌和明天的涨跌高度相关”这个简单的惯性规律,本质上只是在复制当天的收益方向。

怎么识别这个问题?一个简单有效的办法是看模型在“反转日”的表现:假如今天是上涨日,模型给出的明天上涨概率是多少;假如今天是下跌日,模型给出的明天上涨概率又是多少。如果模型对这两类情况的预测几乎完全相反,而且准确率主要来自顺延行情,基本可以断定它陷入了滞后抄作业。

缓解滞后现象的思路有三个:一是把预测标签改为未来5日方向,强迫模型不能只靠今天推断明天;二是在特征中增加更多与当日收益方向正交的信息,比如量价关系、波动率变化;三是增大dropout,降低模型对单日自相关模式的依赖。从我的经验看,改标签周期效果最明显,代价是训练难度上升。

6.2 用方向准确率、AUC和策略回测一起评估

模型的最终价值不应该是“图好看”,而应该是能不能在实际策略里产生正收益。我建立了一套三层评估体系:

第一层,看分类指标。方向准确率超过55%在日线预测模型里已经算可圈可点;同时要关注AUC,AUC能更好地衡量模型在不同阈值下的区分能力。第二层,看预测概率的累计分布:模型给出的上涨概率如果集中在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的价值才真正显现出来。顺带分享一个我自己的小习惯:每次跑完一组实验,我都会写一行代码把预测结果画成散点图,标出连续三日以上的预测正确区间——图上看一眼模型是不是只会顺着昨天的方向说废话,比看一百行指标报表都直观。这个技巧看着简单,真的能帮你避开很多自我感动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询