简介:本资源是一套基于Python与深度学习实现的股票量化系统及可视化项目源码,面向计算机、金融科技等专业的学生与量化入门开发者,可作为课程设计、期末大作业或自学实战案例,帮助理解从数据预处理、模型训练到可视化展示的完整量化流程。压缩包共1409个文件,约16.75MB,以636个py源码与671个pyc编译文件为主体,另含exe可执行程序、nbc与nbi笔记本、csv与xlsx数据样例、h5与pkl模型文件及txt、xml等配置说明,结构完整、下载即用。项目已获导师指导并通过97分高分评审,确保可运行,涵盖深度学习预测、回测与可视化等核心模块。目前已有296人学习下载,适合需要完整高分项目参考、快速搭建量化实验环境并查漏补缺的读者。
1. 从一份高分项目源码说起:Python 深度学习股票量化系统到底在做什么
很多做 Python 数据分析的同行,第一次看到「基于 Python 和深度学习实现的股票量化系统及可视化源码」这类项目时,第一反应往往是:又是一个把 LSTM 套在收盘价上、画几条均线就交差的课程作业。但真正翻过几个能拿高分、能被反复复用的项目之后你会发现,能跑通和能赚钱之间隔着一条很宽的沟,而这条沟恰恰是这类项目最值得拆解的地方。它要解决的核心问题不是「预测明天涨还是跌」,而是把数据获取、特征工程、深度学习建模、回测验证、可视化看板串成一条可复现的流水线,让每一个环节的输入输出都能被检查和替换。
这套系统适合三类人:一是想用深度学习做量化但不知道从哪下手的新手,需要一份能照着跑通的骨架;二是已经会写策略但模型部分薄弱的从业者,想补上深度学习这一环;三是需要做课程设计或毕业设计的学生,想要一个结构完整、文档清晰、可视化拿得出手的参考。它不承诺收益,也不保证模型有效,它提供的是一套工程化的组织方式——数据怎么存、特征怎么算、模型怎么训、结果怎么展示,每一步都有明确的接口和可调参数。下面按落地顺序,把这条流水线拆开讲清楚。
2. 数据层与特征工程:量化系统的地基怎么打
2.1 数据获取与本地存储的选型理由
量化系统的第一道坎不是模型,是数据。常见做法是用 akshare、tushare 这类开源接口拉 A 股日线数据,或者用 yfinance 拉美股数据。选哪个取决于你的标的池和网络环境,但无论选哪个,都不要在训练脚本里直接调接口——接口会限流、会改字段、会突然不可用,一旦训练中途断掉,前面的计算全白费。我一般会先把数据落到本地,用 SQLite 或 Parquet 存起来,训练时只读本地文件。
下面是一个把日线数据落到 SQLite 的最小脚本,字段按量化常用的 OHLCV 加复权因子来设计:
import akshare as ak import sqlite3 import pandas as pd # 拉取单只股票日线,adjust="qfq" 表示前复权 df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20180101", end_date="20241231", adjust="qfq") # 统一列名,避免后续脚本因接口字段变动而崩 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", "成交额": "amount", "换手率": "turnover" }) df["date"] = pd.to_datetime(df["date"]) df = df[["date", "open", "high", "low", "close", "volume", "amount", "turnover"]] conn = sqlite3.connect("stock.db") df.to_sql("daily_000001", conn, if_exists="replace", index=False) conn.close() print(df.tail())这段代码的关键点有三个:adjust="qfq"必须显式指定,否则不同时间拉的数据复权方式不一致,回测结果会失真;列名重命名是为了隔离接口变动,接口改字段名时你只需要改这一处映射;if_exists="replace"在增量更新时要改成append并配合去重,否则会重复写入。参数上,start_date建议至少覆盖一轮完整牛熊,日线级别 5 年以上才有统计意义。
2.2 特征工程:把价格序列变成模型能吃的张量
深度学习模型不会直接理解「收盘价 12.34」这种标量,它需要的是归一化后的窗口序列。常见做法是构造滑动窗口,每个样本包含过去 N 天的多因子特征,标签是未来 M 天的收益率方向或回归值。特征不要只用收盘价,至少加入成交量、换手率、以及几个技术指标,否则模型学到的只是价格自相关。
import numpy as np import pandas as pd def build_features(df, window=30, horizon=5): df = df.copy() # 基础收益率与波动特征 df["ret"] = df["close"].pct_change() df["vol"] = df["ret"].rolling(10).std() df["ma5"] = df["close"].rolling(5).mean() df["ma20"] = df["close"].rolling(20).mean() df["ma_ratio"] = df["ma5"] / df["ma20"] # 成交量相对强度 df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean() df = df.dropna() feat_cols = ["ret", "vol", "ma_ratio", "vol_ratio", "turnover"] # 按窗口切样本,标签为未来 horizon 天收益率 X, y = [], [] arr = df[feat_cols].values label = df["close"].pct_change(horizon).shift(-horizon).values for i in range(window, len(arr) - horizon): X.append(arr[i-window:i]) y.append(label[i]) X = np.array(X, dtype=np.float32) y = np.array(y, dtype=np.float32) # 逐特征标准化,避免量纲差异 mean = X.reshape(-1, X.shape[-1]).mean(axis=0) std = X.reshape(-1, X.shape[-1]).std(axis=0) + 1e-8 X = (X - mean) / std return X, y, mean, stdwindow=30表示用过去 30 个交易日,horizon=5表示预测未来 5 日收益。标准化必须用训练集的 mean 和 std,验证集和测试集要用同一组参数,否则就是数据泄露——这是新手最容易翻车的地方,模型在验证集上表现很好,一上测试集就原形毕露。ma_ratio和vol_ratio这类比值特征比原始价格更稳定,能减少模型对绝对价位的过拟合。
注意:特征工程阶段就要划分训练/验证/测试集,且必须按时间顺序切,不能随机打乱。随机切分会让未来信息泄露到训练集,回测曲线会漂亮得不像话,实盘一用就废。
3. 深度学习模型搭建:LSTM、Transformer 还是简单 MLP
3.1 模型选型的判断依据
股票序列建模常见三类结构:LSTM/GRU 适合捕捉中长期依赖,Transformer 适合长序列和多因子交互,MLP 加手工特征适合样本量小、信噪比低的场景。很多人一上来就上 Transformer,结果发现数据量根本撑不起注意力机制,训练 loss 震荡得厉害。我的经验是:日线级别、单标的、样本量几千条时,两层 LSTM 加 dropout 往往比 Transformer 更稳;如果做多标的、多因子、分钟级,再考虑 Transformer 或 TCN。
下面是一个可直接训练的 LSTM 回归模型,输出未来收益的预测值:
import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, n_feat, hidden=64, layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=n_feat, hidden_size=hidden, num_layers=layers, batch_first=True, dropout=dropout ) self.head = nn.Sequential( nn.Linear(hidden, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) # 回归输出未来收益率 ) def forward(self, x): # x: (batch, window, n_feat) out, _ = self.lstm(x) last = out[:, -1, :] # 取最后一个时间步 return self.head(last).squeeze(-1)hidden=64和layers=2是日线数据的常用起点,特征维度通常 5 到 20 之间。dropout=0.3在金融序列上很关键,因为信噪比低,不加 dropout 几乎必然过拟合。batch_first=True让输入维度是(batch, seq, feature),和 PyTorch 的 DataLoader 默认行为一致,省得来回转置。输出层用线性回归而不是分类,是因为收益率本身是连续值,强行二分类会丢掉幅度信息,而幅度对仓位管理很重要。
3.2 训练循环与早停策略
训练部分要处理好三件事:损失函数、学习率调度、早停。金融序列的标签分布不均,MSE 对大波动样本敏感,可以改用 HuberLoss 或对标签做截尾。早停的监控指标不要用 loss,用验证集上的 IC(信息系数)或方向准确率更贴近实际用途。
from torch.utils.data import TensorDataset, DataLoader from torch.optim.lr_scheduler import ReduceLROnPlateau def train_model(X_tr, y_tr, X_val, y_val, n_feat, epochs=100, patience=10): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = StockLSTM(n_feat).to(device) opt = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) sched = ReduceLROnPlateau(opt, mode="min", factor=0.5, patience=5) loss_fn = nn.HuberLoss(delta=0.01) # 对异常收益更鲁棒 tr_loader = DataLoader(TensorDataset( torch.tensor(X_tr), torch.tensor(y_tr)), batch_size=64, shuffle=True) val_x = torch.tensor(X_val).to(device) val_y = torch.tensor(y_val).to(device) best_val, best_state, wait = float("inf"), None, 0 for ep in range(epochs): model.train() for xb, yb in tr_loader: xb, yb = xb.to(device), yb.to(device) opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() model.eval() with torch.no_grad(): val_loss = loss_fn(model(val_x), val_y).item() sched.step(val_loss) if val_loss < best_val: best_val, best_state, wait = val_loss, model.state_dict(), 0 else: wait += 1 if wait >= patience: print(f"early stop at epoch {ep}") break model.load_state_dict(best_state) return modelHuberLoss(delta=0.01)里的 delta 要按标签量级调,收益率通常在 0.01 到 0.05 之间,delta 设 0.01 意味着超过 1% 的误差按线性处理,避免个别暴涨暴跌样本主导梯度。clip_grad_norm_是防梯度爆炸的后悔药,LSTM 在长序列上很容易出现梯度爆炸。patience=10配合ReduceLROnPlateau的patience=5,形成两级耐心机制,先降学习率再早停,比直接早停更充分。
提示:训练时把随机种子固定下来,
torch.manual_seed(42)和np.random.seed(42)都加上。金融数据本身噪声大,不固定种子的话每次跑出来的曲线都不一样,根本没法判断改动是否有效。
4. 回测与可视化:把模型输出变成能看的结论
4.1 回测框架的最小实现
模型输出的是预测收益率,要变成可评估的策略,需要一套回测逻辑:根据预测值决定仓位,计算每日净值,统计夏普、最大回撤、胜率。不要用现成的复杂回测框架起步,先手写一个最小版本,把逻辑吃透。
import numpy as np import pandas as pd def backtest(pred, close, threshold=0.002, cost=0.0003): # pred: 模型预测收益率, close: 对应收盘价 signal = np.where(pred > threshold, 1, np.where(pred < -threshold, -1, 0)) ret = pd.Series(close).pct_change().shift(-1).values # 次日收益 strat_ret = signal * ret - np.abs(np.diff(signal, prepend=0)) * cost strat_ret = np.nan_to_num(strat_ret) nav = (1 + strat_ret).cumprod() sharpe = np.mean(strat_ret) / (np.std(strat_ret) + 1e-8) * np.sqrt(252) mdd = (nav / np.maximum.accumulate(nav) - 1).min() win = np.mean(strat_ret[signal != 0] > 0) return nav, {"sharpe": sharpe, "mdd": mdd, "win_rate": win}threshold=0.002是开仓阈值,预测收益超过 0.2% 才动手,过滤掉噪声信号。cost=0.0003是单边交易成本,A 股按万三算,双边加冲击成本可以设到 0.0005。np.diff(signal, prepend=0)统计仓位变化次数,每次变化扣一次成本,这一步很多人漏掉,导致回测收益虚高。夏普用 252 个交易日年化,最大回撤用累计净值算,胜率只统计有仓位的日子。
4.2 可视化:从净值曲线到交互看板
可视化分两层:一层是给研究者看的诊断图,一层是给使用者看的看板。诊断图用 matplotlib 快速出,看板用 ECharts 或 Plotly 做交互。下面这段用 Plotly 画净值对比和回撤,可以直接嵌到网页里。
import plotly.graph_objects as go from plotly.subplots import make_subplots def plot_result(nav, benchmark, dates): dd = nav / np.maximum.accumulate(nav) - 1 fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.7, 0.3], subplot_titles=("净值曲线", "回撤")) fig.add_trace(go.Scatter(x=dates, y=nav, name="策略净值"), row=1, col=1) fig.add_trace(go.Scatter(x=dates, y=benchmark, name="基准净值"), row=1, col=1) fig.add_trace(go.Scatter(x=dates, y=dd, name="回撤", fill="tozeroy"), row=2, col=1) fig.update_layout(height=600, title="回测结果") fig.write_html("backtest.html") return figmake_subplots把净值和回撤放在同一张图里上下对齐,方便看回撤发生在哪个阶段。fill="tozeroy"让回撤区域填充,视觉上更直观。write_html输出单文件,不依赖服务器,直接浏览器打开就能交互。如果要接百度可视化大屏或企业级看板,把数据导出成 JSON,前端用 ECharts 渲染即可,后端只需要提供一个返回净值和指标的接口。
注意:可视化里的基准要选对。做个股策略就用个股买入持有做基准,做多标的就用沪深 300 或中证 500。基准选错,超额收益全是假的。
5. 避坑与排查:那些让回测曲线一夜回到解放前的细节
5.1 未来函数:最隐蔽也最致命的坑
现象:回测夏普 3 以上,净值曲线几乎不回撤,实盘一用就亏。原因:特征计算里用了未来数据,比如用当日收盘价算了指标又用当日收盘价交易,或者标准化时用了全样本的均值和方差。解决:所有特征只能用到 t 时刻及之前的数据,标签用 t+1 及之后;标准化参数只在训练集上拟合;回测时信号和收益要错开一天,shift(-1)的位置不能错。
5.2 过拟合:验证集漂亮测试集崩
现象:训练 loss 降到很低,验证集 IC 0.1 以上,测试集 IC 接近 0 甚至为负。原因:模型太复杂、特征太多、样本太少,或者反复用测试集调参。解决:减少 LSTM 层数和隐藏单元,加 dropout 和 weight_decay,特征控制在 10 个以内;测试集只用一次,调参全在验证集上做;样本量少于 5000 时优先用简单模型。
5.3 数据对齐:复权、停牌、除权日的处理
现象:回测收益里出现单日 20% 以上的异常值,或者某段时间净值不动。原因:没做复权导致除权日价格跳空,停牌日数据缺失被前向填充后产生虚假收益。解决:统一用前复权数据;停牌日直接剔除,不要填充;除权日检查复权因子是否连续。这一步没有捷径,只能逐只标的核对。
5.4 交易成本与滑点被低估
现象:策略换手率高,回测赚钱,实盘被手续费吃光。原因:回测里成本设成 0 或只算单边,滑点没考虑。解决:双边成本按万三到万五设,高频策略再加滑点;统计换手率,换手率超过 50 倍每年的策略要慎重;用np.diff(signal)精确计算每次调仓。
5.5 随机种子与可复现性
现象:同样的代码跑两次结果不一样,改了个参数不知道是参数起作用还是随机性。原因:PyTorch、NumPy 的随机种子没固定,DataLoader 的 shuffle 没设种子。解决:开头统一设torch.manual_seed、np.random.seed、random.seed,DataLoader 加generator参数;把配置写成 YAML 或 JSON,每次实验存档。
6. 进阶技巧:用 walk-forward 验证替代单次回测
单次训练测试划分在金融数据上说服力有限,因为市场风格会切换。更靠谱的做法是 walk-forward:把时间轴切成多段,每段用前面所有数据训练、后面一段测试,滚动前进。这样得到的样本外表现更接近实盘。实现上不需要改模型,只需要改数据切分逻辑。
def walk_forward(X, y, n_splits=5, train_ratio=0.7): n = len(X) step = n // n_splits results = [] for i in range(n_splits): end = step * (i + 1) if end > n: break train_end = int(end * train_ratio) X_tr, y_tr = X[:train_end], y[:train_end] X_te, y_te = X[train_end:end], y[train_end:end] if len(X_te) < 50: continue model = train_model(X_tr, y_tr, X_te, y_te, X.shape[-1]) model.eval() with torch.no_grad(): pred = model(torch.tensor(X_te)).numpy() ic = np.corrcoef(pred, y_te)[0, 1] results.append({"fold": i, "ic": ic, "n_test": len(X_te)}) return pd.DataFrame(results)n_splits=5把数据切成 5 段,train_ratio=0.7表示每段前 70% 训练后 30% 测试。每折算一个 IC,最后看 IC 的均值和标准差——均值高说明模型有预测力,标准差小说明在不同市场阶段都稳。如果某一折 IC 是负的,要去看那段时间市场发生了什么,是风格切换还是数据问题。这个表比单次回测的夏普更有说服力,也是答辩和评审时最能拿出手的证据。
我自己的习惯是:任何策略在写进文档之前,先跑一遍 walk-forward,IC 均值低于 0.03 的直接放弃,不浪费时间调参。这个门槛不高,但能过滤掉大部分自欺欺人的结果。希望帮到你。
本文还有配套的精品资源,点击获取