🚀 AI 一键生成 joinquant 策略代码

JoinQuant 深度剖析:为什么我的回测结果和模拟交易对不上?

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

问题描述

我的策略回测收益很高,但模拟交易结果却不一样,可能是什么原因?

解决方案

回测收益高但模拟交易(或实盘)表现不一致,是量化交易中非常常见的问题,通常被称为“回测与实盘的偏差”。在 JoinQuant 聚宽平台上,造成这种差异的原因主要集中在未来函数撮合机制差异数据处理方式以及代码逻辑的不确定性上。

以下是导致回测与模拟交易结果不一致的详细原因分析及排查建议:

1. 未来函数(Future Data)

这是最常见的原因。策略在回测时“看到”了当时不应该知道的数据。

  • 现象:在开盘前(如 09:00)或开盘时(09:30)就获取了当天的收盘价、最高价或最低价进行决策。
  • JoinQuant 特性
    • 在回测中,如果代码逻辑不严谨,很容易获取到当天的全天数据。
    • 排查:检查代码中是否在 before_trading_start09:30handle_data 中使用了当天的 closehighlow 等数据。
    • 解决:在 initialize 中开启防未来数据选项:
      set_option("avoid_future_data", True)
      

2. 复权与真实价格模式(Real Price Mode)

回测和模拟交易对价格的处理机制可能不同,导致信号触发点不一致。

  • 现象:回测通常默认使用前复权价格,而模拟交易面对的是市场的真实价格(不复权),仅在发生分红派息时调整持仓。
  • JoinQuant 特性
    • 如果不开启真实价格模式,模拟盘会使用基于创建日期的后复权价格,这可能导致与行情软件看到的价格不一致,且容易产生理解偏差。
    • 解决:强烈建议在 initialize 中开启真实价格模式,使回测和模拟环境保持一致:
      set_option('use_real_price', True)
      

3. 撮合机制与流动性差异

回测通常是理想化的,而模拟交易更接近真实市场环境。

  • 盘口撮合
    • 回测:通常只要价格触及即可成交,且默认成交量限制较宽松(如不超过当日总量的 25%)。
    • 模拟交易:如果开启了盘口撮合(match_with_order_book),系统会根据买一/卖一的挂单量进行严格撮合。如果对手盘量不足,订单可能无法成交或仅部分成交。
  • 滑点(Slippage)
    • 回测中滑点通常是固定的(如设置万分之二)。
    • 模拟交易中,受限于实时盘口深度,大额订单产生的实际滑点可能远高于回测设定。
  • 涨跌停限制
    • 模拟盘在下单时会检查从开盘到下单时刻的累积成交量,若为 0(如一字涨跌停),会直接拒绝订单。回测中有时可能因为数据精度问题未能完美过滤。

4. 代码逻辑中的随机性

如果策略代码中包含不确定的逻辑,会导致每次运行结果都不同。

  • 字典遍历(Dict Iteration):在 Python(尤其是旧版本)中,字典的遍历顺序是不固定的。如果策略逻辑依赖于 for stock in context.portfolio.positions 的顺序来卖出股票,可能导致回测和模拟的卖出顺序不同,进而影响资金占用和后续买入。
  • 随机函数:使用了 random 库但未固定随机种子。
  • 解决:对列表或字典键进行排序后再遍历:
    # 推荐做法
    for stock in sorted(context.portfolio.positions.keys()):
        # 逻辑处理
    

5. 运行环境与数据的微小差异

  • 停牌处理:回测数据是清洗过的,而模拟交易使用的是实时数据流。某些临时停牌或复牌在实时数据中可能存在延迟。
  • 数据更新时间:回测使用的是收盘后校对过的静态数据;模拟交易使用的是实时推送数据,可能存在毫秒级的延迟或瞬时的脏数据(Tick 级策略尤为明显)。
  • Python 版本:确保回测和模拟交易使用的 Python 版本一致(推荐 Python 3)。

6. 过度拟合(Overfitting)

  • 现象:策略参数是针对历史数据“训练”出来的(例如专门挑选了过去表现最好的均线参数)。
  • 结果:这种策略在已知历史数据(回测)中表现完美,但在未知数据(模拟交易/未来市场)中往往失效。这不是技术故障,而是策略逻辑本身的缺陷。

总结与建议排查步骤

  1. 强制一致性设置:在策略的 initialize 函数中加入以下代码:
    def initialize(context):
        set_option('use_real_price', True)      # 开启真实价格
        set_option("avoid_future_data", True)   # 开启防未来数据
        # 统一手续费设置
        set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
  2. 检查日志:对比回测日志和模拟交易日志。查看同一时间点的买卖信号、持仓数量、可用资金是否一致。
  3. 固定逻辑顺序:确保所有涉及循环遍历的地方(如股票池遍历)都进行了排序操作。
  4. 关注未成交订单:在模拟交易中,经常检查 get_open_orders(),看是否有因为涨跌停或流动性不足导致的废单。

Q&A

Q: 什么是“未来函数”?如何避免?
A: 未来函数是指在策略回测时使用了当时时间点实际上无法获取的数据(如在周一开盘时就使用了周一的收盘价)。在 JoinQuant 中,可以通过 set_option("avoid_future_data", True) 来让系统自动检测并报错,同时编程时应避免在盘中直接调用当天的 close 数据,除非是分钟级回测且只调用已结束分钟的数据。

Q: 为什么开启 use_real_price 很重要?
A: 开启后,回测和模拟交易都会基于真实的除权除息机制运行,账户会在分红送股日自动调整持仓和现金,这与真实的股票账户逻辑一致。如果不开启,系统会使用后复权价格模拟,虽然计算结果理论上近似,但在处理分红税费、挂单价格精度以及模拟盘对接时容易产生偏差。

Q: 模拟交易中订单一直未成交(Open状态)是什么原因?
A: 这通常是因为开启了盘口撮合(match_with_order_book)或受限于流动性。如果您的委托价格偏离当前盘口价格太远,或者标的处于涨跌停状态、停牌状态,订单就会一直处于 Open 状态直到收盘自动撤销。建议检查下单价格逻辑(如是否使用了市价单或合理的限价单)。