🚀 AI 一键生成 joinquant 策略代码

JoinQuant 回测与模拟交易:关键差异与注意事项全解析

joinquant | 发布时间: 2025/12/12
以下内容由 EasyQuant 生成。

问题描述

回测和模拟交易的结果为什么会不一样?

解决方案

回测和模拟交易结果不一致的原因分析

在量化交易中,策略在回测(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),导致回测中买入了停牌股(回测引擎可能允许以停牌前价格买入,或者一直持有不动)。
  • 模拟交易:严格遵循交易所规则,停牌股票无法买卖。

如何减少回测与模拟的差异(解决方案)

  1. 开启真实价格模式:在 initialize 中必须调用 set_option('use_real_price', True)
  2. 消除随机性
    • list(set(stocks))dict.keys() 的结果进行 .sort() 排序,确保顺序固定。
    • 避免使用 random 模块,或在使用前设定固定的 random.seed()
  3. 规避未来函数
    • 开启 set_option("avoid_future_data", True),让系统自动检测并报错常见的未来数据引用。
    • 检查代码,确保没有在盘前获取当天的收盘价、成交量等数据。
  4. 统一环境:确保回测和模拟交易的资金量、起止时间(模拟盘是延续的)、频率(分钟/天)一致。
  5. 日志对比:在关键逻辑处(如选股结果、交易信号)打印 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: 会。回测是基于同一套代码逻辑从头跑到尾。模拟交易中途修改代码后,策略的逻辑变了,但持仓和资金状态是继承自旧代码的,这种逻辑断层会导致结果与静态回测完全不同。建议修改代码后,重新建立一个新的回测和模拟交易进行对比。