问题描述
g.stockDF = get_price(initial_list, end_date=context.previous_date, frequency='daily', fields=['close', 'high_limit', 'low_limit', 'money'], count=250, panel=False)
为了加快回测速度,我已经把策略里所有get_price合并成一个250日大Dataframe,每日只获取一次。
想再进一步优化,我在考虑每日缓存250日的DF,然后第二天只获取count=1,添加到缓存,再丢弃最老一天记录。
于是问题来了,请问count=250和count=1的请求速度有显著区别吗?缓存数据,每日只取最新是否值得?
经本人实测回测,一年又一个月仅快了8秒。
本来一行能弄完的事情写了两天。反复优化后最终精简为31行代码。
实测回测1年又1个月仅把代码段运行速度从
Total time: 48.9142 s
File: /tmp/strategy/user_code.py
减少到
Total time: 40.3312 s
File: /tmp/strategy/user_code.py
可谓毫无意义。
建议大家也别浪费coding时间在优化get price上
原来:
g.stockDF = get_price(initial_list, end_date=context.previous_date, frequency='daily', fields=['close', 'high_limit', 'money'], count=250, panel=False)
新的:
#更新 g.stockDF 函数
def updatestockDF(context, initial_list):
yesterday = context.previous_date
newstocklist = []
updatestocklist = []
removestocklist =[]
# 初始化
if g.stockDF is None:#空的
g.stockDF = get_price(initial_list, end_date=yesterday, frequency='daily', fields=['close', 'high_limit', 'money'], count=250, panel=False)
# 准备stockDF中股票代码的列表
stocklist_in_stockDF = g.stockDF['code'].values.tolist()
#检测在initial_list,但不在g.stockDF中股票 (更新250日)
newstocklist = list(set(initial_list).difference(stocklist_in_stockDF))
#检测在initial_list,也在g.stockDF中股票 (更新1日)
#updatestocklist = list(set(initial_list) & stocklist_in_stockDF)
updatestocklist = list(set(initial_list).intersection(stocklist_in_stockDF))
#检测在g.stockDF中,但不在initial_list中的股票 (删除)
removestocklist = list(set(stocklist_in_stockDF).difference(initial_list))
#获取newstocklist的信息进入newstockDF,合并进g.stockDF
if newstocklist: #非空列表返回True
newstockDF = get_price(newstocklist, end_date=yesterday, frequency='daily', fields=['close', 'high_limit', 'money'], count=250, panel=False)
g.stockDF = g.stockDF.append(newstockDF)
#获取updatestocklist的1日信息至updatestockDF,合并
if updatestocklist: #非空列表返回True
updatestockDF = get_price(updatestocklist, end_date=yesterday, frequency='daily', fields=['close', 'high_limit', 'money'], count=1, panel=False)
g.stockDF = g.stockDF.append(updatestockDF)
# 删除最老的第251日数据的代码
g.stockDF = g.stockDF.sort_values('time').groupby('code').tail(250)
#删除stockDF中removestocklist相关的股票信息
if removestocklist: #非空列表返回True
g.stockDF = g.stockDF[~g.stockDF['code'].isin(removestocklist)]
解决方案
JoinQuant 平台 get_price 函数性能优化分析
在 JoinQuant 平台上编写量化策略时,数据获取往往是影响回测速度的关键因素之一。许多开发者会尝试各种方法来优化 get_price 函数的调用,例如将多次调用合并为一次,或者尝试自己实现数据缓存机制。本文将针对“缓存数据,每日只取最新是否值得”这一问题进行深入探讨。
1. get_price 的底层机制与 IO 速度
在 JoinQuant 的回测引擎中,get_price 函数的底层实现已经经过了高度优化。平台通常会在内存中缓存常用的行情数据,或者使用高效的数据库查询机制。
- 批量获取 (count=250) vs 单次获取 (count=1):
- 当您请求
count=250时,底层系统执行一次查询,返回一个较大的数据块。 - 当您请求
count=1时,底层系统同样执行一次查询,返回一个较小的数据块。 - 关键点在于:在 JoinQuant 的架构下,这两种请求的 IO 延迟差异非常小。网络传输和数据库查询的开销主要集中在“建立连接”和“发起请求”上,而不是数据量的大小(在合理范围内,如 250 条记录)。
- 当您请求
2. 缓存机制的成本与收益分析
您尝试实现的缓存机制逻辑如下:
- 识别新加入股票池的股票,获取 250 日数据。
- 识别已在股票池的股票,获取 1 日数据。
- 识别移出股票池的股票,从缓存中删除。
- 合并新数据,并截断保留最近 250 日数据。
收益:
- 减少了
get_price返回的数据量(从 250 日减少到 1 日)。
成本:
- 复杂的集合运算:计算
newstocklist,updatestocklist,removestocklist需要进行多次集合操作。 - DataFrame 的拼接与截断:
g.stockDF.append()和g.stockDF.sort_values('time').groupby('code').tail(250)是非常耗时的 Pandas 操作。特别是groupby和sort_values,在数据量较大时,其计算开销远大于直接调用一次get_price获取 250 日数据的 IO 开销。
3. 实测结果验证
正如您实测的结果所示,经过复杂的代码编写和优化,回测 1 年又 1 个月的时间仅从 48.9 秒减少到 40.3 秒,提升幅度微乎其微(约 17%)。
这证明了:在 JoinQuant 平台上,为了减少 get_price 的 count 参数而自行实现复杂的 DataFrame 缓存和拼接逻辑,是得不偿失的。 Pandas 的数据处理开销抵消了甚至超过了减少数据传输带来的微小收益。
4. 优化建议
如果您希望进一步提升回测速度,建议从以下几个方面入手,而不是纠结于 get_price 的缓存:
- 合并请求(已做):您已经将策略里所有
get_price合并成一个大 DataFrame,每日只获取一次,这是最有效的优化手段。尽量避免在循环中调用get_price。 - 关闭缓存 (慎用):如果策略内存占用极大导致频繁被杀,可以尝试
disable_cache(),但这通常会导致速度下降,仅作为解决内存超限的手段。 - 优化 Pandas 操作:检查策略中是否有其他低效的 Pandas 操作,例如在循环中使用
.loc或.iloc逐行赋值,或者频繁的append/concat。尽量使用向量化操作。 - 使用
history或attribute_history:在某些特定场景下,如果只需要单只股票的少量字段,history或attribute_history可能会比get_price稍快,但差异通常不大。 - 开启性能分析:使用
enable_profile()函数,找出策略中真正耗时的代码行,进行针对性优化。
总结
在 JoinQuant 平台上,直接使用 get_price(..., count=250) 获取所需数据是最高效且代码最简洁的方式。自行维护 DataFrame 缓存不仅增加了代码复杂度,而且由于 Pandas 的计算开销,往往无法带来显著的性能提升。将精力集中在策略逻辑的向量化和减少不必要的循环上,才是提升回测速度的正道。