ARTICLE DETAIL

资讯详情

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

用TensorFlow CNN预测股票走势:特征工程、模型搭建与回测避坑

用TensorFlow CNN预测股票走势:特征工程、模型搭建与回测避坑 简介从资源标题与描述看这是一套面向股票量化分析初学者的TensorFlow深度学习实战资料核心演示如何用Python构建CNN模型预测股价走势并延伸至DQN等强化学习应用。压缩包共34个文件大小5.16MB文件类型以Python脚本.py、Jupyter Notebook.ipynb、结果示意图.png、PDF文档为主其中6个py脚本覆盖CNN、DQN模型训练与绘图5个ipynb提供可逐步运行的交互式实验2个pdf为架构与预测大纲说明另有训练好的模型权重及checkpoint文件可直接加载使用。目前已有1440人学习下载。资料按DQN_closePrice、DQN_KD_value、CNN_tsc等目录组织完整包含数据预处理、模型搭建、训练评估与可视化代码配合README和图表适合希望从零上手用深度学习预测股票、并理解CNN与DQN在时间序列中落地的学习者。1. 用TensorFlow跑CNN预测股票走势不是荐股神器但也不是玄学先说结论用CNN预测股票走势最难的部分从来不是模型而是特征工程和时间切分。你会发现模型训练时准确率很漂亮一换到未来数据就翻车——这不是模型坏了而是你的验证集和训练集在时间轴上发生了泄漏。本文会把整套流程拆给你看如何把K线数据变成CNN能吃的三维张量、用TensorFlow的Keras API搭一个轻量外观模型、以及回测时最容易踩的数据坑。这套方法适合两类人一是想入门量化交易的Python开发者二是已经在做技术指标分析、想用深度学习替代人工看图的研究者。它能回答的不是明天涨不涨而是模型输出的概率信号能不能稳定跑赢买入持有。方向真实落点清晰接下来按步骤来。2. 从K线到张量把行情数据处理成Conv1D能直接读入的输入结构2.1 特征列怎么选OHLCV之外再加哪些列才算够用CNN处理的是局部模式不是孤立数字。所以喂给模型的每一行应当是一条能让卷积核看图说话的特征组合。最基础的五列是开盘价、最高价、最低价、收盘价、成交量但这五列直接进模型有一个明显问题——不同股票的价格绝对水平差异巨大茅台和农业银行的数据不在同一个量级模型会被价格大小牵着走。我一般的做法是在OHLCV基础上派生五类特征日收益率、振幅、成交量相对前N日均量的比值、收盘价相对N日均线的偏离度、以及最近N日收益率的滚动标准差。这样做的目的是把价格的多空关系转换为形态的多空关系卷积核才能真正学到放量突破这类结构而不是死记硬背价格区间。import pandas as pd import numpy as np def build_features(df): df df.copy() # 基础列列名对齐常见数据源的行情接口 df[daily_return] df[close].pct_change() df[amplitude] (df[high] - df[low]) / df[close] amount_ma5 df[amount].rolling(5).mean() df[vol_ratio] df[amount] / amount_ma5 df[close_ma20] df[close] / df[close].rolling(20).mean() - 1 df[return_std_10] df[daily_return].rolling(10).std() return df.dropna()特征构建这一段关键是不要引入未来函数。pct_change()、rolling()这些操作天然错位当前行的close_ma20只用了包括当日在内的过去20根K线没有问题。vol_ratio是第一版必调参数它衡量的是当前成交量的热度比绝对成交量稳定得多。这几列之间相关性较高但CNN的卷积核本身具备特征提取能力不需要像传统机器学习那样做严格的去相关处理。2.2 滑窗与标签构造未来N日涨跌如何映射成监督信号CNN不知道昨天和前天的先后它只知道窗口内的顺序就是时间顺序。因此构造训练样本时要把连续的时间序列切成固定长度的滑窗每个窗口对应一个标签。这个窗口长度直接决定模型看到的历史跨度——短了学不到中期趋势长了引入太多噪音。标签的构造方式取决于你做回归还是分类。如果只关心方向就把未来N日的累计收益率映射成二分类标签如果关心幅度就把未来N日累计收益率当作回归目标。我默认用二分类因为它对标注噪音更鲁棒而且评估时能用混淆矩阵直观看出模型是不是在躺平。def make_windows(data, feature_cols, window20, horizon5): X, y [], [] # 按时间顺序遍历禁止随机采样 for i in range(len(data) - window - horizon 1): x data[feature_cols].iloc[i:i window].values future_ret (data[close].iloc[i window horizon - 1] / data[close].iloc[i window - 1] - 1) X.append(x) y.append(int(future_ret 0)) return np.array(X), np.array(y)这里horizon5表示预测未来5个交易日的方向。int(future_ret 0)把累计收益为正记为1否则记为0。两个注意点一是i的终止条件同时减去了window和horizon保证最后一个样本的标签不会越界二是整个构建过程严格按行的先后顺序进行没有做任何shuffle这一点对后续训练验证集的划分至关重要。如果准备把方向预测升级为幅度预测只需把y从int(future_ret 0)改成future_ret模型输出层从sigmoid换成线性激活即可。两套方案的前置特征与滑窗逻辑完全一致。2.3 数据标准化一次性写好别在训练和测试之间重复计算股票数据的标准化有一个极其容易踩的坑直接用全样本的均值和方差做归一化然后再切训练集测试集。这样做的后果是测试集的信息通过统计量泄露进了训练数据模型的评估结果天然虚高。正确做法是只计算训练集的均值和方差用它同时去变换训练集、验证集、测试集。from sklearn.preprocessing import StandardScaler def split_and_scale(X, y, train_ratio0.6, val_ratio0.2): n len(X) train_end int(n * train_ratio) val_end train_end int(n * val_ratio) X_train X[:train_end].reshape(-1, X.shape[1], X.shape[2], 1) y_train y[:train_end] X_val X[train_end:val_end].reshape(-1, X.shape[1], X.shape[2], 1) y_val y[train_end:val_end] X_test X[val_end:].reshape(-1, X.shape[1], X.shape[2], 1) y_test y[val_end:] # 关键只用训练集拟合scaler scaler StandardScaler() orig_shape X_train.shape X_train scaler.fit_transform(X_train.reshape(-1, X_train.shape[-1])).reshape(orig_shape) orig_shape_val X_val.shape X_val scaler.transform(X_val.reshape(-1, X_val.shape[-1])).reshape(orig_shape_val) orig_shape_test X_test.shape X_test scaler.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(orig_shape_test) return X_train, y_train, X_val, y_val, X_test, y_test, scalerreshape(-1, seq_len, features, 1)把每个样本变成一个四维张量最后一维的1是通道数对应Conv2D需要的输入格式。标准化操作在每个通道上独立做fit_transform只作用在训练集上验证集和测试集用的都是训练集那组统计量。这里要特别提醒数据标准化是训练-验证-测试全流程的第一步必须先切分再标准化任何先全量归一化再切分的写法都是给自己埋雷后面评估出来的准确率没有参考价值。3. 用Keras搭建轻量CNN模型从Conv2D到可训练的最小实现3.1 输入形状与卷积核设计为什么用Conv2D而不是Conv1D股票数据本身是一维时间序列很多教程推荐直接上Conv1D。但把滑窗数据组织成(窗口长度, 特征数, 1)之后Conv2D同样适用而且有一个实际好处卷积核可以同时覆盖时间长度和特征维度两个方向。比如一个(3, 3)的卷积核可以同时看到连续3个交易日里的3个不同特征这对捕捉量价配合这类复合形态很有效。我一般优先用Conv2D。原因是它在视觉任务里被验证过训练更加稳定对局部结构的提取更细粒度。而且当你想尝试把CNN替换成Transformer时输入结构只需要做很小的改动。3.2 最小可运行代码三层卷积加全连接直接能跑的版本下面这个模型结构是我经过多次调整后保留的最小骨架三层Conv2D池化接一个全局池化层最后用dropout和全连接输出分类概率。这个规模在CPU上训练也能承受适合先跑通整个流程。import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(window_size20, feature_dim5): model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shape(window_size, feature_dim, 1)), layers.MaxPooling2D(pool_size(2, 1)), layers.Conv2D(64, (3, 2), activationrelu), layers.MaxPooling2D(pool_size(2, 1)), layers.Conv2D(128, (2, 2), activationrelu), layers.GlobalAveragePooling2D(), layers.Dropout(0.3), layers.Dense(1, activationsigmoid) ]) model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy, tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]) return model模型的关键参数有三个Conv2D的卷积核形状、MaxPooling2D的池化窗口、以及最后的Dropout比例。(3, 3)的卷积核是时间序列任务里比较中庸的选择太小感受野不足太大容易过拟合。pool_size(2, 1)只在时间维度上池化保留特征维度的完整性这是一个容易被忽略的细节——如果在特征维度上也做池化特征之间的区分度会被磨平。GlobalAveragePooling2D替代传统的Flatten好处是大幅减少全连接层的参数数量对防止过拟合有明显帮助。Dropout(0.3)在股票这类低信噪比数据上是必要的模型很容易记住训练集里的噪音dropout是第一道防线。3.3 训练流程与回调早停和检查点怎么设训练CNN预测股票不需要追求极致的epoch数。小数据集上反复迭代只会让模型过拟合把历史K线形状背下来。我常用的训练策略是设置EarlyStopping盯验证集loss耐心值设为20用ModelCheckpoint保留最优权重回调机制能让模型在训练集上的表现看起来完美但真实可用的模型恰恰是那个在验证集上最早的稳定点。checkpoint tf.keras.callbacks.ModelCheckpoint( best_cnn.keras, monitorval_loss, save_best_onlyTrue) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience20, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size64, callbacks[checkpoint, early_stop], verbose1 )batch_size64是股票日线数据的常见选择。数据量通常在几千到几万条太小会让梯度更新方向漂移大太大则会陷入局部平滑区导致收敛到平庸解。如果数据量更小batch_size可以降到32配合上面提到的dropout一起使用。训练完成后第一件事不是看准确率而是画一张训练loss和验证loss的曲线。如果两条线在某个epoch后开始明显分叉说明模型开始背训练数据了这时的最优检查点通常出现在分叉点之前。4. 训练评估与朴素回测验证集和测试集如何说真话4.1 按时间切分和随机切分的本质差别很多入门项目在sklearn里写一行train_test_split(X, y, test_size0.2)就完事了这在股票预测里是原则性错误。原因很简单股票数据的顺序本身就携带信息——市场的状态是连续的2020年的数据和2024年的数据分布完全不一样。随机切分会让训练集和测试集交织在同一个时间段里测试集里混有训练样本的邻居模型等于作弊。正确做法是严格按时间顺序切分前60%作训练集中间20%作验证集最后20%作测试集。上一章split_and_scale函数已经做了这件事。训练集和验证集的时间段绝不能重叠而且验证集必须晚于训练集。这样才能模拟用历史学到的规律去预测未来的真实场景。4.2 评估指标准确率在股票预测里的局限与替代方案股票预测的正负样本比例天然不平衡用准确率评估会让模型产生一种假象如果上涨样本占55%模型全部预测上涨准确率就有55%。这在验证集上看起来比抛硬币强实则是亏损的信号。from sklearn.metrics import classification_report, confusion_matrix y_pred_prob model.predict(X_test, verbose0) y_pred (y_pred_prob 0.5).astype(int) print(classification_report(y_test, y_pred, target_names[下跌/平, 上涨])) print(confusion_matrix(y_test, y_pred))评估模型时我更关注recall和precision的组合。recall衡量所有上涨的日子模型抓到多少precision衡量模型的每次上涨判断有多少是对的。对交易策略来说precision低意味着频繁误报会产生大量交易成本和亏损recall低则容易错过大行情方向对了但没仓位。人工调整阈值0.5可以改变这两个指标的平衡——把阈值从0.5提高到0.65precision通常上升recall下降这是一个在策略层面做取舍的工具。4.3 朴素回测用预测信号做次日调仓验证策略收益是否真实回测的目的是验证按模型的信号去执行交易能不能赚到钱。这里的核心不是收益率数字本身而是不要引入未来函数——信号必须基于当日收盘后的数据交易只能发生在次日开盘且要计入手续费与滑点。def backtest_simple(results_df, fee_rate0.001): results_df.columns: [date, close, pred_prob, future_ret] 模拟策略pred_prob 0.6 次日持有否则空仓 results_df[position] (results_df[pred_prob] 0.6).shift(1).fillna(0) results_df[strategy_ret] results_df[position] * results_df[close].pct_change() # 扣除交易成本只在仓位变化时扣除 trades results_df[position].diff().abs().fillna(0) results_df[strategy_ret_net] results_df[strategy_ret] - trades * fee_rate results_df[benchmark_ret] results_df[close].pct_change() return results_dfshift(1)是整个回测的灵魂——它把当天的预测信号移到下一天执行确保没有用到当天的收益信息。fee_rate0.001对应单边千一的手续费与滑点这是A股日线交易中比较保守的估计。position.diff().abs()用于识别换仓时刻只在换仓那天扣手续费避免频繁交易的信号错误地免于成本惩罚。回测评价不应只看最终收益要看最大回撤和夏普比率。我通常直接用annual_return / (daily_std * sqrt(252))估算年化夏普2以上算优秀1.5以下对于日频策略不够有吸引力。5. 避坑与常见问题五条高频踩坑记录每一条都对应一个不靠谱模型5.1 标准化泄漏验证集acc奇高测试集一落千丈现象训练和验证都达到85%准确率测试集直接跌回55%和抛硬币差不多。原因写代码时先对整个数据集做了StandardScaler().fit_transform()再切分训练集和测试集标准化统计量把测试集的信息泄露到了训练过程里。解决训练集的scaler保存下来测试集只能用scaler.transform()绝不能重新fit。这是量化项目里最常见的数据泄漏每个人都会至少踩一次。5.2 滑窗重叠训练样本前后高度相关模型是在背答案现象训练集loss降得飞快验证集准确率却比训练集低15个百分点。原因滑窗的滑动步长为1相邻两个样本重叠了19天它们几乎是一模一样的结构。模型看到大量重复样本严重过拟合局部形态。解决计算样本量与实际独立行情天数的比例。如果样本量远超K线根数说明重叠严重。可以增加滑窗步长如step5或者接受这个现实、仅依靠更强的正则化手段。5.3 标签不平衡95%的时间预测上涨精确率却惨不忍睹现象模型输出的概率几乎都在0.6到0.8之间徘徊很少低于0.5。原因训练集中上涨日占比超过60%CNN学到的最优策略就是多喊涨反正喊错的代价不大。解决先做标签重采样或者加权损失。在model.compile()里给正样本设置更高的class_weight让模型对下跌样本更敏感。同时把评估指标从accuracy换成precision和recall不要被整体准确率骗了。5.4 复权数据变化同一只股票前后下载两次K线结果完全不一致现象模型在历史回测上稳定盈利换一台电脑重新下载数据同样的策略变成了亏损。原因行情接口默认返回前复权数据而前复权价格会随除权除息事件不断回溯调整之前的历史价格也随之改变。解决下载数据时固定复权方式最好用后复权或不复权数据进行建模并在代码中记录数据快照的日期与来源。这个问题是血泪经验几乎所有量化入门者都会在这个地方消耗两三天。5.5 回测盈利、实盘亏损手续费和滑点被忽视了现象回测曲线很漂亮年化收益50%实盘跑起来持续小亏。原因回测里每笔交易只扣了万几的佣金没有计算滑点也没有考虑涨跌停无法成交的情况。解决在回测里按千分之一到千分之二扣除综合成本并过滤掉开盘涨停无法买入、开盘跌停无法卖出的样本。这部分的成本差异直接决定策略的真实生死线。6. 进阶玩法从单模型到多模型组合用概率输出而非单纯涨跌标签做决策如果CNN的单模型已经跑通下一步不是堆更多卷积层而是用概率输出代替涨跌标签。CNN最后一层sigmoid输出的值域是0到1这个值反映的并不仅仅是分类信心它本身可以当作连续信号来用。一个可行的尝试是统计预测概率高于0.7的日子和概率在0.5到0.7之间的日子各自后续N日的平均收益——你会发现高概率区间的平均收益明显更优这时概率阈值就变成了可调仓位的依据。另一个方向是模型组合。用相同的数据切分方式分别训练CNN、LSTM和一个非线性SVM三者预测结果做简单投票。由于CNN捕捉局部形态、LSTM捕捉长程依赖、SVM捕捉决策边界它们的假设空间差异够大投票结果往往比任何单一模型都稳。def ensemble_predict(models, X): probs np.mean([m.predict(X, verbose0) for m in models], axis0) return probs组合逻辑很简单每个模型单独出概率然后取平均。需要留意的是每个模型的输入形状必须一致因此整个流程的特征构造和标准化必须复用同一套代码。我还保留了一个习惯每次实验结束时把模型的训练日期、数据区间、特征列表记录在config.json里。这面后悔药在三个月后回头调参时能救你一命不然你根本想不起来当时用了哪几列特征。关于TensorFlow与PyTorch的选型2024年的社区趋势里PyTorch热度明显占优但如果项目目标是快速落地回测框架Keras的简洁API和TensorFlow生态的完整工具链仍然是最省事的选择。CNN做股票预测的上限不在模型结构而在于你如何定义正确的预测信号。我的建议是先把分类方案不亏钱地跑上半年再考虑换架构。希望这篇笔记能帮你在量化交易这条路上少走几步弯路也希望你在第一次实盘时想起上面这五条避坑记录。本文还有配套的精品资源点击获取
返回列表