Python股票量化回测完整源码:MACD策略与回测引擎搭建

发布时间:2026/10/3 23:46:10
Python股票量化回测完整源码:MACD策略与回测引擎搭建
简介面向高校课程设计、毕业设计与期末大作业场景的股票量化回测解决方案适合需要完成金融数据分析与策略回测项目的学生群体整体难度适中。全部源代码均经过本地编译并确认可运行项目评审分达到九十八分内容经助教老师审定既可用于课程学习也能用作完整项目参考或二次开发基础。压缩包共含一百五十个文件大小约三点五五兆字节。其中十二个源码文件构成核心辅以三十三个数据文件与二十七个表格文件用于数据存储和结果记录十六个样式表与十六个脚本文件搭建展示界面另有若干编译辅助文件与配置文档目录清晰便于按模块阅读。目前已有三百一十五人学习下载。通过该包可获得完整量化回测流程覆盖数据加载、特征处理、策略编写、回测执行与结果展示可直接运行查看效果。配套图表与配置文件能帮助快速定位关键逻辑适合高效完成课程报告、毕业设计答辩演示或期末大作业提交。1. Python股票量化回测源码包一份能直接交作业的完整方案期末大作业截止前一周很多人开始到处找能跑的python大作业股票量化回测源代码。这份股票量化回测Python解决方案是照着“可交付”标准整理的从CSV行情读取、MACD策略信号生成、回测撮合到绩效评估和净值可视化一条链路全打通不是只画一张K线图的演示脚本。项目按策略模块和回测引擎分离设计改参数、换策略都不用动框架代码。适合三类人课程大作业或期末设计需要完整项目的学生、毕业设计想少踩坑的开发者、以及刚入门量化想拿现成代码验证策略思路的从业者。2. 环境搭建与项目结构先把回测框架跑起来拿到压缩包第一件事不是看代码而是把环境跑通。这个项目的依赖很克制没有ta-lib这类编译困难的库MACD指标直接用pandas算所以安装阶段基本不会翻车。2.1 Python版本与依赖清单项目推荐Python 3.8及以上版本我实际在3.10和3.11都跑过没有遇到兼容性问题。核心依赖只有三个依赖库版本要求用途pandas1.3行情数据读取、指标计算、回测结果整理numpy1.21数组运算、绩效统计matplotlib3.5净值曲线和回撤图绘制pip install pandas numpy matplotlib如果你的机器上已经有Anaconda这三个库基本是预装的直接跳到下一步。国内网络环境下建议加镜像源用清华或阿里云的源会快很多pip install pandas numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后用一行命令验证版本确保pandas不低于1.3python -c import pandas, numpy, matplotlib; print(pandas.__version__, numpy.__version__, matplotlib.__version__)pandas版本偏低会导致部分链式赋值语法报错这是一个值得提前确认的点。我见过不少人在DataFrame上做条件筛选时报SettingWithCopyWarning其实不是代码逻辑错了是pandas版本不同带来的警告处理机制差异。与其慌着改代码不如先把版本对齐到这个项目锁定的范围。2.2 项目目录逐文件解读把zip解压后你会看到下面这个结构。这个布局是典型的分层设计数据、策略、引擎、评估各管各的这也是“高分项目”被老师认可的重要原因——代码结构比实现本身更能说明工程能力。文件/目录职责main.py回测入口串起数据加载、策略运行、绩效输出config.py全局参数初始资金、手续费率、滑点、策略参数data/存放CSV行情数据按股票代码命名strategy/策略类含MACD双底策略实现engine/回测引擎负责撮合成交和持仓管理evaluate/绩效指标计算与可视化输出实际运行时main.py会先读取config.py里的参数然后加载data目录下对应的CSV逐根K线推进策略最后调用evaluate生成绩效报告和图表。模块之间是单向依赖main依赖engineengine依赖strategystrategy不反向依赖engine。这样以后想换策略或换撮合规则只需要改一个文件其他模块完全不受影响。2.3 首次运行从CSV到绩效摘要环境装完先用小样本数据跑通全流程。项目里data目录自带了几只股票的日线数据以平安银行000001为例运行python main.py --code 000001 --start 2020-01-01 --end 2023-12-31看到控制台输出类似下面这段说明回测链路已经通了[2020-01-02] load 000001.csv, total 971 rows [2020-01-03] signal: MACD double bottom detected [2020-01-06] order: BUY 1000 shares at 16.80 ... [2023-12-29] final equity: 142385, total return: 42.38%, max drawdown: 18.72%第一次跑先别急着分析收益数字重点确认两个信息一是数据条数和日期范围与CSV一致二是最后一行的绩效摘要能正常打印。如果这两点都满足说明环境、数据、策略、引擎四个环节全部正常后面所有调优都在这个基础上进行。main.py的入口逻辑不复杂核心就是一句话把配置、数据、策略实例传给engine.run()返回值再交给evaluate模块统计。这个流程写得很直接适合初学者顺着读。如果你改动了策略参数只需要编辑config.py里的fast、slow、signal值不用动任何其他文件。3. 行情数据获取与预处理数据质量决定回测可信度量化回测里有一句话策略决定上限数据决定下限。很多回测结果看着漂亮拉到实盘就亏根源往往不在策略逻辑而在数据预处理这关没过。3.1 数据源选型本地CSV、tushare还是akshare这个项目默认用本地CSV目的是保证复现的确定性。在线接口虽然方便但存在两个问题一是接口字段随平台升级频繁变动今天能跑通的代码下周可能报错二是不同数据源的复权因子和字段命名不一致换数据源等于换一套清洗逻辑。数据源优点缺点适用场景本地CSV离线可用、字段可控、复现稳定需要自己更新大作业、毕设、演示tushare字段丰富、有复权因子token限制、积分门槛需要财务数据的策略akshare免费、无需token接口稳定性一般快速取数、爬虫学习我一般会先从tushare或akshare导出历史日线数据存储为CSV后交给项目跑回测。这样既享受在线数据源的覆盖面又保留本地复现的稳定性。CSV字段约定是date, open, high, low, close, volume日期格式统一为YYYY-MM-DD。这是最通用的OHLCV格式pandas读进来直接能用。3.2 复权处理前复权、后复权和未来函数股票分红除权后价格会留下一个向下跳空缺口。如果不做复权处理回测系统会把这个跳空误判成价格大跌从而产生错误的买卖信号。常见做法是用复权因子把历史价格调整到同一个基准。def adjust_price(df: pd.DataFrame, method: str qfq) - pd.DataFrame: 按复权因子调整OHLC价格 if method qfq: factor df[adj_factor] / df[adj_factor].iloc[0] elif method hfq: factor df[adj_factor] / df[adj_factor].iloc[-1] else: raise ValueError(method must be qfq or hfq) for col in [open, high, low, close]: df[col] df[col] * factor return df这段代码的逻辑是读取复权因子列adj_factor分别计算前复权qfq和后复权hfq的比例系数再对OHLC四列做等比缩放。注意volume不需要复权成交量不受价格缩放影响。这里必须警惕未来函数如果你下载的是已经复权好的前复权数据回测到历史某个日期时当天价格已经包含了之后所有分红除权的影响这本身就是一种未来信息。正确做法是保存原始的adj_factor序列在回测引擎推进到某个日期时只用该日期当日及之前的因子做累计调整。我在这个项目里把复权逻辑放在了数据读取模块引擎每根K线都基于当时的累积因子计算从源头杜绝了这个问题。3.3 交易日历与缺失值清洗A股不是每天都交易遇到国庆、春节这类长假期以及突发临时停牌CSV里会出现日期不连续。回测引擎逐日推进时如果只按CSV里的日期循环可能漏掉停牌日但如果按自然日循环又会把周末当作K线推进导致持仓时间计算错误。def clean_market_data(df: pd.DataFrame) - pd.DataFrame: 清洗去重、排序、过滤非交易日缺失 df[date] pd.to_datetime(df[date]) df df.drop_duplicates(subsetdate).sort_values(date) df df[(df[volume] 0) | (df[close].notna())] return df.reset_index(dropTrue)这段清洗逻辑做三件事按日期去重防止同一天出现两根K线按日期排序保证时间顺序过滤成交量异常为0或价格为空的记录——这类记录通常是停牌日留下的空壳。另一个容易忽略的点是日期类型。CSV读进来后date列默认是字符串直接传给matplotlib画图会变成分类轴画出来的横坐标密集堆在一起。所以数据入口处必须统一做pd.to_datetime()转换这是每个项目都该有的初始化步骤。数据准备这一步做完后面策略代码才能专心处理信号逻辑不用反复怀疑“这个跳空到底是信号还是数据坏点”。这也是我推荐把数据清洗独立成模块的原因调试时只检查一处而不是散落在策略代码里。4. 策略实现拆解MACD双底识别与买卖信号生成策略模块是整个项目的核心。示例策略是基于MACD双底形态的择时策略等MACD指标出现底背离信号后再确认买入属于偏右侧的短线逻辑。它好讲、好验证也方便老师答辩时展开提问。4.1 策略主循环与信号骨架项目里的策略类设计成事件驱动接口回测引擎每天调用一次on_day()策略在on_day()里决定是否下单。这种设计的好处是引擎不关心策略内部用什么指标只要策略给出买卖信号引擎就负责撮合。class MACDDoubleBottomStrategy: def __init__(self, params: dict): self.fast params.get(fast, 12) self.slow params.get(slow, 26) self.signal params.get(signal, 9) self.holding False # 当前是否持仓 def on_day(self, data: pd.DataFrame, idx: int) - str: 输入截至今日的完整行情, 返回 BUY / SELL / HOLD if idx self.slow: return HOLD dif, dea, macd self._calc_macd(data, idx) if self._is_double_bottom(data, idx, dif, dea): return BUY if not self.holding else HOLD if self._is_macd_dead_cross(dif, dea): return SELL if self.holding else HOLD return HOLDon_day方法的输入是截至当日的完整DataFrame和当前索引返回一个信号字符串。这里的关键设计是策略永远只看历史数据不能读取未来。idx之前的行可以随便用idx之后的任何信息都不能碰。on_day内部的调用顺序也讲究先算MACD三值再做双底判断最后看死叉离场。之所以把死叉放在双底后面是因为双底形态成立时往往伴随金叉此时不应立即卖出必须先让买入信号生效。4.2 双底识别与金叉确认MACD双底的识别逻辑是价格创出近期新低但MACD的DIF线没有同步创新低形成底背离随后出现金叉。这里用前值比较和滚动窗口来定位形态代码实现如下def _calc_macd(self, data: pd.DataFrame, idx: int): 计算截至idx位置的DIF、DEA、MACD柱值 close data[close].iloc[:idx 1] if len(close) self.slow: return None, None, None ema_fast close.ewm(spanself.fast, adjustFalse).mean().iloc[-1] ema_slow close.ewm(spanself.slow, adjustFalse).mean().iloc[-1] dif ema_fast - ema_slow dea dif.ewm(spanself.signal, adjustFalse).mean().iloc[-1] macd 2 * (dif - dea) return dif, dea, macd这里用ewm(spanN)直接算指数移动平均pandas内部处理了递归关系不需要手写循环。adjustFalse参数很关键它保证EMA的递归权重和主流行情软件一致——如果改成默认的adjustTrue算出来的MACD值会跟同花顺或通达信对不上答辩时容易被老师问住。双底形态的判断落在另一个私有方法里核心是比较当前DIF值与左侧低点时的DIF值def _is_double_bottom(self, data, idx, dif, dea): if dif is None or dea is None or idx 20: return False recent_low data[low].iloc[idx - 20:idx 1].min() price_breaks data[close].iloc[idx] recent_low * 1.02 prev_dif, prev_dea, _ self._calc_macd(data, idx - 5) if prev_dif is None or prev_dea is None: return False dif_bullish dif prev_dif # 价格新低, DIF未新低 golden_cross (prev_dif prev_dea) and (dif dea) return price_breaks and dif_bullish and golden_cross窗口长度取20对应大约一个月的交易日是量化代码里比较常用的形态观察窗口。price_breaks允许2%的容差避免收盘价刚好卡在低点上方时漏掉信号。dif_bullish判断的是“价格创新低但DIF没有创新低”也就是底背离的核心条件。golden_cross做最后的金叉确认防止在下跌趋势中途接飞刀。4.3 信号到交易订单的映射策略返回的BUY/SELL是字符串回测引擎需要把它转成具体的订单对象设置交易数量和价格约束。def signal_to_order(signal: str, price: float, cash: float, position: int) - dict: if signal BUY: shares int(cash * 0.95 / (price * 100)) * 100 shares min(shares, 10000) if shares 0: return None return {action: buy, price: price, shares: shares} if signal SELL and position 0: return {action: sell, price: price, shares: position} return None买入数量这里有个隐含规则A股以100股为1手买入必须是100的整数倍所以先算出可用现金能买多少整手。0.95是把手续费和滑点的余量预留出来防止计算出的金额刚好卡在资金上限导致委托失败。卖出时直接清仓不带部分卖出的逻辑方便回测结果计算。这个映射逻辑写在策略模块里而不是引擎里是为了让不同策略可以自定义下单规则。比如有的策略想分批建仓有的策略想设置止盈止损单只要在策略里返回不同的order_dict引擎不需要改任何代码。5. 回测避坑指南五条亲身踩过的坑回测代码写出来容易写对很难。下面五条是我在这个项目上实际踩过的坑每条都按“现象、原因、解决”的顺序讲清楚你可以对照自己的回测结果自查。5.1 回测翻倍实盘亏光未来函数现象策略回测结果年化收益超过60%拉到实盘模拟盘跑了一个月净值不仅没涨还亏了8%。对比回测交易记录和实盘成交记录发现实盘的买入时间总比回测晚一天。原因策略代码里用了iloc[idx 1]读取第二天的开盘价作为信号判断依据等于在用“未来”确认今天是否买入。这是回测里最经典的未来函数look-ahead bias在日线级别数据上几乎无法察觉但在逐笔数据上会完全失真。解决把策略内部所有下标访问改成只看idx及之前的数据。我的自查方法是写一个断言在on_day函数入口处加一句assert idx 0 and idx len(data), idx out of range但更关键的是逻辑审查逐个检查data.iloc的下标凡是出现idx nn大于0的地方全部改掉。从那以后我每次写完策略都会强制走一遍这个检查防止盘中临时改代码时带进去新隐患。5.2 涨停板上的成交记录撮合没加涨跌停约束现象回测日志里出现几十笔买入价恰好等于当日涨停价的记录而且全部显示成交。按真实交易规则一字涨停板根本买不进。原因回测引擎撮合时只判断了现金是否足够没有判断当日是否涨停或跌停。买入价等于涨停价时卖单稀少普通账户排在队尾很难成交。解决在引擎撮合函数里加上涨跌停过滤def can_trade(row): limit_up row[close] row[prev_close] * 1.099 limit_down row[close] row[prev_close] * 0.901 return not limit_up and not limit_down注意创业板和科创板的涨跌停幅度是20%ST股是5%所以这个比例要按股票池动态配置。更稳妥的做法是直接比较当日最高价和最低价而不是收盘价如果当日开盘等于涨停价且一直封死则没有成交机会。5.3 净值虚高两个点手续费设太低现象策略每笔交易赚的价差不大靠高频换手累积收益回测年化25%但实盘只有11%差出一倍多。原因回测里手续费设置成了万分之一单边还没算印花税和滑点。A股卖出要交千分之一的印花税佣金实际双边约万三加上买卖价差滑点高频策略的成本远超想象。解决把成本参数设置成接近真实水平config { commission_rate: 0.0003, # 佣金双边 stamp_tax: 0.001, # 印花税卖出 slippage: 0.001, # 滑点比例 }这里我一般会再做一次敏感性测试把滑点从0.001调到0.005看收益下降幅度是否在可接受范围内。如果滑点翻5倍收益直接转负说明策略对交易成本过于敏感实盘基本跑不赢。5.4 最优参数换个时间段就崩过拟合现象用2020-2022年的数据调出来的最优参数EMA快线16、慢线34回测收益45%换到2023年数据上跑收益掉到-3%完全变脸。原因参数寻优时只在单一时间段里找最大值实际上是把历史噪音拟合进了参数里。这类最优参数在样本内表现越好样本外往往越差。解决把数据严格分成训练段和测试段只在训练段调参测试段只用不调。我自己的习惯是留出最后20%的数据作为验证集训练段调好的参数如果能在验证集上跑出正收益才认为策略是稳健的否则宁可放弃这个策略。5.5 除权日净值跳水复权没做现象净值曲线在每年五六月份出现一个突兀的下跳幅度在3%-8%之间但股票价格并没有明显利空。原因数据没有做复权处理。股票分红除权后股价从20元变成19元这1元不是亏损而是权益拆分但回测系统把它当成了真实跌幅。解决回测前统一做前复权处理用第3章里的adjust_price()一次搞定。重要提醒复权必须在策略计算之前做不能等信号生成后再修正否则EMA等指标算出来就是错的。五条坑讲完你会发现它们有一个共同特征回测代码不会报错但结果就是骗人。所以我自己验收回测项目的顺序永远是成本参数、复权、涨跌停、未来函数、过拟合一条条过最后才看收益数字。6. 进阶验证回测引擎、绩效指标与参数寻优前面的路径已经能跑通一个完整回测接下来这一步是把回测结果用一套客观指标量化出来。这个项目最后的输出包括交易记录表和绩效摘要绩效部分是答辩时最容易被追问的地方。6.1 撮合逻辑与成交价设定引擎撮合时按次日开盘价成交这是日线回测的常用设定比用当日收盘价少一层未来信息。具体实现是在引擎内部维护一个待成交队列当日收盘后检查信号次日开盘后用开盘价执行def match(self, signal: str, open_price: float, cash: float, position: int): if signal BUY: price open_price * (1 self.slippage) shares int(cash * 0.95 / (price * 100)) * 100 return shares, price if signal SELL: price open_price * (1 - self.slippage) return position, price这里的slippage是对真实市场冲击成本的简单建模买入时把成交价抬高卖出时压低。真实回测还会区分开盘跳空和盘内滑点但对课程项目和入门实践这个线性模型已经够用。6.2 年化、回撤、夏普怎么算绩效指标里最容易被错误计算的是年化收益率和最大回撤。年化不能简单用总收益除以年数应该用复利方式计算def yearly_return(total_return: float, days: int) - float: return (1 total_return) ** (252 / days) - 1最大回撤的算法是记录历史净值的峰值然后用每个时点净值除以峰值取所有比值的最小值再减1。夏普比率的核心是收益波动率计算时统一用日度收益算标准差再乘根号252转成年化。daily_ret equity.pct_change().dropna() sharpe daily_ret.mean() / daily_ret.std() * np.sqrt(252)这三个指标粗看是公式问题实际上背后藏着交易频率的差异同一策略日频换手和月频换手的夏普比率分母完全不同对比时一定要确认统计口径一致。6.3 参数网格寻优的落地脚本参数寻优是回测项目的加分项写一个双层循环就能跑通results [] for fast in range(8, 21, 2): for slow in range(24, 41, 2): if slow fast: continue ret, sharpe run_backtest(fast, slow) results.append((fast, slow, ret, sharpe)) best max(results, keylambda x: x[3]) print(fbest: fast{best[0]}, slow{best[1]}, sharpe{best[3]:.2f})跑完这个网格你会得到一张参数与绩效的对照表。我习惯把这张表里的最优参数在验证段再跑一遍如果验证段结果排不进前30%就判定这个最优参数不稳定直接放弃。从那以后我每次调参都会强制跑一遍训练段找参数、验证段验效果的流程这个习惯帮我避开了至少三套漂亮但不可用的策略。这套回测项目的价值也正在于此它不保证你调出必赚的策略但能保证策略的收益是在真实成本、真实涨跌停约束、干净数据下计算出来的。希望帮到你。本文还有配套的精品资源点击获取