问题描述
解决方案
在使用聚宽(JoinQuant)进行策略回测时,开启 set_option('use_real_price', True)(真实价格模式)是非常推荐的做法,因为它能更真实地模拟交易账户的资金变动。
然而,这种模式下处理跨天数据时存在一个常见的陷阱。以下是关于为什么直接比较会出错以及如何正确处理的详细解释。
1. 为什么直接比较昨天和前天的收盘价可能会出错?
根本原因在于除权除息(分红、送股、拆股等)。
在真实价格模式下:
- 当天的数据是真实的:你在
handle_data中通过data[security].close或get_current_data()获取到的价格是当天的真实挂牌价格(不复权)。 - 历史API获取的是基于当前日期的前复权价格:当你调用
attribute_history或history时,系统为了保证技术指标(如均线)的连续性,会返回基于当前回测日期进行前复权处理后的历史价格。
举个具体的例子:
假设某股票在 T日 收盘价为 20元。
在 T日收盘后,该股票进行了 10送10 的拆股(除权)。
在 T+1日(第二天),股价自然除权变为 10元(假设没有涨跌)。
如果你采用了“缓存数据”的错误做法:
- T日:你把当天的收盘价
20元存入全局变量g.last_close。 - T+1日:你获取当前的真实价格
10元。 - 比较:你用
10元(T+1日价格) 与20元(T日缓存价格) 比较,计算出跌幅为 -50%。 - 后果:策略会误判为股价暴跌,从而触发错误的止损或卖出信号。
但实际上,股价并没有跌,只是发生了除权。
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
总结
- 开启真实价格:
set_option('use_real_price', True)是为了让账户资产计算更准确(分红到账、拆股后持仓数量自动调整)。 - 不要缓存价格:不要把今天的
data[security].close存到g变量里留给明天用。 - 现用现取:每次需要对比历史数据时,使用
attribute_history、history或get_bars。这些 API 返回的数据在use_real_price=True模式下,都是基于当前日期的前复权数据,保证了与当前价格的连续性和可比性。
Q&A
Q: 开启真实价格模式后,history 函数返回的是什么价格?
A: 在 use_real_price=True 模式下,history、attribute_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)。