问题描述
解决方案
在聚宽(JoinQuant)平台中,找出代码耗时瓶颈的最直接、最有效的方法是使用内置的性能分析函数 enable_profile()。
该函数可以详细统计策略中每一行代码的执行次数、总耗时以及占用总运行时间的百分比。
以下是具体的使用步骤和说明:
1. 如何使用 enable_profile()
你需要将 enable_profile() 函数放在策略代码的最上方(所有函数之外),或者在 initialize 函数的第一行调用。
代码示例:
# -*- coding: utf-8 -*-
# 开启性能分析(请在所有代码之前调用)
enable_profile()
def initialize(context):
# 正常的初始化代码
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
run_daily(market_open, time='every_bar')
def market_open(context):
# 你的策略逻辑
security = '000001.XSHE'
close_data = attribute_history(security, 5, '1d', ['close'])
# ... 其他逻辑
2. 查看分析结果
- 点击“运行回测”:性能分析功能仅在点击编辑器右上角的 “运行回测”(Full Backtest)时才会生效。点击“编译运行”是看不到分析结果的。
- 查看日志/结果页面:回测完成后,在回测结果页面的“日志”或“性能分析”标签页中,你会看到类似下面的统计表格。
结果示例解读:
// 函数名及所在行号
Function: market_open at line 10
// Line #: 行号
// Hits: 该行代码执行的次数
// Time: 该行代码总耗时(单位通常是微秒)
// Per Hit: 每次执行平均耗时
// % Time: 该行耗时占整个函数运行时间的百分比(重点关注)
Line # Hits Time Per Hit % Time Line Contents
==============================================================
10 def market_open(context):
11 240 500 2.1 0.1 security = '000001.XSHE'
12 240 5000000 20833.3 95.5 close_data = attribute_history(...)
13 240 1000 4.2 0.1 ...
分析方法:
- 重点关注
% Time列数值最高的行。 - 关注
Hits(执行次数)非常大的行,这通常意味着循环过多或在不必要的地方(如每分钟)重复计算了不需要频繁更新的数据。
3. 注意事项与建议
- 缩短回测时间:开启
enable_profile()本身会显著增加回测的耗时。因此,不要跑全量回测。建议将回测时间范围缩短到 1周或1个月,足以暴露出代码的性能瓶颈。 - 用完即删:找到问题并优化代码后,务必删除或注释掉
enable_profile(),否则正式回测会非常慢。 - 常见耗时原因:
- 在循环中获取数据:例如在
for循环中反复调用get_price或attribute_history。应尽量批量获取数据(传入股票列表)后再处理。 - 频繁的 I/O 操作:过多的
log.info或print输出会严重拖慢速度。 - 重复计算:在
handle_data(分钟级)中计算了其实只需要每天算一次的数据。
- 在循环中获取数据:例如在
Q&A
Q: enable_profile() 可以在模拟交易中使用吗?
A: 不可以。该函数是回测环境专用的 API,仅用于策略开发和调试阶段的性能优化。
Q: 为什么我加了 enable_profile() 还是没看到输出?
A: 请确认你点击的是 “运行回测” 按钮,而不是“编译运行”。此外,确保代码没有在初始化阶段就报错退出。
Q: 既然 enable_profile 会拖慢速度,有没有不使用它也能大致判断瓶颈的方法?
A: 可以通过观察日志输出的时间戳,或者手动在代码关键位置插入 import time; s=time.time() 和 print(time.time()-s) 来计算特定代码块的耗时,但这比使用内置工具要繁琐得多。
Q: 批量获取数据真的比循环获取快很多吗?
A: 是的。例如 history(count, unit, field, security_list) 一次性获取 100 只股票的数据,比写一个 100 次的循环每次获取一只股票要快得多,因为减少了与底层数据库交互的次数和上下文切换的开销。