A股自动交易系统实战:从akshare数据获取到backtrader回测闭环
简介这套毕业设计资料围绕基于Python的股票自动交易系统面向计算机、金融商贸等专业需要完成课程设计或毕业设计项目的学生。资源提供完整的项目源码与前端界面文件共687个文件类型覆盖Python后端脚本、Java辅助代码、HTML/CSS/JS前端页面、配置文件及图标字体等压缩包整体约14.76MB目录划分清楚便于按模块检索与二次开发。内容预览显示系统中包含丰富的Web前端资源如样式表、动画库、对话框组件等说明项目具备完整的可视化操作界面适合作为金融量化交易入门项目的参照。目前已有692人学习下载对正在设计交易系统类毕业设计的同学具有直接参考价值可用于理解系统整体架构、界面搭建、模块划分与前后端协作思路也能帮助快速梳理毕业设计文档中的技术实现细节。1. 自动交易系统的骨架数据闭环先于策略逻辑这个毕业设计题目看着唬人拆开看就三层行情数据怎么来、策略怎么回测、信号怎么执行。我接触到的多数毕业论文翻车都翻在第一层——用了不连续的K线、复权方式前后不一致、或者把未来数据喂进了回测。先把数据链路走通策略反而花不了多少时间。本文按一个可复现的完整方案来讲akshare拉取行情、MySQL存日线数据、backtrader跑双均线回测、虚拟撮合落订单最后用滚动窗口检验参数是否过拟合。这套路径适合两类人正在做金融方向毕业设计的学生以及想快速搭一套A股研究框架但不想碰券商API的从业者。实盘通道在合规和成本上都有门槛毕业设计做到虚拟撮合加回测闭环已经足够支撑答辩。2. 行情数据链路akshare拉取、复权处理与MySQL落库2.1 数据源选型先确认免费额度与字段稳定性A股日线数据的可选来源有akshare、tushare pro、baostock三家。毕业设计场景下我首推akshare原因是免token、接口示例多、字段覆盖全tushare pro需要120积分才开放daily接口注册到攒够积分通常要一周时间成本不划算baostock免注册且稳定但字段较旧部分复权接口返回格式跟pandas版本有兼容问题。数据源免费额度主要痛点适用场景akshare无token限制接口免费东财网页结构变更时字段会位移毕业设计、快速原型tushare pro120积分解锁daily积分门槛、接口限流有长期数据需求的研究baostock完全免费字段更新慢、文档零散纯K线回测这里有个现实问题akshare各个接口的数据源是东方财富、新浪、腾讯的网页接口页面改版会导致返回字段名变化。所以我一般会在拉取函数外再包一层字段白名单校验只保留自己需要的列避免下游策略代码被拖垮。2.2 增量拉取与MySQL表结构设计行情数据落库表结构的关键是主键设计。用“股票代码交易日期”做组合主键天然防止重复插入后续做增量更新时可以直接使用INSERT语句的IGNORE语义。CREATE TABLE stock_daily ( ts_code VARCHAR(10) NOT NULL, trade_date DATE NOT NULL, open DECIMAL(10,2) NOT NULL, high DECIMAL(10,2) NOT NULL, low DECIMAL(10,2) NOT NULL, close DECIMAL(10,2) NOT NULL, volume BIGINT NOT NULL, amount DECIMAL(18,4), PRIMARY KEY (ts_code, trade_date), KEY idx_date (trade_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;组合主键在这里的用意有两个一是数据库层面挡住重复数据二是回测时按ts_code和trade_date排序索引直接命中不需要额外排序操作。字段类型上价格用DECIMAL(10,2)成交量用BIGINT成交额用DECIMAL(18,4)避免浮点误差。拉取函数如下注意akshare返回的是中文列名必须重新映射import akshare as ak import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://quant:123456localhost:3306/stock) def fetch_history(symbol: str, start: str, end: str) - pd.DataFrame: df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjustqfq ) df.rename(columns{ 日期: trade_date, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: volume, 成交额: amount }, inplaceTrue) df[ts_code] symbol return df[[ts_code, trade_date, open, high, low, close, volume, amount]]列名映射这一步不能省。如果直接把中文列名写进MySQL后面每次写策略都要loc[收盘]代码可读性差且容易出错。统一转成英文小写是让数据层和策略层解耦的第一步。增量更新策略上我一般先查表里该股票的最大交易日期再从这个日期往前多取5个自然日原因是有停牌和除权除息纯粹的MAX日期容易漏掉边界事件last_date pd.read_sql( SELECT MAX(trade_date) FROM stock_daily WHERE ts_code %s, engine, params[600519] ).iloc[0, 0] if pd.isna(last_date): start 20000101 else: start (last_date - pd.Timedelta(days5)).strftime(%Y%m%d) df fetch_history(600519, start, 20241231) df.to_sql(stock_daily, engine, if_existsappend, indexFalse, methodmulti)这里回退5天的原因如果最后一条记录是周五停牌到周一才恢复直接以MAX日期为起点会漏掉周一的行情。多取几天靠数据库主键去重既能补数据又不会产生脏记录。2.3 前复权与后复权回测数据一致性怎么选复权处理是毕业设计答辩的高频提问点。前复权把历史价格向下调整信号计算时更接近“当时可看到的价格走势”适合回测后复权保持历史价不变适合计算累计真实收益。两种复权算出的均线信号在除权除息日附近会有差异。常见做法是库里同时存未复权收盘价和复权收盘价。策略计算用前复权价除权除息校验用未复权价——当未复权价的当日跌幅超过9.9%且不是ST时大概率是除权事件反过来可以检查复权数据是否遗漏。2.4 数据质量校验三件套数据落库后回测前必须跑一遍校验否则回测结果会被垃圾数据污染交易日是否连续和akshare的交易日历做差集缺的日期可能是停牌也可能是拉取失败。量价为0的记录直接剔除或标记这类数据多为停牌或新股上市首日。最高价小于最低价的记录数据源错误必须处理。df[bad] (df[close] 0) | (df[high] df[low]) bad_rows df[df[bad]] if not bad_rows.empty: print(f发现 {len(bad_rows)} 条异常数据) print(bad_rows[[ts_code, trade_date, open, high, low, close]])这套校验逻辑简单但对回测结果影响巨大。一份包含零成交量的数据会让均线策略在停牌日产生虚假信号而最高价低于最低价说明接口返回字段错位需要重新拉取而不是直接删行。3. 回测引擎落地双均线策略从零到绩效报告3.1 选型backtrader还是自研事件驱动回测框架的选择直接决定项目进度。backtrader的优势是内置了broker撮合、佣金模型和技术指标库策略代码量可以控制在100行以内自研事件驱动框架能展示代码能力但撮合逻辑、仓位管理和资金曲线的实现细节很容易出bug调试成本远高于预期。毕业设计场景我推荐backtrader。理由有三点一是社区资料多遇到问题容易搜到解决方案二是自带的Analyzer体系直接输出夏普、最大回撤、年化收益等指标不用自己写金融公式三是数据接口兼容PandasData从MySQL读出DataFrame直接喂进去就行。缺点也要知道数据量超过百万行时内存占用明显全市场回测需要分批跑。3.2 双均线策略完整实现双均线是所有趋势策略的基础。短期均线上穿长期均线做多下穿做空。A股没有做空机制所以空仓等待信号只做多头import backtrader as bt class DualMA(bt.Strategy): params dict(short_period5, long_period20) def __init__(self): self.ma_short bt.ind.SMA(self.data.close, periodself.p.short_period) self.ma_long bt.ind.SMA(self.data.close, periodself.p.long_period) self.cross bt.ind.CrossOver(self.ma_short, self.ma_long) def next(self): if self.cross 0 and not self.position: size int(self.broker.getcash() * 0.95 / self.data.close[0]) self.buy(sizesize) elif self.cross 0 and self.position: self.close()这段代码的核心逻辑在CrossOver指示器它返回三个状态1表示金叉、-1表示死叉、0表示无交叉。next()方法每根K线触发一次先判断是否有持仓再执行买卖。买入仓位用当前现金的95%除以现价取整剩下的5%留作滑点和手续费缓冲避免出现可用资金不足的报错。3.3 回测入口与佣金、滑点配置回测入口代码要把数据、策略、资金和费用参数全部组装起来data bt.feeds.PandasData( datanamedf, datetimeNone, openopen, highhigh, lowlow, closeclose, volumevolume, openinterest-1 ) cerebro bt.Cerebro() cerebro.adddata(data) cerebro.addstrategy(DualMA, short_period5, long_period20) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission0.0003, mult1.0) cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) cerebro.addanalyzer(bt.analyzers.DrawDown, _namemaxdd) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe, timeframebt.TimeFrame.Days, riskfreerate0.02) results cerebro.run()佣金参数这里有个高频错误commission0.0003对应的是万分之三佣金很多人误写成0.03导致每次交易手续费放大100倍回测收益直接变成负的。mult1.0表示不加杠杆A股普通账户不存在融资融券这个参数保持默认。riskfreerate设成0.02即无风险利率2%和十年期国债收益率大致相当。3.4 绩效指标的正确解读方式指标backtrader字段常见误读年化收益率returns.rnorm理解为“每年都能赚这么多”最大回撤maxdd.max.drawdown误以为只是单日最大跌幅夏普比率sharpe.sharperatio忽略交易频率直接看数值年化收益率在backtrader里默认按252个交易日复利计算它代表的是资金曲线的复合增长不代表每年实际实现的收益。最大回撤是从资金曲线峰值回落到谷底的最大幅度不是单日跌幅的最大值很多人在答辩时这两个概念说不清。夏普比率还有一个前提如果策略整个回测期只交易了三次夏普算出来再高也没有统计意义至少要保证30笔以上的交易样本。答辩时被问“这个策略为什么稳定”先检查交易次数和最大回撤不要只拿年化收益率说事。4. 信号执行从回测信号到虚拟下单4.1 实盘接口的现实边界很多毕设方案里写着“对接券商实盘”实际做下来会碰到两个问题一是个人开通券商API权限需要柜台申请和资金门槛多数学生不具备条件二是监管对程序化交易有报备要求个人直接上实盘合规风险高。这个系统在设计上把执行层做成虚拟撮合未来要接实盘只需要替换撮合模块里的成交函数前面的信号生成逻辑完全不用动。4.2 信号生成器与回测策略解耦信号生成器直接从MySQL读取日线数据计算均线关系输出买卖信号def generate_signal(df: pd.DataFrame, short: int 5, long: int 20) - pd.DataFrame: df df.copy() df[ma_short] df[close].rolling(short).mean() df[ma_long] df[close].rolling(long).mean() df[signal] 0 df.loc[df[ma_short] df[ma_long], signal] 1 df.loc[df[ma_short] df[ma_long], signal] -1 df[position] df[signal].diff() return df[df[position] ! 0].tail(5)这里用diff()找出信号发生变化的位置position为2表示刚从空仓转为满仓为-2表示反向切换。取tail(5)只是为了展示最近五条信号实际生产环境会结合持仓状态判断是否下单。与回测策略共用一套均线计算逻辑可以避免信号偏差。4.3 虚拟撮合按次日开盘价成交回测里的成交价和实盘有天然差异。为了保证系统不偷看未来数据虚拟撮合规则设计为当日14:50产生的信号按下一个交易日的开盘价成交。这个规则在毕业论文里要明确写出来它是回测可信度的基础。next_open get_next_trade_date_open(stock_code, signal_date) fill_price next_open * (1.0002 if direction BUY else 0.9998)加两个基点的偏移量是用来模拟滑点和冲击成本。股票交易还有佣金和印花税买入万三佣金卖出万三佣金加千一印花税这些在虚拟撮合里都要计算进去否则回测和模拟盘的收益差距会非常大。订单表设计要考虑防重复下单。定时任务每天跑一次如果网络抖动导致任务重复执行必须在数据库层面挡住CREATE TABLE order_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, signal_date DATE NOT NULL, direction VARCHAR(4) NOT NULL, fill_price DECIMAL(10,2) NOT NULL, shares INT NOT NULL, status VARCHAR(10) DEFAULT PENDING, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_signal (signal_date, stock_code, direction) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;UNIQUE约束的作用是同一只股票在同一天只能有一条买单记录和一条卖单记录即使调度器重复触发了两次job第二次插入会被数据库拒绝从根上杜绝重复下单。4.4 定时任务与交易日历别让策略在节假日空跑自动交易系统的调度器有个容易踩的坑A股有调休和临时休市用周一到周五判断交易日完全不可靠。正确做法是拉取交易所官方交易日历每天任务触发时先校验今天是否开市from apscheduler.schedulers.blocking import BlockingScheduler from datetime import datetime import akshare as ak def is_trade_day() - bool: today datetime.now().strftime(%Y%m%d) cal ak.tool_trade_date_hist_sina() return today in set(cal[trade_date].astype(str)) def job(): if not is_trade_day(): return df read_daily_data(600519) signals generate_signal(df) write_orders(signals) sched BlockingScheduler(timezoneAsia/Shanghai) sched.add_job(job, cron, hour14, minute50) sched.start()timezone参数这里必须显式设置。服务器如果部署在境外或使用UTC默认时区14:50的crontab会在北京时间22:50触发策略拿到的是第二天的数据整个信号就废了。这个细节在毕业设计答辩时主动讲出来能让评委觉得你真的跑过生产环境。调度时间选14:50而不是收盘后的15:30是因为15:00收盘后拿到的当日K线已经是完整数据再用它计算信号其实属于未来函数。14:50跑用的是盘中实时行情信号在收盘前10分钟落地次日开盘价成交逻辑上完全闭环。4.5 交易日核对函数与信号日志每次任务执行把信号和成交明细写JSON日志后续复盘可以直接回溯。日志文件名带上日期方便按天检索。这一段在论文里可以作为系统鲁棒性的证据。5. 参数前瞻性测试滚动窗口优化与过拟合自查5.1 训练集和测试集必须分层用全部历史数据选出最优参数是答辩现场最容易翻车的问题。面试官或评委只要问一句“这个参数为什么是5和20而不是3和15”就能看出是否做了样本外验证。正确做法是把时间轴切两段train_end 2021-12-31 test_start 2022-01-01 train_df df[df[trade_date] train_end] test_df df[df[trade_date] test_start]训练段用来选参数测试段只跑一次验证。测试段的绩效如果明显低于训练段说明参数过拟合需要回到策略层修改假设而不是继续调参。5.2 参数热力图与稳健区域把短周期从2遍历到10长周期从10遍历到60在训练段上扫描夏普比率输出热力图后选择一块“参数高原”而不是单一尖峰。通常做法是只要短周期在3-7、长周期在20-40区间内绩效都为正说明策略对这个参数组合不敏感这样的结论在论文里才站得住。短周期长周期训练段夏普测试段夏普3151.120.325201.081.018350.950.87上表第二行是理想情况训练和测试都为正且相差不大。第一行就是典型的过拟合训练段很好看测试段直接腰斩。5.3 蒙特卡洛置换检验判断策略是否真有alpha最后一个进阶检验把策略每天的收益率序列随机打乱后进行重采样跑1000次看真实策略的夏普比率排在随机分布的什么位置。如果排在95%分位以上说明策略存在真实的预测能力而非运气def shuffle_test(returns: pd.Series, n: int 1000) - tuple: origin returns.mean() / returns.std() null_dist [] for _ in range(n): shuffled returns.sample(frac1.0).reset_index(dropTrue) null_dist.append(shuffled.mean() / shuffled.std()) quantile (null_dist origin).mean() return origin, quantilequantile大于0.95时可以认为策略的收益结构显著区别于随机序列。这个检验代码写进论文附录既增加工作量又体现学术严谨性。实操中还能把置换检验的结果保存成图表答辩时作为策略有效性的佐证材料。本文还有配套的精品资源点击获取