问题描述
解决方案
回测和模拟交易结果不一致的原因分析
在量化交易中,策略在回测(Backtest)和模拟交易(Paper Trading)中表现不一致是一个非常常见且重要的问题。这通常不是由单一原因造成的,而是数据环境、代码逻辑、撮合机制等多方面因素共同作用的结果。
以下是导致回测与模拟交易结果出现偏差的主要原因及详细解析:
1. 未来函数(Future Data)
这是导致回测表现优异但模拟盘亏损的最常见原因。
- 定义:策略在回测时使用了当时那个时间点本不应该获取到的数据。
- 差异:
- 回测:历史数据是全量的,代码可能无意中调用了收盘后才有的数据(如在09:30就获取了当天的收盘价、最高价,或使用了当季度的财报数据但当时财报尚未发布)。
- 模拟交易:时间是线性流动的,系统严格限制只能获取当前时刻之前的数据,无法预知未来,因此无法复现回测中的“神操作”。
- 典型案例:在
before_trading_start中使用当天的close价格进行选股。
2. 策略中的随机性因素
如果策略逻辑中包含不确定的随机因素,每次运行的结果都会不同。
- 无序容器遍历:Python 中的
dict(字典)和set(集合)在迭代时顺序是不固定的(尤其是在不同 Python 版本或解释器重启后)。如果策略逻辑是“取股票池中的前5只买入”,而股票池存储在set中,回测和模拟盘选出的股票可能完全不同。 - 随机函数:使用了
random库生成随机数,且没有固定随机种子(seed)。 - 排序不稳定:当排序条件相同时,不同环境下的排序算法可能导致次序不同。
3. 撮合机制与滑点(Slippage)
回测是基于历史数据的理论模拟,而模拟交易更接近真实市场环境。
- 撮合逻辑:
- 回测:通常假设流动性充足,只要价格触及即可成交,且通常按 Bar 的收盘价或开盘价成交。
- 模拟交易:如果开启了盘口撮合(
match_with_order_book),系统会根据买一/卖一价和盘口挂单量进行撮合。如果对手盘不足,可能无法成交或部分成交。
- 滑点设置:回测中如果未设置滑点或滑点设置过小,会高估收益。模拟交易中,受限于实时行情波动,实际成交价往往比预期差。
4. 数据差异与复权模式
- 动态复权 vs 静态复权:
- 回测:建议开启
set_option('use_real_price', True)(真实价格模式)。如果关闭此选项,回测使用的是基于回测结束日期的前复权数据,这会导致历史价格与当时真实价格不符,计算买入股数时产生偏差。 - 模拟交易:默认使用真实价格。如果回测没开真实价格模式,两者的持仓数量和资金利用率会有显著差异。
- 回测:建议开启
- 数据更新延迟:模拟盘可能有微小的系统延迟(如10秒),或者行情数据到达的微小时间差,导致触发交易的信号点不同。
5. 运行环境与代码变更
- 代码替换:回测是一次性跑完历史数据。而在模拟交易中,用户可能会中途修改策略代码、调整参数或暂停策略。这些人为干预会导致模拟盘状态与回测状态脱节。
- 初始化状态:模拟交易重启时会从数据库恢复
g全局变量和context,如果代码中有无法被序列化保存的对象(如数据库连接、打开的文件句柄),重启后会丢失或报错,而回测不会经历重启。 - Python版本:虽然聚宽目前主要支持 Python 3,但如果回测和模拟交易使用的 Python 版本不一致(极少情况),可能会导致浮点数计算精度或语法特性的差异。
6. 停牌与退市处理
- 回测:虽然包含退市股票,但部分策略可能未妥善处理停牌股票(如未开启
skip_paused=True),导致回测中买入了停牌股(回测引擎可能允许以停牌前价格买入,或者一直持有不动)。 - 模拟交易:严格遵循交易所规则,停牌股票无法买卖。
如何减少回测与模拟的差异(解决方案)
- 开启真实价格模式:在
initialize中必须调用set_option('use_real_price', True)。 - 消除随机性:
- 对
list(set(stocks))或dict.keys()的结果进行.sort()排序,确保顺序固定。 - 避免使用
random模块,或在使用前设定固定的random.seed()。
- 对
- 规避未来函数:
- 开启
set_option("avoid_future_data", True),让系统自动检测并报错常见的未来数据引用。 - 检查代码,确保没有在盘前获取当天的收盘价、成交量等数据。
- 开启
- 统一环境:确保回测和模拟交易的资金量、起止时间(模拟盘是延续的)、频率(分钟/天)一致。
- 日志对比:在关键逻辑处(如选股结果、交易信号)打印
log.info。对比回测日志和模拟交易日志,定位第一处出现差异的地方。
Q&A 常见问题解答
Q1: 为什么我的策略在回测中收益很高,一上模拟盘就不交易或亏损?
A: 最可能的原因是使用了未来函数。例如在 before_trading_start 中获取了当天的 close 价格来判断涨跌,这在回测中能拿到数据,但在模拟盘实盘时刻,当天的收盘价还未产生,导致信号无法触发或触发错误。
Q2: 为什么使用字典(Dict)存储股票会导致结果不一致?
A: 在 Python 中,字典和集合是无序的(尤其是在 Python 3.6 之前,虽然后续版本优化了顺序,但仍不建议依赖其顺序)。如果策略逻辑是 for stock in g.security_dict: 然后买入前3只,每次运行遍历的顺序可能不同,导致买入的股票不同。建议先将键值转为列表并排序:for stock in sorted(g.security_dict.keys()):。
Q3: 开启“盘口撮合”对模拟交易有什么影响?
A: 开启 set_option('match_with_order_book', True) 后,模拟交易会更严格地模拟真实市场的流动性。如果您的策略资金量较大或交易不活跃的标的,可能会因为盘口挂单不足而无法成交,或者成交价格比回测(通常假设无限流动性)更差,导致收益低于回测。
Q4: 模拟交易中途修改代码会影响结果吗?
A: 会。回测是基于同一套代码逻辑从头跑到尾。模拟交易中途修改代码后,策略的逻辑变了,但持仓和资金状态是继承自旧代码的,这种逻辑断层会导致结果与静态回测完全不同。建议修改代码后,重新建立一个新的回测和模拟交易进行对比。