🚀 AI 一键生成 joinquant 策略代码

JoinQuant 回测高级技巧:为何不能在真实价格模式下跨日期缓存历史数据?

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

问题描述

在使用真实价格回测时,为什么直接比较昨天和前天的收盘价可能会出错?应该如何正确处理跨天的数据?

解决方案

在使用聚宽(JoinQuant)进行策略回测时,开启 set_option('use_real_price', True)(真实价格模式)是非常推荐的做法,因为它能更真实地模拟交易账户的资金变动。

然而,这种模式下处理跨天数据时存在一个常见的陷阱。以下是关于为什么直接比较会出错以及如何正确处理的详细解释。

1. 为什么直接比较昨天和前天的收盘价可能会出错?

根本原因在于除权除息(分红、送股、拆股等)

在真实价格模式下:

  • 当天的数据是真实的:你在 handle_data 中通过 data[security].closeget_current_data() 获取到的价格是当天的真实挂牌价格(不复权)。
  • 历史API获取的是基于当前日期的前复权价格:当你调用 attribute_historyhistory 时,系统为了保证技术指标(如均线)的连续性,会返回基于当前回测日期进行前复权处理后的历史价格。

举个具体的例子:

假设某股票在 T日 收盘价为 20元
T日收盘后,该股票进行了 10送10 的拆股(除权)。
T+1日(第二天),股价自然除权变为 10元(假设没有涨跌)。

如果你采用了“缓存数据”的错误做法:

  1. T日:你把当天的收盘价 20元 存入全局变量 g.last_close
  2. T+1日:你获取当前的真实价格 10元
  3. 比较:你用 10元 (T+1日价格) 与 20元 (T日缓存价格) 比较,计算出跌幅为 -50%
  4. 后果:策略会误判为股价暴跌,从而触发错误的止损或卖出信号。

但实际上,股价并没有跌,只是发生了除权。

2. 应该如何正确处理跨天数据?

为了避免上述问题,处理跨天数据的核心原则是:不要跨日期缓存价格数据,每次需要比较时,都重新获取历史数据。

因为聚宽的数据获取函数(如 attribute_history)会自动根据当前的复权因子,将过去的价格调整为与当前价格具备可比性的前复权价格

正确的做法:

在每一天(或每一分钟)运行策略时,通过 API 重新拉取过去 N 天的数据。

场景:比较今天价格与昨天收盘价

def handle_data(context, data):
    security = '000001.XSHE'
    
    # 【正确做法】
    # 获取过去2天的收盘价(包含昨天和前天,或者今天和昨天,取决于调用时机)
    # 注意:attribute_history 默认不包含当前时刻的数据(避免未来函数),
    # 但它返回的数据是基于“今天”的复权因子处理过的,与今天的真实价格是连续的。
    
    # 获取过去1天的历史数据(即昨天的收盘价)
    # 系统会自动将昨天的价格按照今天的复权因子进行处理
    hist = attribute_history(security, 1, '1d', ['close'])
    yesterday_close = hist['close'][-1]
    
    # 获取当前的真实价格
    current_price = data[security].close
    
    # 此时比较是安全的,因为 yesterday_close 已经被系统处理为前复权价格
    # 如果发生了10送10,yesterday_close 会被API自动调整为 10元(原价20元)
    # current_price 为 10元
    # 涨幅计算结果为 0%,符合逻辑
    if current_price > yesterday_close * 1.05:
        order(security, 100)

错误的做法(请勿模仿):

# 【错误做法】试图使用全局变量缓存昨天的价格
def initialize(context):
    set_option('use_real_price', True)
    g.last_price = None

def handle_data(context, data):
    security = '000001.XSHE'
    current_price = data[security].close
    
    # 这里的 g.last_price 是上一交易日的“真实价格”
    # 如果昨晚发生了除权,今天的 current_price 和 g.last_price 就不在同一个价格基准上了
    if g.last_price is not None:
        if current_price > g.last_price * 1.05: # 逻辑错误!
            order(security, 100)
            
    # 缓存今日价格供明天使用
    g.last_price = current_price

总结

  1. 开启真实价格set_option('use_real_price', True) 是为了让账户资产计算更准确(分红到账、拆股后持仓数量自动调整)。
  2. 不要缓存价格:不要把今天的 data[security].close 存到 g 变量里留给明天用。
  3. 现用现取:每次需要对比历史数据时,使用 attribute_historyhistoryget_bars。这些 API 返回的数据在 use_real_price=True 模式下,都是基于当前日期的前复权数据,保证了与当前价格的连续性和可比性。

Q&A

Q: 开启真实价格模式后,history 函数返回的是什么价格?
A: 在 use_real_price=True 模式下,historyattribute_history 等函数返回的是基于当前回测日期前复权价格。这意味着你在不同日期调用同一个历史时间点的数据,得到的值可能不同(因为复权因子变了)。

Q: 如果我非要获取昨天的“真实”收盘价(不复权)怎么办?
A: 你可以使用 get_price 函数,并将 fq 参数设置为 None

# 获取不复权的真实历史价格
df = get_price(security, end_date=context.previous_date, count=1, frequency='daily', fields=['close'], fq=None)
real_yesterday_close = df['close'][-1]

但在编写策略逻辑(如均线突破)时,通常不建议使用不复权价格,因为除权造成的缺口会破坏技术指标的计算。

Q: 模拟交易中是否也需要这样处理?
A: 是的。模拟交易和回测的逻辑是一致的。且官方强烈建议在模拟交易中也开启 set_option('use_real_price', True)