ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

tushare + TensorFlow 2 实战:LSTM预测股票开盘价全流程

tushare + TensorFlow 2 实战:LSTM预测股票开盘价全流程 简介围绕贵州茅台历史行情数据提供了一套基于tushare与TensorFlow 2.0的股票开盘价预测示例。面向希望入门金融时序建模、熟悉RNN和LSTM应用的Python开发者清晰演示了从数据接口调用、数据预处理到模型训练与评估的完整链路。资源包内含5个文件包含3个Python脚本分别对应数据下载、RNN预测、LSTM预测、1个CSV格式的茅台历史数据以及1个Markdown说明文档整体体积仅56KB文件结构紧凑便于快速阅读和复用。目前已有1912人学习下载适合作为课程设计或个人实践的参考模板。通过配套代码与说明读者可以理解日期编码、开盘价归一化、Adam优化器及均方误差等关键细节并直接基于已有数据复现预测流程进一步体会循环神经网络与长短期记忆网络在时间序列上的效果差异同时也能认识到此类预测在应对市场情绪和突发事件等方面的局限性。1. 从贵州茅台日线到开盘价预测tushare TensorFlow 2 到底能不能落地如果你打开 tushare 看了几天数据又照着网上的 LSTM 股票预测教程敲了一遍你会发现一个尴尬的事实验证集上的 RMSE 很好看但拿最近三个月的行情一推预测的开盘价几乎就是「昨天收盘价」的翻版。这个标题想做的事是用 tushare 拉取贵州茅台的历史行情再用 TensorFlow 2 搭 RNN 和 LSTM 去预测下一个交易日的开盘价。适合的人群很明确刚学完深度学习、想用真实金融数据练手的初学者以及想验证时序模型在单只股票上到底有几分成色的开发者和量化研究者。它不是一套能直接实盘的策略而是一个把数据、预处理、建模、评估串起来的最小闭环。读完你会清楚每个环节的坑在哪、参数怎么调以及为什么你的模型经常「看似很准实则没用」。2. 用 tushare 拉取贵州茅台历史行情接口选型、字段说明与数据清洗2.1 注册 token 与两种调用方式pro 接口为什么是唯一选择tushare 是中文社区里最容易上手的财经数据接口尤其适合做股票历史行情下载。早年教程流行用ts.get_k_data()这个老接口不用积分直接按代码取日线但现在数据稳定性和字段完整性都跟不上很多代码已经跑不动。新版的tushare.pro需要注册账号、登录官网拿到专属 token然后在本地用ts.set_token()初始化。token 相当于访问钥匙直接写在脚本里并不安全常见做法是存到环境变量里用的时候再读出来。import os import tushare as ts import pandas as pd token os.environ.get(TUSHARE_TOKEN, 你的token) ts.set_token(token) pro ts.pro_api() df pro.daily(ts_code600519.SH, start_date20180101, end_date20231231) df.sort_values(trade_date, inplaceTrue) df.reset_index(dropTrue, inplaceTrue) print(df.head())这段代码里pro.daily是取日线行情的核心方法。ts_code指定股票代码贵州茅台在上交所所以写成600519.SH交易所后缀不能丢丢了会直接报参数错误。start_date和end_date用八位字符串tushare 对日期格式要求很严写成带横杠的2018-01-01会返回错误。返回的数据默认按日期倒序所以用sort_values转成时间正序再重置索引方便后面做滑窗。打印出来之后你应该能看到至少包含trade_date、open、high、low、close、pre_close、vol等列这就是后续建模的全部原料。2.2 复权问题的处理预测开盘价到底要不要复权pro.daily返回的是未复权价格也就是每个交易日实际撮合出来的价格。茅台这类股票分红送股不算频繁但把时间跨度拉到五年十年同样会遇到除权除息。未复权价格在除权日会出现一个肉眼可见的向下跳空模型会把这个跳空当成真实的暴跌去学习从而给预测带来额外噪声。处理方式有两种一是用pro.adj_factor拿复权因子自己算前复权价二是直接用 tushare 封装好的复权函数。我习惯手动算因为可以清楚看到每一步在做什么。adj pro.adj_factor(ts_code600519.SH, start_date20180101, end_date20231231) df df.merge(adj[[trade_date, adj_factor]], ontrade_date, howleft) base_factor df[adj_factor].iloc[-1] df[adj_open] df[open] * df[adj_factor] / base_factor df[adj_high] df[high] * df[adj_factor] / base_factor df[adj_low] df[low] * df[adj_factor] / base_factor df[adj_close] df[close] * df[adj_factor] / base_factor前复权的思想是「以最新一天为基准把历史价格按复权因子比例缩小」。如果某天除权复权因子会发生跳变调整后的价格序列连续趋势不会被破坏。注意这里保留原始的open、close列后续构造特征时只使用adj_*列避免原始价与复权价混用。如果你只做几个月的短期练手不复权影响不大但跨年数据一定要复权否则测试集上遇到除权日模型会预测出一个“假大跌”误差直接爆表。另一个细节是vol不需要复权但除权日当天的成交量会受到市场情绪影响通常把它当作原始量使用。2.3 数据清洗与特征列的取舍缺失值、空值与开盘价目标日线数据按交易日返回一般不会整行缺失但合并复权因子后个别停牌日的复权因子可能缺失产生 NaN。最简单的清洗方法是把有空值的行直接删掉。然后考虑特征列的取舍预测目标是下一交易日的开盘价所以特征只能用已经发生的数据。我常用的组合是前复权收盘价、最高价、最低价、成交量vol这四列在预测开盘价时信息量最大。有人喜欢把amount成交额也加进来可以试但特征太多会加大 LSTM 过拟合的风险。df.dropna(subset[adj_open, adj_high, adj_low, adj_close], inplaceTrue) df df.reset_index(dropTrue) features df[[adj_close, adj_high, adj_low, vol]].values opens df[adj_open].values print(features.shape, opens.shape)这里刻意没有加入pre_close。原因是pre_close是“昨天收盘价”它和今天开盘价之间只差一个跳空模型如果学到「开盘价约等于 pre_close 加残差」本质上就是在复制最近值而不是从窗口形态中提取信息。我们希望模型看到过去 20 天的整体走势而不是依赖一个太强的“最近值”捷径。另外vol的量级和价格完全不同价格是元成交量是手差值很大所以后面归一化是必须的。如果你在features里混入了trade_date字符串后续 numpy 数组会变成 object 类型LSTM 直接报错这也是经常遇到的一个坑。2.4 训练集和测试集怎么切时间序列不能随机打乱股票数据最大的特点是「顺序即信息」。如果用train_test_split随机打乱模型会把未来某一天的数据混进训练集测试时就等于开了天眼。正确做法是按时间顺序切分前 80% 当训练后 20% 当测试。切分位置也要考虑滑窗长度窗口是 20 天时测试集内部第一个样本会使用切分点开始的 20 天数据所以切分点后面必须还有足够多的天数来构造样本。split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].reset_index(dropTrue) test_df df.iloc[split_idx:].reset_index(dropTrue) train_features train_df[[adj_close, adj_high, adj_low, vol]].values train_opens train_df[adj_open].values test_features test_df[[adj_close, adj_high, adj_low, vol]].values test_opens test_df[adj_open].values注意这里切分的对象是 DataFrame而不是已经做过窗口的数组。原因是 MinMaxScaler 的 fit 必须基于原始时间段的特征分布窗口构造不能发生在 fit 之前否则切分边界会变得非常混乱。切分完成后train_opens和test_opens都是原始价格后面在构造序列样本时再决定如何配对。还有一点容易被忽略如果总样本数只有 40080% 切分后训练集 320、测试集 80窗口是 20 时训练样本数大约是 300 个测试样本数大约是 60 个这样的量级对 LSTM 来说其实很小过拟合风险很高后面要通过早停和 Dropout 来控制。3. 把行情数据变成 LSTM 能吃的时间序列样本窗口化、归一化与 tf.data3.1 为什么预测开盘价要用「过去 N 天」而不是全历史RNN 和 LSTM 理论上能处理任意长度的序列但 TensorFlow 的模型输入必须固定维度所以需要把连续的行情截成一段一段等长的窗口。窗口长度是一个超参数太短模型只能看到最近两三天的局部波动学不到趋势太长模型被十几年前的旧行情带偏参数多训练也慢。对日线数据我一般先试 20 天大约是一个自然月的交易天数能覆盖一次像样的回调或上涨又不会引入太多噪声。30 天和 60 天可以作为对比实验但记住窗口增长带来的计算量是线性增加的预测效果不一定更好。很多人把「用过去 N 天」理解成「用历史所有数据一步步喂给 RNN」这是不对的。RNN 在训练时依然需要切成多个固定长度的样本只是在每个样本内部按时间步展开。窗口之外的更早信息不会被当前样本看到所以 20 天窗口就是一个明确的时间视野。你甚至可以把它当成一个特征工程决策先试 20如果验证集误差不理想再试 30、60不要一上来就追求超大窗口。3.2 窗口函数与样本构造滑窗代码与索引细节构造样本时X 是特征窗口y 是窗口结束后的下一个交易日开盘价。例如窗口为 20则第 0 到第 19 天的close/high/low/vol作为输入预测第 20 天的open。这样预测就是「用过去一个月预测明天」而不是「用今天预测今天」。这个索引关系是整个项目最容易翻车的地方必须反复检查。import numpy as np def make_sequences(features, opens, window20): X, y [], [] for i in range(len(features) - window): X.append(features[i:iwindow]) y.append(opens[iwindow]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)这段代码最关键的是opens[iwindow]。features[i:iwindow]取的是第 i 天到第 iwindow-1 天opens[iwindow]取的是第 iwindow 天也就是窗口结束后的那一天二者不会重叠。如果把 y 写成opens[iwindow-1]就变成预测窗口最后一天的开盘价而这一天的特征已经在 X 里了直接构成作弊。dtypenp.float32是给 TensorFlow 统一输入类型不然后面可能报TypeError。调用时分别对训练集和测试集构造WINDOW 20 X_train, y_train make_sequences(train_features, train_opens, WINDOW) X_test, y_test make_sequences(test_features, test_opens, WINDOW) print(X_train.shape, y_train.shape)如果打印出来是(样本数, 20, 4)和(样本数,)说明窗口和特征数都对。如果中间报ValueError: setting an array element with a sequence多半是features里混入了非数值列检查一下是否把trade_date也放进了矩阵。3.3 归一化MinMaxScaler 的坑与 fit/transform 的先后股票价格从几百到几千成交量从几万到几亿量级差异会让梯度下降震荡必须归一化。常见做法是 MinMaxScaler把数据压到 [0,1] 区间。但这里有一个必须强调的顺序先切分再 fit。如果先对整个数据 fit测试集的 min/max 就会混进 scaler属于典型的未来信息泄露。正确顺序是拿训练集特征 fit然后用同一个 scaler 去 transform 训练集和测试集。from sklearn.preprocessing import MinMaxScaler scaler_x MinMaxScaler(feature_range(0, 1)) scaler_x.fit(train_features) train_features_scaled scaler_x.transform(train_features) test_features_scaled scaler_x.transform(test_features) scaler_y MinMaxScaler(feature_range(0, 1)) scaler_y.fit(train_opens.reshape(-1, 1)) train_opens_scaled scaler_y.transform(train_opens.reshape(-1, 1)).ravel() test_opens_scaled scaler_y.transform(test_opens.reshape(-1, 1)).ravel()这里对特征和目标分别用了两个 scaler。为什么目标需要单独缩放因为 LSTM 的输出层是线性激活直接预测几千块的价格很难收敛缩放后输出范围在 [0,1] 附近训练更稳定。如果目标和特征放在一起缩放预测后逆变换时必须重新构造一个和原始特征一样宽的数组非常麻烦。分开缩放后scaler_y.inverse_transform可以直接把单个预测列还原成价格。注意reshape(-1,1)是 sklearn 对一维数组的强制要求。测试集只调用transform绝不调用fit这条规则我会在每行代码的注释里反复强调。3.4 TensorFlow 2 的数据管道tf.data 的 shuffle、batch、prefetch构造好X_train/y_train之后下一步把它们喂给模型。直接用model.fit(X_train, y_train)也能跑但 TensorFlow 2 更推荐用tf.data.Dataset它能把 batch、shuffle、prefetch 自动串起来减少训练时 CPU 到 GPU 的数据传输瓶颈。注意 shuffle 的对象是训练集测试集绝对不能 shuffle否则预测顺序和真实顺序对不上。import tensorflow as tf train_dataset tf.data.Dataset.from_tensor_slices((X_train, train_opens_scaled)) test_dataset tf.data.Dataset.from_tensor_slices((X_test, test_opens_scaled)) train_dataset train_dataset.shuffle(buffer_size1000).batch(64).prefetch(tf.data.AUTOTUNE) test_dataset test_dataset.batch(64).prefetch(tf.data.AUTOTUNE)batch(64)表示每 64 个样本做一次参数更新。如果样本总数只有 500batch 开 64 会导致每个 epoch 只有 8 次更新可以降到 32。shuffle(buffer_size1000)的缓冲池不需要超过样本总数几百到一千就够太大会让每个 batch 里出现时间跨度极大的样本反而破坏序列内部的相对顺序。prefetch(tf.data.AUTOTUNE)让数据准备和模型训练并行能减少 GPU 空转。这里还要注意from_tensor_slices要求 X 和 y 的第一个维度长度相同如果你在 3.2 少构造了一个样本这里就会报 shape mismatch。训练时model.fit会直接消费这个 datasetfit 里不要再写batch_size否则会冲突。4. 用 TensorFlow 2 实现 RNN 和 LSTM 预测开盘价两套模型代码与对比评估4.1 SimpleRNN 循环神经网络为什么在股票序列上容易翻车SimpleRNN 是最朴素的循环神经网络它把当前输入和上一时间步的隐状态拼在一起经过一个 tanh 非线性变换得到当前步的隐状态。由于每一时间步都乘同一个权重矩阵梯度在反向传播时连乘序列一长梯度不是指数消失就是指数爆炸。在贵州茅台的日线数据上窗口 20 天虽然不算长但股价序列包含大量噪声和跳空SimpleRNN 很难构建稳定的长期记忆最终常会学到退化解——预测值始终在近期收盘价附近抖动。它适合作为整个流程的基线模型用来验证数据管道和训练流程是否通畅但不要指望它有什么惊艳表现。对从「rnn循环神经网络」关键词进来的读者来说需要明确一点RNN 在短序列任务里可以跑通但股票时间序列预测里更常见的是 LSTM。SimpleRNN 代码简单、参数少、跑得快但梯度问题是硬伤。如果你发现 SimpleRNN 的验证 loss 一直下不去不一定是你数据有问题很可能是模型本身的表达力不足。这时可以先跑通流程再切到 LSTM 看效果差别。4.2 最小可运行的 RNN 模型代码与训练from tensorflow.keras.layers import SimpleRNN, Dense, Dropout, LSTM WINDOW 20 n_features X_train.shape[2] rnn_model tf.keras.Sequential([ SimpleRNN(64, activationtanh, return_sequencesFalse, input_shape(WINDOW, n_features)), Dense(1) ]) rnn_model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmse, metrics[mae]) print(rnn_model.summary())SimpleRNN(64)表示隐层有 64 个单元。input_shape的(WINDOW, n_features)表示每个样本有 20 个时间步、4 个特征。return_sequencesFalse是默认值表示只输出最后一个时间步的隐状态形状为(None, 64)后面直接接 Dense 输出预测价。如果你把return_sequences改成 True输出形状会变成(None, 20, 64)Dense 层无法直接处理三维输入必须先展平或者再接一层循环层。rnn_history rnn_model.fit(train_dataset, epochs50, validation_datatest_dataset)训练时epochs50对几百个样本来说足够。观察 loss 曲线如果训练 loss 下降但验证 loss 在高位震荡说明模型没有泛化能力先检查数据泄露再考虑换 LSTM。RNN 在这里的定位就是把流程跑通不要在它身上调太多超参数否则就是玄学调参。RNN 的启动学习率用1e-3是安全的Adam 本身能自适应但如果数据没有归一化这个学习率也会翻车。4.3 换用 LSTM 模型门控机制、return_sequences 与 DropoutLSTM 在 SimpleRNN 的基础上增加了输入门、遗忘门和输出门让模型可以有选择地保留历史信息。对开盘价预测来说这意味着它能记住关键几天的价格形态同时忘掉无关噪声。LSTM 的参数量约为 SimpleRNN 的四倍更容易过拟合所以需要 Dropout 或减小隐层单元。下面是一个典型的双层 LSTM 结构lstm_model tf.keras.Sequential([ LSTM(64, return_sequencesTrue, input_shape(WINDOW, n_features)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) lstm_model.compile(optimizertf.keras.optimizers.Adam(learning_rate5e-4), lossmse, metrics[mae])第一层LSTM(64, return_sequencesTrue)会输出每个时间步的隐状态形状是(None, 20, 64)这样第二层 LSTM 可以继续沿时间轴处理。第二层LSTM(32, return_sequencesFalse)只输出最后一个隐状态形状是(None, 32)再传给 Dense。层间的 Dropout 0.2 是一个保守值如果过拟合明显可以提高到 0.3。学习率用5e-4比 RNN 低一半因为 LSTM 参数多学习率太大会导致 loss 后期上下翻跳。这里没有用recurrent_dropout因为它会把训练拖慢很多Dropout 加在层间已经够用。lstm_history lstm_model.fit( train_dataset, epochs80, validation_datatest_dataset, callbacks[tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue)])EarlyStopping监控验证集 loss当连续 10 轮不下降时停止并自动恢复到验证集最好的那轮权重。这能有效避免最后几轮过拟合。如果你在数据泄露的情况下损失会一路走低早停也会失效所以早停不能代替数据检查它只是在数据干净的前提下拦住多余 epochs。4.4 训练评估RMSE、MAE 与涨跌方向准确率训练结束后评估环节才真正暴露模型成色。首先要做的是把预测值从归一化空间还原回真实价格。目标列用的是单独的 scaler所以逆变换很简单。from sklearn.metrics import mean_absolute_error, mean_squared_error def to_price(pred_scaled): return scaler_y.inverse_transform(pred_scaled.reshape(-1, 1)).ravel() rnn_pred_price to_price(rnn_model.predict(test_dataset)) lstm_pred_price to_price(lstm_model.predict(test_dataset)) true_price scaler_y.inverse_transform(test_opens_scaled.reshape(-1, 1)).ravel() mae_rnn mean_absolute_error(true_price, rnn_pred_price) mae_lstm mean_absolute_error(true_price, lstm_pred_price) rmse_lstm np.sqrt(mean_squared_error(true_price, lstm_pred_price)) print(fRNN MAE: {mae_rnn:.2f}, LSTM MAE: {mae_lstm:.2f}, LSTM RMSE: {rmse_lstm:.2f})这里的test_dataset经过 batchmodel.predict返回的数组顺序与test_dataset一致因为测试集没有 shuffle。true_price使用test_opens_scaled而不是从 dataset 里取 y这样能保证索引完全对应。MAE 和 RMSE 反映误差的绝对值但股票预测更关心方向。方向准确率的逻辑是如果预测的开盘价比前一日实际开盘价高实际也比前一日高就算方向正确。def direction_acc(y_true, y_pred): return np.mean(np.sign(np.diff(y_true)) np.sign(np.diff(y_pred))) print(RNN Direction ACC:, direction_acc(true_price, rnn_pred_price)) print(LSTM Direction ACC:, direction_acc(true_price, lstm_pred_price))如果 LSTM 的方向准确率在 0.5 附近说明它没有比抛硬币强多少。这个指标对实盘决策更有意义因为价格预测偏差几块钱并不可怕方向判断才是策略的核心。建议把真实值、RNN 预测值、LSTM 预测值画在一张图里观察预测曲线是滞后还是漂移。画图时会发现很多模型的预测线比真实线滞后一天这正是模型学到了「今天收盘价 ≈ 明天开盘价」的表现。5. 避坑清单与常见问题排查从归一化泄漏到虚高结果5.1 数据泄露用全量 MinMaxScaler 的结果为什么好得离谱现象验证集和测试集的预测曲线几乎完全贴住真实线MAE 只有几毛钱你觉得自己已经找到了交易圣杯。原因在切分训练集和测试集之前对整个特征矩阵scaler.fit(features)。MinMaxScaler 会记录每个特征的全局最小值和最大值测试区间的价格范围已经包含在内。如果测试区间恰好创出新高归一化后的测试样本就隐含了未来信息——模型在训练时见过这个新高的“相对位置”预测自然准得离谱。这是一种隐蔽的数据泄露损失曲线看起来完全正常但换到新数据上立刻失效。解决严格执行「先切分再 fit」的流程。在 3.3 的代码里我已经先切了train_df和test_df然后只对train_features调scaler_x.fit测试集只做transform。目标列同理。建议你在每次训练前加一个断言检查训练集和测试集是否都使用了同一 scaler 的 transform 结果。这条血泪经验我强调多少遍都不嫌多因为很多公开教程就是用全量归一化展示了漂亮曲线误导了一批批初学者。5.2 特征里带了当天 open模型为什么会作弊现象训练 loss 和验证 loss 都低到近乎 0但把预测结果拿到下一周的真实行情一测完全偏离。原因构造样本时把adj_open放进了特征列。模型只需要在最后一个时间步直接读取这个值输出层线性映射一下就能复现目标。常见于使用 pandas 时没有做 shift或者直接df[[open, close, ...]]把开盘价也一并塞进矩阵。另一个隐蔽版本是把pre_close作为特征它和开盘价只差一个跳空同样接近「开卷考试」。我见过不少教程把预测目标定义成「当天的开盘价」同时特征里又有当天的 open然后展示出接近零的误差这是完全错误的实验设计。解决特征矩阵中只保留close/high/low/vol这些「窗口结束时已经发生」的数据目标取opens[iwindow]也就是窗口结束后的下一天。建议在训练前打印一下X_train[0, -1, :]和y_train[0]人工确认这两者不是同一天的 open。如果你用的是 pandas可以先构造一个df[[adj_open]].shift(-1)作为目标列再检查 shift 方向。把这一步做成固定习惯后我再没有在这个坑里翻过车。5.3 过拟合训练集胜率 90%测试集胜率 50%现象训练集方向准确率能做到 90%验证集和测试集直接掉到 50%预测曲线在测试集上几乎是一条水平线。原因LSTM 参数量大日线样本量可能只有几百到一千模型把训练集里的噪声当成规律背下来了。窗口越大、隐层节点越多过拟合越严重。还有一个容易被忽视的因素没有用早停让模型在训练后期继续逼近训练集导致泛化能力持续恶化。解决加 EarlyStopping 是最立竿见影的办法。在 4.3 的代码里我已经加了patience10和restore_best_weightsTrue。其次是降低模型容量把 LSTM 隐层从 64 降到 32Dropout 从 0.2 提到 0.3。窗口可以从 20 缩短到 10输入维度变小模型记忆难度增大只能学更粗粒度的趋势。还有一种验证方法每训练完一轮在测试集上打印一次方向准确率如果测试集准确率在训练后期反而下降说明正在过拟合。不要迷信越深的网络越好股票数据信噪比很低简单模型往往更靠谱。5.4 归一化时对 y 做了全量缩放逆变换后偏差现象预测价格反算回原始尺度后整体偏低或偏高 1%~2%但训练时 loss 看起来正常。原因对opens全量调scaler_y.fit切分后测试集的目标值在缩放时也参与了 min/max 的计算。如果训练集和测试集的价格区间不一致缩放后的测试集目标会被压缩或拉伸逆变换后自然出现系统性偏差。现实中 A 股价格长期上涨训练集是前几年低价段测试集是后面高价段这种偏差会更明显。有时候问题被当成模型欠拟合其实模型的输出本身没问题是缩放器的范围里混入了未来数据。解决目标列的 scaler 同样只 fit 训练集。即scaler_y.fit(train_opens.reshape(-1, 1))然后 transform 训练集和测试集。逆变换时用同一个scaler_y。另外评估时不要对缩放后的预测值直接算 MAE必须逆变换回原价格再算否则数字会小很多但不具备业务含义。一个简单验证方法把true_price和lstm_pred_price的标准差对比如果预测标准差明显小于真实标准差说明模型在收缩预测范围这往往是目标缩放或均值回归共同作用的结果。6. 进阶方向滚动回测、方向准确率与 baseline 验证静态的「训练一次测试一段」只能评估一个固定历史区间的表现但真实行情是滚动向前的。更贴近实战的做法是滚动回测从某个起点开始用过去WINDOW天的特征预测下一天开盘价然后把真实开盘价移入窗口继续预测下一天。这样得到的误差序列覆盖了连续多个交易日能更诚实地反映模型在不同行情段下的表现。一个最简单的滚动预测流程是先维护一个长度为WINDOW的历史特征矩阵每次用model.predict得到下一预测值等真实值出来后把真实值加入窗口尾部丢弃窗口头部最早的一行。注意这里一定用真实值不是预测值否则误差会不断累积方向准确率会失真。如果你想做更严格的评估还可以每个交易日都重新训练一次模型但那会非常慢实际中常做的是「每日增量更新」或者「每周重新训练」。另一个必做的对比实验是 baseline。用「昨日收盘价作为今日开盘价预测」跑一条曲线计算同样的 MAE 和方向准确率。如果 LSTM 在这两个指标上赢不了 baseline说明模型并没有从历史窗口中提取出超出惯性的信息。我每次跑模型前都会先跑这个 baseline它像一把尺子专门用来量那些「看着很准」的模型。方向准确率高于 50% 不一定说明模型有效但要低于 baseline 的方向准确率就真的没有再调参的必要了。最后分享一个我自己的教训第一次跑 LSTM 时看到验证集 MAE 只有 5 元兴奋了好几天后来发现那段时间茅台整体波动很小模型只要预测一个均值就能获得低误差。后来我把滚动回测和 baseline 一直挂在嘴边每次训练强迫自己看三张表baseline MAE、模型 MAE、方向准确率缺一不可。这个习惯帮我避免了很多自我感动希望帮到你。本文还有配套的精品资源点击获取
返回列表