尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

股票量化系统实战:Python与LSTM搭建从数据到回测的完整链路

股票量化系统实战:Python与LSTM搭建从数据到回测的完整链路 简介一套基于Python与深度学习的股票量化系统及可视化高分项目面向金融量化初学者、课程设计及毕业设计学生。项目已获导师指导并达到97分涵盖数据获取、特征工程、模型训练、回测与可视化展示等环节下载后可直接运行无需修改。压缩包共1409个文件以Python源码py与编译后的pyc文件为主同时包含可执行文件、配置文件、模型权重及少量数据集整体约16.75MB目录结构清晰便于按模块查阅与二次开发。目前已有295人学习下载。资源附有文档说明可帮助使用者快速理解整体架构与关键代码逻辑提供的可视化模块能直观展示股票走势与预测结果csv等数据文件则支撑回测与验证流程。对于希望快速搭建量化系统、完成课程报告或学习深度学习在金融领域应用的同学是一份可直接落地的完整参考。1. 股票量化系统为什么绕不开 Python 和深度学习做一套能从数据变成交易信号的股票量化系统市面上有很多现成工具但真正走到“改模型、调参数、加特征”这一步Python 几乎是当前最短路径数据拉取、特征计算、深度学习训练、回测、可视化五件事本来就要反复迭代用同一门语言串起来能省掉大量跨工具搬运的成本。深度学习在这套系统里负责的不是“预测明天涨跌”这个过于宽泛的问题而是从开盘价、收盘价、成交量、换手率这些低层序列里压缩出对后续涨跌方向有区分度的表示。这篇文章按数据层、训练层、回测层、可视化层展开把完整系统的搭建思路和容易踩的坑讲清楚适合想自己搭一套可反复实验的量化环境、又不想只停留在调用现成接口的人。2. 从数据到特征股票量化系统的数据层和因子工程怎么做2.1 用 akshare 拉行情数据接口选型与字段校验做 A 股场景我用的比较多的是 akshare日线、分钟线、复权因子和基础财务数据都能覆盖适合在本地快速建数据集。拉日线本身只要一行接口但有两个细节必须处理一是复权方式要全局统一二是长时间跨度或多标的批量拉取会遇到限频脚本里要有重试和本地缓存否则连续拉几百只股票大概率中断。import akshare as ak import pandas as pd # 前复权日线示例平安银行 2022-2024 df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20220101, end_date20241231, adjustqfq, ) # 统一字段名只保留训练和回测要用的列 df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount, 换手率: turnover, }) df df[[date, open, close, high, low, volume, amount, turnover]] df[date] pd.to_datetime(df[date])adjustqfq表示前复权发生分红送股之后价格曲线不会出现向下跳空不然模型会把除权当作暴跌学进去这是量化数据里最常见的脏数据来源。symbol传纯数字代码不需要带交易所前缀如果同时拉沪深两市的多只股票建议落库时补一个market字段方便后续按行业或板块分组做特征对比。字段含义入模前是否直接使用open / close开盘价 / 收盘价不直接用用于计算收益和特征high / low最高价 / 最低价用于区间位置类特征volume成交量手需滚动平均做量比变换amount成交额元与 volume 结合识别量价关系turnover换手率可直接作为原始特征拿到数据后还要做基础校验起止日期是否连续、close 是否有 0 值或 NaN、停牌日是否整行缺失。停牌的处理方式我一般不是直接填充而是删除缺失价格的行但后续做滑动窗口时仍要考虑日期不连续导致窗口跨过停牌期的问题。如果不处理模型会以为停牌前后两个交易日是相邻的学出错误的时间关系。2.2 把行情数据清洗成深度学习能吃的特征原始 OHLCV 字段不是不能直接进 LSTM而是效果差。价格水平本身没有跨股票、跨时间段的可比性模型更应该看到相对位置和比值关系。常见的做法是合成三类特征均线偏离、波动率、量能变化。我还会加一个区间位置特征表示当前收盘价落在过去十天高低点之间的位置相当于给模型一个简单的市场热度信号。def make_features(df: pd.DataFrame) - pd.DataFrame: out df.copy() # 均线偏离度正值代表收盘价在均线上方 for w in [5, 10, 20]: ma out[close].rolling(w).mean() out[fma_dist_{w}] out[close] / ma - 1 # 过去 5 日累计收益率 out[ret_5] out[close].pct_change(5) # 量比当日成交量 / 5 日均量 vol_ma5 out[volume].rolling(5).mean() out[vol_ratio] out[volume] / vol_ma5 # 区间位置0 表示在最低点1 表示在最高点 h10 out[high].rolling(10).max() l10 out[low].rolling(10).min() out[pos_10] (out[close] - l10) / (h10 - l10) return out这段代码里rolling会产生前 N 行 NaN常见做法是直接截掉前 20 行并且固定特征计算顺序。ma_dist、vol_ratio这类比值特征天然消除了价格绝对值的影响也能规避不同股票价格水平差异过大的问题。至于一些量化群里流传的“指标源码”比如量能饱和度、分时点火之类大多是通达信公式搬进深度学习特征前要先确认它是不是用了当天收盘后才知道的信息如果是放进当日特征就等于偷看了未来。2.3 数据标准化与时间窗口切片特征拼接后必须统一量级。LSTM 内部的 tanh 和 sigmoid 都有输入饱和区间原始数值差异过大会让梯度在训练初期不稳定。我一般用StandardScaler做 z-score 标准化关键点是只用训练集 fit验证集和测试集用同一套均值和方差 transform不能全量数据一起 fit否则验证集信息会泄漏进训练过程。import numpy as np from sklearn.preprocessing import StandardScaler feature_cols [ ma_dist_5, ma_dist_10, ma_dist_20, ret_5, vol_ratio, pos_10, turnover, ] scaler StandardScaler() scaled scaler.fit_transform(df[feature_cols].iloc[20:]) def to_sequences(data: np.ndarray, seq_len: int 20): X, y [], [] for i in range(seq_len, len(data)): X.append(data[i - seq_len:i]) y.append(data[i, 0]) # 标签后续会替换 return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) X, y_placeholder to_sequences(scaled) print(X.shape)to_sequences把标准化后的二维数据切成三维张量形状约等于(样本数, 20, 特征维度)seq_len20表示模型每次看 20 个交易日。y 这里先取data[i, 0]只是占位真正有意义的标签要单独构造。提示StandardScaler只能 fit 训练集验证集和测试集必须复用同一组mean_和scale_这是量化建模里最容易犯的数据泄漏错误之一。2.4 标签设计比网络结构更值得花时间很多人在上一步直接拿data[i, 0]当标签训练这是错误的如果第一列是标准化后的 close模型只需要记住上一天收盘价就能把 loss 降得很低实际什么都没学到。更合理的标签是“未来 N 日累计收益”。例如用未来 3 日收益作为回归目标或把未来 5 日收益大于某阈值设为二分类正样本。我倾向于回归到未来收益回测阶段再用阈值转成买卖信号这样预测的强度信息不会丢失。future_ret df[close].pct_change(3).shift(-3).values[20:] y future_ret[seq_len:] # 与 X 对齐这里有个容易忽略的细节预测未来 3 日收益时相邻样本的标签天然存在窗口重叠训练时问题不大但评估指标会虚高因为验证集里相邻样本并不独立。这个问题到最后一章做时间偏移检查时还会再遇到。3. 深度学习模型选型与训练LSTM、Transformer 各适合什么场景3.1 为什么常用 LSTM 做股票序列建模股票数据是变长时序LSTM 的优势是能在窗口内保留关键历史价位的信息同时遗忘掉不相关的噪声。相比普通全连接网络LSTM 对顺序敏感相比 TransformerLSTM 参数量小对数据量的要求低在没有海量分钟级数据时不容易过拟合。所以“中等规模日线数据、目标只是方向性预测”的场景里LSTM 是最划算的起点先跑通基线再考虑更复杂的结构而不是一上来就上多头自注意力。3.2 在本地把 LSTM 训练代码跑起来PyTorch拿到一份股票量化系统的源码包第一步先看 model definition、data loader、backtest 三个模块是否分离这决定了后续替换特征或模型时要不要到处改代码。下面是最简的两层 LSTM 加全连接输出的结构输入形状是(batch, seq_len, features)batch_firstTrue避免每轮都在维度上做搬运。import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout, ) self.head nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, 1), ) def forward(self, x): # x shape: (batch, seq_len, input_dim) out, _ self.lstm(x) return self.head(out[:, -1, :])训练循环里最常见的问题有两个一是预测时忘记切到model.eval()dropout 仍然生效结果带随机性二是梯度爆炸股票收益序列中偶发的涨停跌停会造成梯度异常。固定随机种子、加梯度裁剪都是必须做的。torch.manual_seed(42) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() for epoch in range(30): model.train() optimizer.zero_grad() pred model(X_train_tensor) loss loss_fn(pred, y_train_tensor) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step()max_norm1.0对梯度的 L2 范数截断极端行情不会把参数直接推飞。学习率1e-3对两层 LSTM 通常够用如果训练 loss 震荡不降先把 batch_size 从 64 提到 128或者把 lr 降到 5e-4这比盲目加深网络更有效。X_train_tensor和y_train_tensor由第二章生成的 numpy 数组转成 torch tensor 即可。3.3 训练阶段的坑归一化边界、数据泄漏、随机种子数据切分最忌讳随机打乱后再切训练集和验证集。股票数据相邻样本强相关随机打乱等于让模型提前看到了验证时段附近的信息。正确做法是按时间顺序切分验证集取最后 20%训练时间范围与验证完全不重叠。split_idx int(len(X) * 0.8) X_train, X_val X[:split_idx], X[split_idx:] y_train, y_val y[:split_idx], y[split_idx:]同时所有随机操作都要固定种子包括初始化、数据加载、样本顺序任何一步不固定调参时你比较的都是噪声。更隐蔽的问题是标签索引没对齐导致的“标签漂移”模型实际上在预测昨天已经发生的涨幅回测结果却很好排查方法放在第五章。参数常用范围设置建议seq_len10 ~ 60日线先取 20hidden_dim32 ~ 128从 64 开始过拟合明显时减小num_layers1 ~ 32 层性价比最高dropout0.1 ~ 0.4数据量越小取值越大learning_rate5e-4 ~ 2e-3训练震荡时降低batch_size32 ~ 128数据量少用 32 更稳定4. 回测与可视化把模型的预测结果变成能看懂的收益曲线4.1 先写一个轻量回测信号生成、手续费与滑点模型回归输出的是未来 3 日预期收益不能直接交易需要先定义信号预测收益大于 2% 记为买入小于 -2% 记为卖出中间区域不动。回测我习惯先用 Pandas 手写日频版本几十行能跑通逻辑完全透明等后续做事件驱动或多标的研究再切到 backtrader 这类框架否则框架本身的成交撮合逻辑会干扰问题定位。# pred 是模型在验证集上的预测结果 th 0.02 signals np.where(pred th, 1, np.where(pred -th, -1, 0)) cash 1.0 hold 0.0 fee 0.0003 slip 0.001 close df[close].values for i in range(1, len(signals)): daily_ret close[i] / close[i - 1] - 1 # 昨天出现买入信号且当前空仓今天开盘买入 if signals[i - 1] 1 and hold 0: hold cash * (1 - fee - slip) cash 0 # 昨天出现卖出信号且当前持仓今天开盘卖出 if signals[i - 1] -1 and hold 0: cash hold * (1 - fee - slip) hold 0 # 持仓市值按当日收益结算 if hold 0: hold * (1 daily_ret) net_value cash hold手续费和滑点重点看两个数佣金万三双边是六滑点单边按 0.1% 算一年做 50 次交易就是 10 个百分点的损耗。回测里这两个值必须偏保守否则策略过拟合回测也不容易看出来。日频策略的信号产生与成交时点要分开信号在当日收盘后产生次日开盘价成交这样不会用到“当天收盘价同时成交”这种不可实现假设。4.2 用 pyecharts 输出净值曲线与买卖点回测结果可视化单独用 matplotlib 画静态图信息密度不够。我一般用 pyecharts 输出 HTML 图表能缩放、能悬浮、能叠加多个数据序列适合在同一张图里对比策略净值和沪深 300 基准。from pyecharts import options as opts from pyecharts.charts import Line line Line() line.add_xaxis(date_list.tolist()) line.add_yaxis(策略净值, [round(v, 4) for v in net_values], is_smoothTrue) line.add_yaxis(沪深300基准, bench_values, is_smoothTrue) line.set_global_opts( title_optsopts.TitleOpts(title策略净值 vs 基准), tooltip_optsopts.TooltipOpts(triggeraxis), legend_optsopts.LegendOpts(pos_leftright), ) line.render(net_value.html)triggeraxis让鼠标悬浮时同时显示两条曲线的值复盘时能快速定位到净值尖峰或深坑对应的日期再回去看当天发生了什么。如果想在 Jupyter 里交互也可以把render换成render_notebook但我自己更习惯落成 HTML 文件方便多次保存对比。4.3 可视化看板里必须有的三张图回测报告里信息最密集的三张图策略净值与基准净值叠加、模型预测值与实际收益的散点对比、带买卖点标记的 K 线图。很多项目只放第一张净值图漂亮就认为策略有效实际上净值曲线很容易被两三次大行情拉起来掩盖大部分时间的连续亏损预测值和真实收益的散点关系才能反映模型是否学到了真规律。图表观察重点对应参数净值叠加曲线最大回撤位置、创新高速度信号阈值预测值与真实收益散点是否存在正相关聚集趋势相关系数K 线 买卖点标记信号是否集中在涨跌停附近信号阈值三张图都建议默认输出为本地 HTML最后再抽四个核心数字做可视化面板年化收益、夏普比率、最大回撤、交易次数。这四项足以在一屏内判断一组参数的好坏。5. 离线验证与参数敏感性检查让量化系统不容易“过拟合回测”5.1 用滚动前推验证替代一次性划分数据固定一次性切分有一个隐患当你反复调参后验证集结果已经被你间接看过本质上还是在拟合验证集。更贴近真实运行的方式是滚动前推验证把数据切成多段每一轮只用此前数据训练、之后一段做验证最终把各段结果串起来看整体稳定性。results [] step 60 start_train 600 for split in range(start_train, len(df) - 60, step): train df.iloc[:split] test df.iloc[split:split step] model train_lstm(train) # 复用第三章训练流程 pred predict(model, test) results.append(evaluate(pred, test))看结果时不要只取平均收益要看每一段是否都稳定如果六段里只有一段大赚、其余全亏说明策略依赖某种单一市场状态真实环境很难复现。5.2 用时间偏移检查捕捉前视偏差回测曲线好看不代表模型学到了未来信息还需要检查预测序列与未来收益的对齐关系。方法是计算预测值与不同滞后收益的相关系数如果最强相关出现在 lag1说明模型实际上在预测“今天已经发生的结果”而不是未来。import numpy as np pred np.array(pred).flatten() ret df[close].pct_change().values for lag in range(1, 6): if lag len(pred): corr np.corrcoef(pred[:-lag], ret[lag:])[0, 1] print(flag {lag}: {corr:.4f})正常情况下预测与未来一天收益的相关系数不大但应该能稳定体现微弱的正相关如果 lag1 的相关性显著高于其他滞后值或者数值接近 1切片的对齐大概率有问题。建议把这组检查写进评估脚本每次训练完成自动打印 lag1 到 lag5 的相关系数作为是否发生信息泄漏的常规体检。参数敏感性测试则从 seq_len、信号阈值、手续费三组参数开始每改一个参数就记录净值、回撤和交易次数对比表格出来后你才能看出系统是在哪一步开始变差的而不是只盯着最漂亮的那一条净值曲线。本文还有配套的精品资源点击获取
返回列表