
简介这套基于Python的股票量化系统覆盖数据采集、保存、分析、可视化与深度学习预测全流程适合金融/计算机相关专业毕设、量化交易入门者及需要快速搭建策略原型的开发者。压缩包共244个文件约3.51MB以py/pyc源码为主辅以json数据、png/jpg图表、ui/html/css界面及md/txt说明模块划分明确。已有553人学习浏览代码经实际测试可正常使用。系统将数据采集、数据库存储、统计分析与深度学习建模整合为一个完整闭环学习者既能掌握时间序列处理、因子计算等量化分析方法也能借助Matplotlib等可视化组件直观查看价格走势与回测效果深度学习模块则基于TensorFlow/PyTorch开展股价预测实验适合作为毕业设计、课程项目或企业实践应用的重要参考与二次开发起点。1. 股票量化系统别一上来就跑LSTM先把数据链路做扎实拆掉这套基于Python的股票量化系统之后我最大的感受就是它真正值钱的地方不是深度学习模型而是把“采集保存数据 分析数据 可视化 深度学习”完整串成了一条链路。很多做毕设或入门量化的朋友一上来就调LSTM结果数据拉不下来、存不进去、特征算不对连模型的门都没摸到。这套系统从akshare/tushare取数、MySQL落库、特征工程、K线可视化到PyTorch训练都写好了每一环都能独立替换。它适合人工智能/金融方向毕业设计需要跑通完整项目的人也适合想从回测走向实盘、先把数据链路摸清的从业者。2. 数据采集与落库把行情从接口变成能反复查的本地资产2.1 为什么用Python做数据采集Python做行情采集几乎是唯一不需要绕路的选择。akshare不需要token注册都不用直接pip安装调用就行tushare需要token和积分但数据更稳定、字段更规范。这套系统主要对接akshare同时保留了tushare的适配层。原因很简单对个人和毕设来说akshare零成本接口返回直接是DataFrame和pandas、sqlalchemy衔接是原生的不需要额外解析JSON或拼接字符串。需要提醒的是akshare的接口名和返回字段偶尔会调整尤其股票历史行情接口stock_zh_a_hist的列名是中文不同版本可能多一列“股票代码”。所以我习惯在采集层做个封装把中文列名改成语义明确的英文再对外暴露统一的fetch_daily(symbol, start_date, end_date, adjust)接口。这样后面做分析、可视化、模型训练都只依赖这一层上游数据源怎么变都影响不到业务代码。2.2 数据表设计与增量更新策略先设计落库的表结构。这里有一张推荐的表字段类型说明dateDATE交易日期symbolVARCHAR(12)股票代码如600519openDECIMAL(10,2)开盘价closeDECIMAL(10,2)收盘价highDECIMAL(10,2)最高价lowDECIMAL(10,2)最低价volumeBIGINT成交量股amountDECIMAL(20,2)成交额pct_changeDECIMAL(6,3)涨跌幅PRIMARY KEY(symbol, date)联合主键用MySQL是因为毕设答辩和后续扩展一般够用数据量不大。如果你机器上没装MySQLSQLite也能跑只要把连接URL改成sqlite:///quant.db。不过SQLite在并发写入和查询性能上弱一些全市场多线程跑的时候容易锁库。联合主键(symbol, date)是刻意的akshare接口重复拉取时返回的数据可能重复但如果表有唯一约束to_sql再用append就会报错。所以增量更新的正确姿势是先查库里这只股票的最大日期再只拉从下一天开始的数据。同时因为复权因子会变化我会定期对核心股票做一次全量替换保证复权数据是干净的。2.3 采集脚本落地封装、落库、增量一次做完下面这段是采集函数的核心部分我从项目里抽出来去掉了不必要的日志和异常处理import akshare as ak import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:123456127.0.0.1:3306/quant_db?charsetutf8mb4) def fetch_daily(symbol, start_date20200101, end_date20241231, adjustqfq): raw ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart_date, end_dateend_date, adjustadjust # qfq-前复权 hfq-后复权 空-不复权 ) if raw is None or raw.empty: return None raw.columns [ date, code, open, close, high, low, volume, amount, amplitude, pct_change, price_change, turnover ] raw[symbol] symbol return raw[[date, symbol, open, close, high, low, volume, amount, pct_change]]adjustqfq表示前复权这是我做模型训练时默认的选择。前复权保持了最新价格的真实性历史价格被“压缩”适合看趋势和训练模型后复权更适合计算真实收益率。如果做因子复现你可以切到adjusthfq但全库必须统一不能一半前复权一半后复权否则收益率和均线会出现断点。有了这个函数增量更新就是查一次库再决定拉取区间def recent_dates(symbol): sql SELECT MAX(date) FROM stock_daily WHERE symbol :s with engine.connect() as conn: return conn.execute(sql, {s: symbol}).scalar() def sync_daily(symbol): last recent_dates(symbol) if last: start (pd.Timestamp(last) pd.Timedelta(days1)).strftime(%Y%m%d) else: start 20200101 df fetch_daily(symbol, start_datestart) if df is not None and not df.empty: df.to_sql(stock_daily, engine, if_existsappend, indexFalse)为什么to_sql用append却不会重复因为查询起始日期是“最大日期1天”接口只返回新增交易日。当然如果某天接口崩了返回了残缺数据后被写入恢复接口后再跑会跳过缺失的那几天。所以我一般会在写入后做一次校验对比库里按周分组行数是否符合该股票的正常交易节奏。如果要批量同步多只股票常见做法是循环调用并加sleepimport time symbols [600519, 000001, 300750] for s in symbols: try: sync_daily(s) time.sleep(0.3) # 避免触发接口限流 except Exception as e: print(s, e)sleep(0.3)是按经验来的。akshare免费接口对单IP频率有限制全市场4000多只股票连续拉会被封。如果你要拉全市场建议按行业分批或者把间隔拉大到0.5秒以上。2.4 关于数据质量的四个注意点数据质量是量化系统里最容易被扣分的地方重点说四个前复权和后复权不能混用。同一只股票如果不同时段用不同复权方式拉取算出来的均线和收益率会出现假突破模型结果就是玄学。停牌日没有行情记录。不要试图把缺失日期填成前一日收盘价那会造成虚假的交易连续性。做特征对齐时保留缺失即可模型里用mask机制处理。接口有频率限制。大规模循环拉取时会被限流或封IP常见做法是每个股票之间sleep 0.3~0.5秒。拉沪深全市场建议错峰跑比如按行业分批。一定要记录数据拉取的“截至日期”。我在表里加了一个meta表记录每个symbol最后成功同步的日期避免靠MAX(date)判断因为数据回填时可能补录历史。这一章到这里数据已经能稳定落地了。血泪经验是宁可多花半天做增量更新和唯一键设计也别直接全量清空重来——否则第二天你发现自己把已经清洗好的历史数据覆盖了后悔药都没有。3. 分析与可视化先把数据变成能看懂的信号3.1 特征工程不要直接拿原始价格喂模型原始价格序列是非平稳的而且不同股票价格区间差异太大。茅台一千多银行股几块直接喂给神经网络会让模型学到价格绝对位置而不是波动模式。所以这一步至少要做三件事收益率、均线、波动率。下面这段是我项目里的基础特征函数import numpy as np def make_features(df): df df.sort_values(date).reset_index(dropTrue) df[return] df[close].pct_change() df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[volatility] df[return].rolling(20).std() df[label] np.where(df[close].shift(-5) df[close], 1, 0) return dfpct_change()计算的是当日收盘相对前一日收盘的变动比例解决了价格绝对水平问题。rolling(5).mean()是5日均线rolling(20).std()是20日收益率标准差也就是波动率。标签label是“未来5日是否上涨”用shift(-5)把5天后的收盘价挪到当前行比较注意这会在最后5行产生NaN训练前要删掉。还有一个细节close本身要不要保留我倾向于保留但会做标准化。标准化时只能对训练集拟合scaler验证集用同一个scaler转换不能重新拟合否则就是把未来信息带进了训练属于数据泄漏。3.2 可视化模块K线、均线和信号位置可视化有两个用途一是自己判断数据长什么样二是在答辩或汇报时让系统看起来完整。最少要做收盘价均线图、K线图、买卖信号标注。我通常用matplotlib打底因为可定制性最强。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False def plot_candle(df, symbol): plt.figure(figsize(12, 6)) plt.plot(df[date], df[close], label收盘价, linewidth1) plt.plot(df[date], df[ma5], labelMA5, linewidth1) plt.plot(df[date], df[ma20], labelMA20, linewidth1) plt.title(f{symbol} 价格与均线) plt.legend() plt.xticks(rotation45) plt.tight_layout() plt.show()SimHei是为了处理中文后面避坑章节会详细说。如果想要交互式图表用pyecharts做K线更好看适合做大屏展示但生成的是HTML后续做自动标注不如matplotlib方便。我的经验是分析阶段用matplotlib演示阶段用pyecharts各司其职。3.3 最大回撤、相关性与交易频次三个必看的分析指标模型和信号不是终点必须落到指标上。最大回撤是最直观的风险度量这段代码直接复用特征表def max_drawdown(df): df df.sort_values(date).reset_index(dropTrue) df[cum_max] df[close].cummax() df[drawdown] df[close] / df[cum_max] - 1 return df[drawdown].min()cummax()是到当日为止的最高收盘价drawdown是当前收盘价相对历史最高点的回撤比例。这个函数可以对比不同策略在历史上最惨的时候亏了多少。我有一次测出一个模型年化收益60%但最大回撤45%直接劝退——收益再高扛不住回撤也白搭。相关性分析也值得做。用df.pivot_table(valuesclose, indexdate, columnssymbol)把多只股票的收盘价转成宽表然后corr()算两两相关系数。如果深度学习模型买了十只高度相关的股票风险等于押注一个行业不是分散而是集中。我一般会把相关性0.7的股票组拉出来只在组内选一只。交易频次指标来自信号回测连续N天触发买入/卖出信号到底有多少次平均持仓几天。频次过高说明信号抖动大概率拟合了噪声频次过低说明策略不敏感。这些统计可以写成analyze_trades.py输出一张表格比模型准确率更有说服力。因为这些指标都是直接从特征表算出来的不需要额外数据作为毕设的分析部分很合适。4. 深度学习选股模型从特征窗口到LSTM训练4.1 样本构造时间窗口和标签对齐模型输入我采用60个交易日的特征序列输出“未来5日是否上涨”的二分类。为什么要60天一般一个月有22个交易日60天大约3个月既能覆盖中期趋势又不会让样本太少。你也可以试20/40/120但不要小于10天否则模型学不到周期。def build_sequences(df, seq_len60): features [open, close, high, low, volume, return] data df[features].values x, y [], [] for i in range(len(data) - seq_len - 5): x.append(data[i:iseq_len]) y.append(df[label].iloc[iseq_len]) return np.array(x), np.array(y)这里data[i:iseq_len]取第i到第i59天的6维特征label在iseq_len位置。因为标签是shift(-5)生成所以预测的是第i60天之后5天的涨跌逻辑上没有未来函数。注意特征顺序要固定训练和推理时保持一致。标准化要在build_sequences之前做但只能对训练集拟合scaler验证集用同一个scaler转换不能重新拟合。4.2 LSTM网络结构与训练参数我用两层LSTM加一个全连接层做简单的二分类。PyTorch实现核心如下import torch import torch.nn as nn class LSTMStock(nn.Module): def __init__(self, input_size6, hidden_size64, num_layers2, num_classes2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden) out out[:, -1, :] # 取最后一个时间步 out self.dropout(out) return self.fc(out)参数选择input_size6对应特征个数hidden_size64是LSTM隐层维度太小欠拟合太大在日线数据上容易过拟合num_layers2表示堆叠两层LSTMbatch_firstTrue让输入张量形状是(batch, seq_len, input_size)容易调试。Dropout(0.3)作用在全连接层前抑制过拟合。训练时常用的参数是batch_size64、learning_rate0.001、epochs50。学习率太大loss会震荡太小训练太慢0.001配合Adam是默认搭配。我习惯每10个epoch在验证集上算一次loss保存验证loss最低的模型而不是最后一个epoch的模型if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_lstm.pt)保存best而不是last是很重要的习惯否则你训练50轮最后可能落在过拟合区间。训练结束后加载best模型做测试。4.3 评估与回测单一准确率是不可靠的很多毕设喜欢用“准确率95%”当亮点但在涨跌预测这个场景下如果样本里80%是上涨模型全预测上涨就有80%准确率没有意义。所以评估至少要给出三个数精确率、召回率、F1尤其关注负类下跌的召回率。更贴近交易的是做一次简单回测预测上涨且次日确实上涨的收益率与全样本收益率对比。df_test[pred] model_predict(test_loader) df_test[signal] np.where(df_test[pred] 1, 1, 0) df_test[strategy_return] df_test[return] * df_test[signal].shift(1) df_test[cum] (1 df_test[strategy_return]).cumprod()signal.shift(1)是因为当天收盘后产生信号次日开盘才交易避免用到当日收益率。这里没有考虑涨跌停、滑点所以结果偏乐观但作为模型对比基准够了。实际看模型好坏我会重点看负类召回率因为漏掉下跌才是亏损来源。类别不平衡时可以给loss加权重比如把负类的权重设成2.0criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, 2.0]))这样模型不会一直倾向预测上涨负类召回率会明显提升。整体上第4章这些代码已经足够支撑一个完整可跑的深度学习选股模块关键是要把标签对齐和按时间切分做对。5. 避坑与排查真跑起来才会遇见的五个问题5.1 安装akshare后import报错现象pip install akshare之后一import就报错提示numpy或pandas版本冲突。原因akshare对pandas和numpy的版本要求比较新如果你之前装了旧版pandas比如1.0.x接口内部数据结构会崩。解决建议在虚拟环境里安装。先创建venv然后pip install --upgrade akshare它会自动拉起来新版本依赖。如果还有冲突手动升级pip install --upgrade pandas numpy即可。不要图方便在全局环境里硬装项目之间依赖会互相污染。5.2 重复运行采集脚本导致入库失败现象第一次跑采集脚本正常第二次跑同一个sync_daily到to_sql时抛Duplicate entry 600519-20240101 for key PRIMARY。原因接口返回的起始日期是“最大日期1”但如果你在测试时把库里日期删掉一部分或者接口补了历史数据就会撞上已经有主键的行。解决不要直接靠MAX(date)做唯一判断。更稳妥的做法是建表时用联合主键然后改用INSERT ... ON DUPLICATE KEY UPDATE方式写入或者先删除该symbol的待更新区间再append。我在项目里写了一个safe_write函数先按symbol删除start_date之后的数据再全量插入这个区间虽然慢一点但不会重复。5.3 matplotlib绘图中文全变方块现象图的标题、标签全是方框中文显示不出来。原因matplotlib默认字体里没有中文字体所以遇到中文渲染成占位方块。解决在绘图模块开头设置中文字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseSimHei在Windows常见。如果你在Linux服务器上跑又没有这个字体可以换成WenQuanYi Zen Hei或者使用matplotlib.font_manager动态指定字体文件。注意axes.unicode_minus也要设成False否则负号也会显示成方块。5.4 LSTM训练loss卡在0.69附近不下降现象训练到第5轮开始loss一直在0.690.70反复横跳就是降不下去。原因0.69约等于-ln(0.5)说明模型学成了“两种类别各一半概率”的随机猜测。背后的常见原因是标签严重不平衡比如样本里85%是“未来5日上涨”模型预测全部上涨就能维持很低loss实际上什么都没学到。解决先统计标签分布如果负类占比低于20%就要做类别平衡。可以在CrossEntropyLoss里给负类加权重比如负类权重设23。另外检查是否忘了对特征做标准化或者窗口长度太短导致输入没有区分度。我在项目里使用sklearn的class_weight.compute_class_weight自动计算权重效果比较稳定。5.5 回测收益很高实盘完全对不上现象回测曲线年化50%实盘一跑就亏胜率也明显下降。原因回测按收盘价直接成交没有考虑涨跌停、停牌、滑点。涨停时你根本买不进跌停时卖不出还有手续费和冲击成本。解决回测里至少要加两处限制。一是过滤信号如果预测当天涨停买入信号跳过如果跌停卖出信号跳过。二是给成交价加固定滑点比如买卖各加0.0005大约一个最小报价单位。做了这两件事之后回测收益通常会缩水一大截这才是真实可参考的数字。毕设里如果你能主动说明这些限制导师会觉得你理解交易细节。6. 把系统变成每天自动跑的常驻工具配置化、定时任务与结果验证6.1 把参数从代码里抽出来放到yaml写死的参数只适合一次性实验真正要持续跑就必须把标的、数据库连接、模型参数全部抽出来放在配置文件里。常见做法是创建一个config.yamlsymbols: [600519, 000001, 300750] db: url: mysqlpymysql://root:123456127.0.0.1:3306/quant_db?charsetutf8mb4 model: seq_len: 60 hidden_size: 64 epochs: 50 batch_size: 64训练入口用yaml.safe_load读取这样改参数不用改代码尤其毕设答辩现场换股票、换窗口长度时非常从容。6.2 定时采集数据每天自己进库写一个main.py入口通过命令行参数选择执行采集、训练还是预测。Windows用任务计划程序Linux用crontab一行0 18 * * 1-5 cd /opt/quant /opt/venv/bin/python main.py fetch logs/fetch.log 21注意要设在交易日18点后日线数据收盘后过一段时间才完整可用太早跑容易漏数据。6.3 验证每次改动后先跑最小用例改动特征或模型后先在同一批股票、同一段时间上跑一遍对比累计收益曲线是否一致。我踩过坑改了数据源字段映射后忘了更新测试集结果模型“好像”变好了其实是标签错位。从那以后我每次改代码都强制先跑一遍最小数据集和一份回测归档对比输出差异。这份打包好的项目代码已经按这个目录结构整理好解压后先改config.yaml里的数据库连接再按fetch→features→train的顺序跑就能完整复现。希望帮到你。本文还有配套的精品资源点击获取