ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python量化回测系统实战:从数据清洗到绩效评估的完整搭建指南

Python量化回测系统实战:从数据清洗到绩效评估的完整搭建指南 股票策略回测这件事说简单也简单说复杂也复杂。简单在于核心逻辑无非就是用历史数据模拟交易看看策略赚不赚钱复杂在于当你真正动手去搭一套能用的系统时会发现数据清洗、撮合逻辑、手续费计算、滑点模拟、绩效评估……每一个环节都有坑在等着你。我前后搭过三套回测系统第一套用Excel硬扛第二套自己撸Python第三套才算是真正跑通了一条从数据到结果的完整链路。这篇文章就把我踩过的坑和最终沉淀下来的方案完整分享出来适合有一定Python基础、想自己动手搭建回测框架的朋友参考。1. 先想清楚为什么不用现成平台非要自己搭1.1 现成平台的三个硬伤市面上现成的回测平台不少聚宽、米筐、优矿这些我都用过。它们的好处是开箱即用数据、撮合、评估一条龙。但用久了你会发现几个绕不开的问题。第一是策略保密性。你的策略逻辑必须上传到别人的服务器上运行对于真正有超额收益的策略来说这是很难接受的。第二是撮合细节不可控。平台的撮合规则是黑盒你想模拟涨停板买不进跌停板卖不出这种细节很多平台根本不支持。第三是数据依赖。平台给你什么数据你就用什么数据想接入自己的另类数据源基本没戏。自己搭系统最大的价值就在于完全可控。数据怎么清洗、撮合怎么模拟、手续费怎么算全部由你说了算。这对于做严肃策略研究的人来说是刚需。1.2 自建系统的能力边界需要提前说明的是自建回测系统不是让你去复刻一个商业平台。它的合理定位是服务于个人或小团队的策略验证需求。你不需要支持多用户并发不需要做分布式计算不需要毫秒级撮合模拟。你需要的是一条清晰、可复现、可调试的链路。我最终搭出来的系统核心能力包括支持日线和分钟线回测、支持多标的组合、支持自定义手续费和滑点、支持止损止盈、输出完整的绩效指标和交易明细。这套东西用Python写下来核心代码大概两千行左右完全在个人可控范围内。1.3 技术选型为什么是Python加backtrader技术栈的选择上Python几乎是唯一答案。生态成熟、库丰富、调试方便这些都不用多说。关键问题是自己从零写引擎还是用现成框架我一开始是自己从零写的写了两周后发现光是处理复权、停牌、涨跌停这些边界情况就够喝一壶的。后来转向backtrader发现它把大部分脏活都干了而且源码可读遇到问题能直接改。backtrader的核心优势在于事件驱动架构清晰、支持多数据源、内置大量技术指标、社区活跃。它的学习曲线确实有点陡但一旦理解了它的核心概念Cerebro、Strategy、Data Feed、Analyzer用起来非常顺手。方案开发成本灵活性维护成本推荐场景从零自研极高最高高有特殊撮合需求backtrader中等高低个人策略研究现成平台低低无快速验证想法2. 数据层整条链路里最脏最累的部分2.1 数据源的获取与清洗数据是回测的地基。地基不稳上面盖什么都是空中楼阁。我用过几种数据获取方式各有优劣。免费的有Tushare、AkShare这些优点是零成本缺点是数据质量参差不齐偶尔会有缺失和错误。付费的如Wind、聚宽数据质量好但价格不菲。我目前的方案是用AkShare做日常数据更新关键数据用付费源做交叉验证。数据清洗这一步有几个必须处理的点。首先是复权处理。股票除权除息后价格会跳空如果不做复权回测结果会严重失真。前复权适合回测保持当前价格不变调整历史价格后复权适合看历史收益。其次是停牌处理。停牌期间没有交易但你的持仓还在需要在回测逻辑里正确处理。第三是涨跌停处理。涨停时买不进跌停时卖不出这个细节不处理回测结果会过于乐观。import akshare as ak import pandas as pd # 获取日线数据 df ak.stock_zh_a_hist(symbol000001, perioddaily, start_date20200101, end_date20231231, adjustqfq) # 前复权 # 重命名列统一格式 df df.rename(columns{ 日期: datetime, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: volume }) df[datetime] pd.to_datetime(df[datetime]) df df.set_index(datetime).sort_index() # 检查缺失值 print(df.isnull().sum())2.2 数据存储别小看这一步数据存哪里这个问题我纠结过很久。一开始用CSV简单直接但数据量一大就慢得离谱。后来换SQLite好一些但并发读写还是有问题。最终方案是Parquet加DuckDB。Parquet是列式存储压缩率高、读取快特别适合金融时间序列数据。DuckDB是一个嵌入式分析型数据库查询速度极快而且不需要单独部署服务。两者结合既解决了存储问题又解决了查询问题。import duckdb # 将DataFrame写入Parquet df.to_parquet(data/000001_daily.parquet) # 用DuckDB查询 con duckdb.connect() result con.execute( SELECT * FROM data/000001_daily.parquet WHERE datetime 2023-01-01 ORDER BY datetime ).fetchdf()提示Parquet文件按标的分别存储命名规则建议用代码_周期.parquet方便批量加载。DuckDB可以直接查询Parquet文件不需要先导入。2.3 数据质量校验的几条硬规则数据拿到手别急着往回测引擎里灌。先做几项校验能帮你省掉后面大量的调试时间。第一检查时间连续性。交易日历应该是连续的如果中间缺了几天要么是数据缺失要么是停牌。第二检查价格合理性。最高价应该大于等于开盘价和收盘价最低价应该小于等于开盘价和收盘价这个基本约束必须满足。第三检查成交量。成交量为零但价格有变动这明显是数据错误。我写了一个简单的校验函数每次更新数据后自动跑一遍发现问题就报警。这个习惯帮我抓出过好几次数据源的bug。3. 回测引擎backtrader的核心概念拆解3.1 Cerebro整个系统的大脑backtrader的核心是Cerebro意大利语大脑的意思。你可以把它理解为一个调度器负责协调数据、策略、分析器之间的关系。用Cerebro的标准流程是创建实例、添加数据、添加策略、添加分析器、设置初始资金、运行。看起来简单但每一步都有细节。import backtrader as bt cerebro bt.Cerebro() # 添加数据 data bt.feeds.PandasData(datanamedf) cerebro.adddata(data) # 添加策略 cerebro.addstrategy(MyStrategy) # 设置初始资金 cerebro.broker.setcash(100000.0) # 设置手续费 cerebro.broker.setcommission(commission0.0003) # 运行 cerebro.run()3.2 Strategy策略逻辑的载体Strategy是你写策略逻辑的地方。backtrader提供了一系列回调方法最常用的是next()每个bar都会调用一次。还有notify_order()处理订单状态变化notify_trade()处理交易完成事件。写策略时最容易犯的错误是未来函数。所谓未来函数就是在当前bar使用了未来才能知道的信息。比如用当天的收盘价决定当天的买入这在实盘中是不可能做到的。正确的做法是用昨天的信号决定今天的操作。class MyStrategy(bt.Strategy): params ((fast, 10), (slow, 30),) def __init__(self): self.fast_ma bt.indicators.SMA( self.data.close, periodself.params.fast) self.slow_ma bt.indicators.SMA( self.data.close, periodself.params.slow) self.crossover bt.indicators.CrossOver( self.fast_ma, self.slow_ma) def next(self): if not self.position: if self.crossover 0: self.buy(size100) else: if self.crossover 0: self.close()3.3 Analyzer绩效评估的利器backtrader内置了大量Analyzer可以自动计算夏普比率、最大回撤、年化收益等指标。常用的有SharpeRatio、DrawDown、Returns、TradeAnalyzer。cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedrawdown) cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _nametrades) results cerebro.run() strat results[0] print(f夏普比率: {strat.analyzers.sharpe.get_analysis()}) print(f最大回撤: {strat.analyzers.drawdown.get_analysis()})3.4 多股回测的正确姿势单股回测跑通后下一步就是多股回测。backtrader支持多数据源但有几个坑需要注意。第一数据对齐。不同股票的交易日历可能不同backtrader会自动对齐但你要确保数据中没有重复的日期。第二资金分配。多股回测时资金是共享的你需要决定每只股票分配多少资金。第三信号冲突。同一时间多只股票发出买入信号但资金不够需要有优先级规则。# 添加多只股票 for symbol in [000001, 000002, 000003]: df load_data(symbol) data bt.feeds.PandasData(datanamedf, namesymbol) cerebro.adddata(data) # 在策略中通过self.datas访问多只股票 def next(self): for i, d in enumerate(self.datas): if not self.getposition(d).size: if self.crossover[i] 0: self.buy(datad, size100)4. 撮合与成本模拟决定回测真实度的关键4.1 手续费和滑点的设置很多人回测时忽略手续费和滑点结果实盘一跑发现收益差了一大截。手续费相对好算A股目前是万三左右加上印花税千一卖出时收。滑点则复杂得多它取决于你的交易频率、标的流动性、下单方式。我的经验是日线级别回测滑点设0.1%到0.2%比较合理分钟线级别滑点要设得更高。因为分钟线交易频率高冲击成本更大。# 设置手续费万三佣金 千一印花税 cerebro.broker.setcommission( commission0.0003, # 佣金 marginNone, mult1.0, commtypebt.CommInfoBase.COMM_PERC, stocklikeTrue, percabsTrue ) # 滑点通过自定义Broker或Slippage类实现 class MySlippage(bt.SlippageBase): def __init__(self, slippage_perc): self.slippage_perc slippage_perc def _slip_up(self, price, size, data): return price * (1 self.slippage_perc) def _slip_down(self, price, size, data): return price * (1 - self.slippage_perc)4.2 涨跌停和停牌的处理这是A股回测绕不开的问题。涨停时你买不进跌停时你卖不出停牌时你什么都做不了。backtrader默认不处理这些需要你自己在策略里加判断。我的做法是在next()方法里先检查当前bar是否涨跌停或停牌如果是就跳过交易逻辑。涨跌停的判断标准是收盘价等于涨停价或跌停价。停牌的判断标准是成交量为零。def next(self): # 检查是否停牌 if self.data.volume[0] 0: return # 检查是否涨停简化判断涨幅超过9.8% if self.data.close[0] / self.data.close[-1] - 1 0.098: return # 涨停不买 # 检查是否跌停 if self.data.close[0] / self.data.close[-1] - 1 -0.098: return # 跌停不卖 # 正常交易逻辑 ...4.3 订单类型的选择backtrader支持多种订单类型市价单、限价单、止损单、止损限价单等。回测时用哪种取决于你的策略逻辑。市价单最简单按当前bar的开盘价或收盘价成交。限价单需要指定价格只有价格触及才成交。止损单用于风控价格跌破某个阈值时自动卖出。我的建议是回测时尽量用市价单因为限价单的成交模拟很难做到准确。实盘中限价单可能部分成交回测中很难模拟这个细节。如果策略必须用限价单那回测结果要打个折扣看。5. 绩效评估别只看收益率5.1 核心指标的计算与解读收益率只是最表面的指标。真正评估一个策略需要看一组指标夏普比率、最大回撤、卡玛比率、胜率、盈亏比、换手率。夏普比率衡量的是单位风险带来的超额收益一般认为大于1算不错大于2算优秀。最大回撤衡量的是最坏情况下的亏损幅度这个指标直接关系到你能不能拿得住。卡玛比率是年化收益除以最大回撤衡量的是收益回撤比。指标计算公式参考标准夏普比率(年化收益-无风险利率)/年化波动率1良好2优秀最大回撤max(峰值到谷底的跌幅)越小越好20%较优卡玛比率年化收益/最大回撤1良好2优秀胜率盈利交易数/总交易数视策略类型而定盈亏比平均盈利/平均亏损1.5较好5.2 回测结果的常见陷阱回测结果好看不代表策略能用。有几个陷阱必须警惕。第一是过拟合。你在历史数据上反复调参调到最优但这个最优参数只适用于历史未来未必有效。判断过拟合的方法是把数据分成样本内和样本外样本内调参样本外验证。如果样本外表现大幅下降说明过拟合了。第二是幸存者偏差。如果你只用现在还在上市的股票做回测那些退市的股票就被忽略了结果会偏乐观。正确的做法是使用包含退市股票的全样本数据。第三是前视偏差。前面提到的未来函数就是一种前视偏差。还有一种隐蔽的前视偏差是使用了后来才发布的数据。比如用2023年的财报数据去回测2022年的交易但2022年时这份财报还没发布。5.3 从回测到实盘的差距回测和实盘之间永远存在差距。这个差距来自几个方面滑点比预期大、流动性比预期差、心理因素影响执行、市场结构发生变化。我的经验是回测结果打个七折再考虑实盘。如果打七折后还能接受那这个策略值得一试。实盘初期用小资金跑三个月到半年看看实际表现和回测的偏差有多大再决定是否加大资金。6. 系统集成把各个模块串起来6.1 项目结构的设计一个可维护的回测系统项目结构要清晰。我的结构是这样的backtest_system/ ├── data/ # 数据存储 │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── src/ │ ├── data_loader.py # 数据加载 │ ├── strategy.py # 策略定义 │ ├── engine.py # 回测引擎封装 │ ├── analyzer.py # 绩效分析 │ └── utils.py # 工具函数 ├── config/ │ └── settings.py # 配置文件 ├── results/ # 回测结果输出 └── main.py # 入口这样的结构数据、逻辑、配置分离改起来方便也容易复用。6.2 配置化的参数管理策略参数不要硬编码在代码里用配置文件管理。这样你可以快速切换参数组合做参数敏感性分析。# config/settings.py CONFIG { initial_cash: 100000, commission: 0.0003, slippage: 0.001, strategy_params: { fast_ma: 10, slow_ma: 30, stop_loss: 0.05, }, data: { symbols: [000001, 000002], start_date: 2020-01-01, end_date: 2023-12-31, } }6.3 批量回测与参数优化单次回测跑通后下一步是批量回测。比如你想测试不同均线组合的效果可以写一个循环遍历所有参数组合记录每组的结果。backtrader本身支持参数优化用cerebro.optstrategy()代替cerebro.addstrategy()然后传入参数范围。但要注意参数优化很容易导致过拟合结果要谨慎解读。cerebro.optstrategy( MyStrategy, fastrange(5, 20, 5), slowrange(20, 60, 10) ) # 运行优化 results cerebro.run(maxcpus1)注意参数优化时一定要留出样本外数据做验证。否则你找到的只是历史最优不是未来最优。7. 我踩过的几个典型坑7.1 复权方式选错导致收益虚高刚开始做回测时我用的是不复权数据。结果发现有些股票在某一天突然跌了50%策略触发止损卖出然后第二天又涨回来。后来才明白那是除权除息导致的不是真实下跌。换成前复权数据后这个问题就消失了。这个坑的教训是回测必须用复权数据前复权适合回测后复权适合看长期收益。7.2 忽略停牌导致虚假成交有一次回测一只停牌股发现策略在停牌期间还有交易记录。查了半天才发现backtrader默认不会跳过停牌日需要自己在策略里判断。加上停牌判断后回测结果更真实了。7.3 手续费设置遗漏印花税A股卖出时有千一的印花税我一开始只设了佣金忘了印花税。结果高频策略的回测收益虚高了不少。加上印花税后收益直接降了一截。这个细节虽小但对高频策略影响很大。7.4 多股回测时的资金分配问题多股回测时我一开始给每只股票平均分配资金。后来发现有些股票长期没有信号资金就闲置了。改成动态分配后资金利用率提高了收益也更合理。8. 后续可以怎么扩展这套系统跑通后可以往几个方向扩展。一是接入更多数据源比如财务数据、舆情数据做多因子策略。二是加入机器学习模块用模型生成信号。三是做实时模拟盘用实时数据跑策略但不真实下单观察策略在实盘环境下的表现。我个人觉得最有价值的扩展方向是实时模拟盘。回测再真实也是历史数据。实时模拟盘能让你看到策略在当前市场环境下的真实表现这个反馈比回测结果有价值得多。最后分享一个小技巧回测系统的日志一定要详细。每笔交易的信号、下单、成交、盈亏都要记录。出问题时日志是唯一的排查依据。我现在的系统每笔交易都会输出一行结构化日志包含时间、标的、方向、价格、数量、盈亏、持仓等信息。这个习惯帮我快速定位过好几次逻辑bug。
返回列表