ARTICLE DETAIL

资讯详情

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

TensorFlow+CNN预测股票:从K线特征到滚动回测实战

TensorFlow+CNN预测股票:从K线特征到滚动回测实战 简介使用Python与TensorFlow构建CNN模型预测股票走势的完整实验资源面向股票量化研究初学者与深度学习实践者展示如何借助卷积网络从历史行情中提取特征弥补传统统计方法对复杂非线性关系捕捉不足的问题。压缩包共34个文件大小5.16MB以Python脚本、Jupyter Notebook为核心辅以PNG结果图、PDF学习提纲及股价数据集代码涵盖数据预处理、CNN模型搭建、训练与评估全流程还涉及DQN强化学习、KD指标与年度线等扩展实验。已有1440人学习下载适合希望将TensorFlow落地到金融场景的读者。通过研读脚本与notebook可以掌握滑动窗口构造样本、tf.keras.Sequential建模、损失函数与优化器配置等具体操作并借助可视化图表直观理解预测效果。需要注意的是股市具有高度不确定性项目更多用于教学与算法验证实际投资还需结合基本面与技术面分析。1. 用TensorFlow跑CNN预测股票走势先搞清楚它在预测什么把最近30个交易日的收盘价、成交量和几个衍生指标叠成一个窗口交给TensorFlow里的卷积神经网络去“看图猜未来”输出下一日上涨或下跌的概率——这就是用CNN预测股票走势最朴素、也最常被采用的落地形态。它不是一本万利的黑匣子而是一种把量价形态自动编码成特征的技术手段适合给人工决策做辅助信号也适合当作深度学习与量化交易的入门练手项目。很多人期待自己写一套python量化交易策略代码就能稳定盈利但真正做下来会发现金融时序信噪比极低模型靠不靠谱取决于数据怎么切、特征怎么构造、结果怎么验证。这篇会把从模型选型到滚动回测的整条链路讲透并交代那些反复出现的翻车细节。2. 为什么是CNN而不是LSTM卷积核在时间序列上究竟看见了什么一提到股票预测多数人的第一反应是LSTM毕竟那是处理时间序列的经典结构。但从实践结果看纯CNN在这类日线行情数据上往往收敛更快、训练更稳泛化也不差。原因不复杂K线形态的核心信息是“局部组合”比如连续三天的放量、价格对20日均线的偏离这些信息天然落在一个时间窗口内而CNN的卷积核恰恰就是干这个的。2.1 把K线当作一维图像Conv1D沿时间轴滑动的原理Conv1D的输入形状是(样本数, 时间步, 特征数)对应到我们的场景就是(样本数, 30个交易日, 5个特征)。卷积核是一个一维小窗口比如kernel_size5它在时间轴上滑动每次扫过连续5个交易日的5个特征做加权求和后过激活函数得到一个新的特征图。这个“扫过5天”的操作和图像卷积扫过5x5像素区域在原理上完全同类只不过图像有两个空间维度行情数据只有一个时间维度所以用Conv1D而不是Conv2D。这样设计背后的直觉是股票短期走势在很大程度上由最近一周左右的量价形态决定比如某天放量突破20日均线接下来几天是否延续这种信息的有效期通常在几天到两周。卷积核尺寸设成5相当于专门去识别“一周形态”设成4到6也都可以但奇数值在对称性上更好处理。我一般建议第一层卷积核设成5第二层设成3。两层堆叠之后感受野是53再减去重叠覆盖的部分大约覆盖13到14个交易日加上池化层刚好扣住一个月以内最重要的量价信息。如果一开始就把kernel设成30甚至更大模型很容易把远期无关波动也当成特征泛化能力反而下降。顺便说一下padding。代码里用了paddingsame目的是让卷积后的时间维长度和输入保持一致。如果不加padding卷积一次后时间步会从30变成26连续两层再加池化时间维缩水过快尾部的信息会被反复截断。对股票这种短序列保留长度对后续池化更友好。2.2 LSTM和CNN的取舍速度、梯度与依赖长度LSTM的优势在长依赖——理论上能捕捉几十天前的信息对当下的影响。但它是逐时间步递推的训练速度慢梯度传播路径长在小数据集上很容易过拟合。CNN可以高度并行在GPU上训练速度快一个量级梯度传播也更直接。对日线级别、几千个样本的小数据集来说LSTM的长处很难发挥出来短板却非常痛。对比项LSTMCNN长依赖建模强弱靠堆层扩大感受野训练速度慢逐步递推快可并行局部形态提取弱强可解释性差相对直观参数量大小不易过拟合如果确实需要长依赖常见做法是在CNN后面接LSTM或注意力层但我一般只在纯CNN效果明显不足时才这样组合。刚上手时纯CNN结构简单、参数少、训练快能更快验证“当前这套特征组合到底靠不靠谱”。等到确认特征有效再回头加复杂结构也不迟。2.3 先定任务再选网络分类还是回归这是一个容易被忽略的前置决策预测“涨/跌方向”和预测“收盘价”是两个完全不同的任务。分类任务用sigmoid输出加binary_crossentropy损失输出的是上涨概率回归任务用线性输出加MSE损失输出的是具体价格或涨跌幅。任务定义不同网络尾部的结构和评估方式都不同。实际做下来直接回归收盘价几乎没有稳定靠谱的预测因为价格近似随机游走叠加噪声MSE很难压到有意义的水位。我更建议新手直接从二分类开始预测下一交易日的涨跌方向用方向准确率accuracy评估。输出虽然只有涨跌两个类别但交易决策本身就是二分的做多或者不做这个映射关系非常直接。即使以后想做回归也建议把目标从“收盘价”换成“次日收益率”然后训练时辅以方向准确率作为参考指标而不是只盯MSE。提示预测涨跌时输出概率不一定非要用0.5当阈值。如果验证集上模型的预测概率整体偏向0.4到0.6可以按验证集的分布重新挑阈值比如0.55以上才看涨。我在做策略信号时一般会先打印预测概率的分布再决定阈值而不是无脑取0.5。3. 把K线转成CNN的输入张量特征构建与滑窗切分的完整代码数据准备是整个流程里最枯燥、但翻车率最高的一步。同样的网络结构数据切分和特征构造不同结果可能天差地别。常见做法是用pandas做特征工程再用numpy的循环构造滑窗最后把三维张量交给TensorFlow训练。下面从一份最普通的日线CSV开始字段至少要有date、open、high、low、close、volume。3.1 从日线数据构造特征收益率、均线偏离与量比import pandas as pd import numpy as np df pd.read_csv(stock_daily.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) df[ret] df[close].pct_change() df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[ma_bias] df[close] / df[ma20] - 1.0 df[vol_ratio] df[volume] / df[volume].rolling(20).mean() df[label] (df[close].shift(-1) df[close]).astype(int) feature_cols [ret, ma5, ma_bias, vol_ratio, close] df df.dropna().reset_index(dropTrue) print(df.head())这段代码里每列的含义需要说清楚ret是当日收益率ma_bias是收盘价相对20日均线的偏离度vol_ratio是当日成交量相对20日均量的倍率。这4个特征加上收盘价本身一共5个通道。label用shift(-1)构造——如果明天的收盘价比今天高label就是1否则是0。有人喜欢把开盘价、最高价、最低价全部塞进特征集实测效果并不好因为OHLC四个价格强相关卷积核在它们之间学到的几乎全是冗余组合。保留处理过的量价信息砍掉多余原始列模型泛化反而更好。这里的dropna()有两层作用一是清理rolling计算产生的前20行NaN二是丢掉因为shift(-1)造成的最后一行NaN。处理完的df才可以安全滑窗。数据源方面股票行情可以用akshare或tushare这类公共接口拉取也可以直接用导出的CSV。我一般只关心日线因为日线级别的样本量对CNN来说刚好够用分钟级数据样本变多但噪声也成倍放大不推荐新手一上来就碰。3.2 滑窗采样每个样本都是一段连贯的K线形态WINDOW 30 X, y [], [] for i in range(WINDOW, len(df) - 1): X.append(df[feature_cols].iloc[i - WINDOW:i].values) y.append(df[label].iloc[i]) X np.array(X) y np.array(y) print(X shape:, X.shape, y shape:, y.shape)滑窗大小WINDOW30表示用过去30个交易日的特征预测下一个交易日的涨跌。循环从索引30开始到len(df)-2结束X取的是i-30到i-1这个区间y取的是第i天的label。也就是说第i天收盘时窗口内的所有数据都是已知的而label是第i天到第i1天的涨跌时间上完全同步没有用到未来信息。循环结束后X的形状是(样本数, 30, 5)每个样本是一个30行5列的小矩阵这正好是Conv1D期望的输入格式。如果样本总数少于3000建议把WINDOW降到15到20否则样本量撑不起卷积层的参数量。也可以用滑动步长来控制样本数量步长为1时样本最多步长为2时样本减半适合数据量偏大或想降低相邻样本相关性的情况。3.3 按时间切分与归一化未来数据不能参与训练n_total len(X) n_train int(n_total * 0.7) X_train, X_test X[:n_train], X[n_train:] y_train, y_test y[:n_train], y[n_train:] from sklearn.preprocessing import StandardScaler scaler StandardScaler() ns, steps, feats X_train.shape X_train scaler.fit_transform(X_train.reshape(-1, feats)).reshape(ns, steps, feats) ns, steps, feats X_test.shape X_test scaler.transform(X_test.reshape(-1, feats)).reshape(ns, steps, feats)这里有三个容易踩的细节。第一切分必须按时间顺序前70%训练、后30%验证绝不能随机打乱一旦打乱训练集里混入未来的行情分布模型等于提前偷看了答案回测指标虚高。第二scaler只能在训练集上fit再用训练集的均值和标准差去transform测试集如果对全量数据fit等于把未来价格尺度泄漏给了模型。第三因为输入是三维张量必须先reshape成二维才能交给StandardScaler算完再还原成原来的(样本数, 时间步, 特征数)结构。提示判断有没有数据泄漏最简单的方法是检查样本构造顺序。X的最后一根K线时间戳必须不晚于y的决策时刻任何更晚的信息都不允许出现在样本里。4. 搭建CNN模型并完成训练网络骨架、参数配置与过拟合控制特征工程做完模型构建反而是最直白的部分。用TensorFlow的Keras APISequential顺序模型就够用。下面给出一个能直接跑通的最小骨架然后重点解释每个参数为什么这样设。4.1 网络结构两层一维卷积加全局池化import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(window30, n_features5): model models.Sequential([ layers.Input(shape(window, n_features)), layers.Conv1D(filters32, kernel_size5, activationrelu, paddingsame), layers.MaxPooling1D(pool_size2), layers.Conv1D(filters16, kernel_size3, activationrelu, paddingsame), layers.GlobalAveragePooling1D(), layers.Dense(32, activationrelu), layers.Dropout(0.3), layers.Dense(1, activationsigmoid), ]) return model model build_cnn() model.summary()先说结构。第一层Conv1D有32个卷积核kernel_size5扫过连续5个交易日MaxPooling1D把时间维减半增强平移不变性同时减少计算量第二层Conv1D降到16个卷积核kernel_size3接着GlobalAveragePooling1D把每个特征图压缩成一个数替代Flatten接全连接层。最后是Dense 32、Dropout 0.3、输出层sigmoid给出预测上涨概率。参数上filters不能贪多。行情数据样本撑死几千条卷积核越多、可学习参数越多越容易把噪声背下来。我一般把两层的filters控制在16到64之间超过64在小样本上几乎必定过拟合。kernel_size5和3的组合配上两层卷积感受野约13天对日线级别的短期形态足够。paddingsame保证卷积后时间维长度不变方便池化层均匀下采样。4.2 编译与训练Adam、早停和学习率衰减配合使用model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy], ) 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, validation_data(X_test, y_test), epochs100, batch_size64, callbacks[early_stop, reduce_lr], verbose1, )learning_rate用Adam默认的0.001起步在金融时序这种高噪声数据上如果loss曲线震荡先降到0.0005而不是换优化器。batch_size取64适合几千个样本的规模样本更少时可以降到32。epochs设100只是一个上限真正起控制作用的是EarlyStopping——它盯着val_loss连续15个epoch不下降就停同时restore_best_weightsTrue会把模型回滚到验证损失最低的那个位置。ReduceLROnPlateau也是盯val_loss连续5个epoch不降就让学习率减半把训练中后段的震荡压下来。fit返回的history对象里记录了每个epoch的loss和val_loss。我习惯训练完立刻画一条loss曲线肉眼检查训练loss和验证loss的gap是否在10个epoch内就开始拉大。如果是说明模型容量偏大优先削减filters而不是一味加正则。4.3 过拟合的识别信号与处理顺序CNN预测股票的过拟合和图像分类的表现类似但更容易被忽视因为金融指标的噪声本身就是随机的模型“背下来”之后在训练集上的表现可能漂亮得不真实。具体现象有三类训练accuracy逼近0.95而上验证accuracy在0.55附近横盘训练loss一路下降到0.1以下验证loss不再下降甚至反弹把验证集预测结果打印出来发现概率输出几乎集中在0.4到0.6之间模型缺乏自信很多样本都被判成“有一点涨的概率”。遇到这些信号调整顺序建议是先减少filters从32降到16再去掉一层卷积然后把Dropout从0.3加到0.4最后把WINDOW从30降到20。这四步做完还不行再回头检查特征和标签构造而不是继续堆层。金融时序信噪比极低模型规模越大越容易完美拟合历史而死在下一个阶段。5. CNN预测股票的典型翻车点四条踩坑记录与排查思路前四章把完整流程跑通了但真正让模型从“demo能用”到“实盘不敢用”的往往是下面几个反复出现的问题。每一条我都按现象、原因、解决来写方便直接对照排查。5.1 数据泄漏回测指标虚高的第一大来源现象训练集和测试集上的accuracy都在0.65以上模型看起来稳定但拿最近一个月的新数据去预测方向正确率掉回0.52接近随机。原因数据泄漏基本逃不出三种情况。一是标签构造用了未来信息比如shift(-2)错位或者用“未来5日涨幅”打标二是数据切分时做过随机打乱导致训练集里混入未来的价格分布三是归一化时对全量数据fit了StandardScaler让测试集的统计信息提前进入了训练。解决标签严格用当天收盘时能确定的条件构造预测第i1天的方向训练集和测试集按时间先后截断scaler只在训练集上fit。还有一个简单的自检方法打印每个样本里X的最后时间戳和y对应的日期凡是y日期早于X最后日期的全是泄漏样本直接删掉。5.2 类别不平衡模型学会“永远看涨”现象预测结果里95%都是1看涨测试集accuracy看着不低但交易时完全没有区分度模型什么都没学会。原因真实行情中上涨和下跌交易日的比例并不是五五开上涨天数略多。模型发现把所有样本预测成1accuracy也能拿到55甚至60于是选择了这条偷懒路径。神经网络对类别不平衡很敏感全预测多数类往往就是它在损失函数下的局部最优解。解决先打印混淆矩阵确认预测分布不是单边倒。然后给少数类加样本权重Keras里直接用class_weight参数比如class_weight{0: 1.2, 1: 0.9}或者对多数类做简单的欠采样。加权重会牺牲一点accuracy这是正常的——交易里真正重要的是预测分布有没有区分度而不是一个好看的总分。5.3 过拟合的典型曲线训练loss在降验证loss在涨现象训练到第5个epoch时loss已经到0.3验证loss反而从0.69涨到0.72到第15个epoch训练loss逼近0.1验证loss横在0.69附近不动。原因CNN的参数量大于有效样本量训练过程中模型逐渐把训练集的噪声逐条背下来导致在未见过数据上泛化停滞。行情数据本身信噪比极低这种背噪声的行为会表现得格外明显。解决按4.3节的顺序处理——先降filters再删层然后加Dropout。EarlyStopping的patience不要设太大10到15足够给模型太多训练空间只会让它更从容地过拟合。如果削完容量仍然过拟合回到第3章检查特征是不是太冗余、窗口是不是太长而不是继续堆模型。5.4 特征构造不当特征越多不代表效果越好现象把开盘价、最高价、最低价、收盘价、成交量全部作为原始特征送进去训练速度变慢验证集表现反而比只用5个处理过特征的模型更差。原因OHLC四个价格高度共线卷积核在它们之间学到的组合几乎全是冗余多余维度稀释了权重增大了模型容量最终放大过拟合。很多人偷懒把能拿到的原始数据一股脑塞进模型这是特征工程里最常见的误区。解决只保留能表达“变化”和“偏离”的特征比如收益率、均线偏离、量比而不是原始价格本身。每次改完特征组合做一次最简单的消融实验全特征跑一遍然后逐个删掉一个特征再跑一遍对比验证集accuracy。哪个特征删掉后效果几乎不变说明它在当前模型里就是噪声直接剔除。6. 滚动回测验证模型这样检验才有参考价值前面用的静态切分只能告诉你模型在同一个价格尺度下有没有过拟合但真实行情阶段会轮动——趋势、震荡、急跌波动率和价格水平一直在变。更可靠的验证方法是滚动回测用前一段时间训练、预测后一段时间然后把窗口向前滚动模拟多次“面对未来”的过程。def rolling_backtest(X_all, y_all, train_size2000, step250, epochs30): preds, truths [], [] start 0 while start train_size step len(X_all): end start train_size X_tr, y_tr X_all[start:end], y_all[start:end] X_te, y_te X_all[end:end step], y_all[end:end step] scaler StandardScaler() ns, n_steps, feats X_tr.shape X_tr scaler.fit_transform( X_tr.reshape(-1, feats)).reshape(ns, n_steps, feats) ns, n_steps, feats X_te.shape X_te scaler.transform( X_te.reshape(-1, feats)).reshape(ns, n_steps, feats) bt_model build_cnn() bt_model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.0005), lossbinary_crossentropy, metrics[accuracy], ) bt_model.fit(X_tr, y_tr, epochsepochs, batch_size64, verbose0) prob bt_model.predict(X_te, verbose0) preds.extend((prob 0.5).astype(int).flatten()) truths.extend(y_te) start end return np.array(preds), np.array(truths)train_size2000表示每次用最近2000个滑窗样本训练step250表示每次预测后面250个交易日然后整体滚动。关键点每个循环内部都要重新fit一个StandardScaler不能提前在全部数据上算好epochs固定为30不在回测里挂早停否则训练2000个样本反复评估早停条件会拖慢整个流程。如果总样本量不足3000可以把train_size降到总样本的60%但要保证训练集能覆盖至少一个完整的行情阶段。跑完回测统计总accuracy之前先跟两个基准对比。随机预测的accuracy约0.5类别均衡时“上一日方向”策略的accuracy等于日间涨跌的持续性通常在0.48到0.53之间。我的习惯是CNN的滚动accuracy至少要比“上一日方向”高出3个百分点才认为模型真的学到了形态否则基本可以判定它在拟合噪声这时优化空间不在网络结构而是回到特征和数据上。基准含义参考accuracy区间随机预测每次随机猜涨跌约0.50上一日方向昨天涨就猜今天涨0.48 ~ 0.53CNN滚动回测模型在滚动窗口上的平均表现至少领先上一日方向3%我自己早期做这个方向时静态切分回测accuracy做到0.62就兴冲冲模拟了实盘结果收益被交易费用和滑点磨平。后来改成逐窗口滚动验证才看清模型对行情阶段切换很敏感——趋势行情里能贡献一点优势震荡行情里稳定亏损。现在我的习惯是任何预测结果都先跟上一日方向比一比模型只有在简单基准之上提供额外信息时才算有价值。这个思路供你参考希望帮到你。本文还有配套的精品资源点击获取
返回列表