简介:一套基于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( symbol="000001", period="daily", start_date="20220101", end_date="20241231", adjust="qfq", ) # 统一字段名,只保留训练和回测要用的列 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"])adjust="qfq"表示前复权,发生分红送股之后价格曲线不会出现向下跳空,不然模型会把除权当作暴跌学进去,这是量化数据里最常见的脏数据来源。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[f"ma_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, dtype=np.float32), np.array(y, dtype=np.float32) X, y_placeholder = to_sequences(scaled) print(X.shape)to_sequences把标准化后的二维数据切成三维张量,形状约等于(样本数, 20, 特征维度),seq_len=20表示模型每次看 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 对顺序敏感;相比 Transformer,LSTM 参数量小,对数据量的要求低,在没有海量分钟级数据时不容易过拟合。所以“中等规模日线数据、目标只是方向性预测”的场景里,LSTM 是最划算的起点,先跑通基线再考虑更复杂的结构,而不是一上来就上多头自注意力。
3.2 在本地把 LSTM 训练代码跑起来(PyTorch)
拿到一份股票量化系统的源码包,第一步先看 model definition、data loader、backtest 三个模块是否分离,这决定了后续替换特征或模型时要不要到处改代码。下面是最简的两层 LSTM 加全连接输出的结构,输入形状是(batch, seq_len, features),batch_first=True避免每轮都在维度上做搬运。
import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout, ) 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(), lr=1e-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_norm=1.0) optimizer.step()max_norm=1.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_len | 10 ~ 60 | 日线先取 20 |
| hidden_dim | 32 ~ 128 | 从 64 开始,过拟合明显时减小 |
| num_layers | 1 ~ 3 | 2 层性价比最高 |
| dropout | 0.1 ~ 0.4 | 数据量越小,取值越大 |
| learning_rate | 5e-4 ~ 2e-3 | 训练震荡时降低 |
| batch_size | 32 ~ 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_smooth=True) line.add_yaxis("沪深300基准", bench_values, is_smooth=True) line.set_global_opts( title_opts=opts.TitleOpts(title="策略净值 vs 基准"), tooltip_opts=opts.TooltipOpts(trigger="axis"), legend_opts=opts.LegendOpts(pos_left="right"), ) line.render("net_value.html")trigger="axis"让鼠标悬浮时同时显示两条曲线的值,复盘时能快速定位到净值尖峰或深坑对应的日期,再回去看当天发生了什么。如果想在 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 用时间偏移检查捕捉前视偏差
回测曲线好看,不代表模型学到了未来信息,还需要检查预测序列与未来收益的对齐关系。方法是计算预测值与不同滞后收益的相关系数,如果最强相关出现在 lag=1,说明模型实际上在预测“今天已经发生的结果”,而不是未来。
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(f"lag {lag}: {corr:.4f}")正常情况下,预测与未来一天收益的相关系数不大,但应该能稳定体现微弱的正相关;如果 lag=1 的相关性显著高于其他滞后值,或者数值接近 1,切片的对齐大概率有问题。建议把这组检查写进评估脚本,每次训练完成自动打印 lag1 到 lag5 的相关系数,作为是否发生信息泄漏的常规体检。参数敏感性测试则从 seq_len、信号阈值、手续费三组参数开始,每改一个参数就记录净值、回撤和交易次数,对比表格出来后,你才能看出系统是在哪一步开始变差的,而不是只盯着最漂亮的那一条净值曲线。
本文还有配套的精品资源,点击获取