问题描述
解决方案
在 JoinQuant(聚宽)平台中,handle_data 和 run_daily 都是用于驱动策略逻辑的核心函数,但它们的设计理念和使用场景有所不同。
简短的回答是:强烈建议使用 run_daily(及其衍生的 run_weekly, run_monthly)定时函数。 它是目前平台推荐的写法,比 handle_data 更灵活、更易于管理代码结构。
以下是详细的对比分析和使用建议。
1. 核心区别对比表
| 特性 | handle_data (旧式/简单) | run_daily (新式/推荐) |
|---|---|---|
| 触发机制 | 被动触发:每生成一根 K 线(Bar)就运行一次。 | 主动调度:根据设定的具体时间(如 '09:30', '14:50')或频率运行。 |
| 参数 | (context, data):包含 data 对象,可直接取当前切片数据。 |
(context):不包含 data 对象,需使用 API (如 get_price) 获取数据。 |
| 灵活性 | 低:所有逻辑(开盘、盘中、收盘)都必须写在一个函数里,代码容易臃肿。 | 高:可以将开盘选股、盘中交易、收盘风控拆分为不同的函数,逻辑清晰。 |
| 运行频率 | 绑定回测频率。按天回测则每天一次,按分钟则每分钟一次。 | 可自定义。即使是分钟回测,也可以指定只在每天 10:00 运行一次。 |
| Tick 级支持 | 不支持 Tick 级回测。 | 支持(配合 run_daily 调度或 handle_tick)。 |
| 官方建议 | 不推荐用于复杂策略。 | 强烈推荐。 |
2. 深度解析
handle_data (context, data)
这是早期的 API 设计。它的逻辑非常简单粗暴:只要有新的 K 线产生,我就运行。
- 优点:对于极简策略(例如:每天收盘看一眼,符合条件就买),写起来很快。
- 缺点:
- 控制力弱:如果你想在“每天 10:00 买入,14:50 卖出”,在
handle_data中你需要写大量的if context.current_dt.hour == 10 ...判断语句。 - 效率问题:如果在分钟回测中,你只想每天交易一次,但
handle_data会每分钟被调用 240 次,你必须在代码里防止重复交易。 - 数据对象过时:它依赖的
data参数(SecurityUnitData)在某些新版功能中支持度不如全局 API(如attribute_history)。
- 控制力弱:如果你想在“每天 10:00 买入,14:50 卖出”,在
run_daily (func, time)
这是定时运行框架。它的逻辑是:你告诉我什么时候运行,运行哪个函数。
- 优点:
- 模块化:你可以定义
market_open负责买入,market_close负责卖出,然后在initialize中分别注册它们。 - 精确控制:可以直接设置
time='09:30'或time='14:50',无需在函数内部判断时间。 - 替代性:如果你确实需要每根 Bar 都运行,可以设置
time='every_bar',完全替代handle_data的功能。
- 模块化:你可以定义
3. 代码对比示例
假设策略逻辑是:每天 14:50 判断均线,如果金叉则买入。
写法 A:使用 handle_data (不推荐)
在分钟回测模式下,这个函数每分钟都会运行,必须手动判断时间,否则效率低且容易出错。
def initialize(context):
g.security = '000001.XSHE'
def handle_data(context, data):
# 必须手动判断时间,非常繁琐
current_hour = context.current_dt.hour
current_minute = context.current_dt.minute
# 只有在 14:50 这一分钟执行逻辑
if current_hour == 14 and current_minute == 50:
# 获取数据
close_data = attribute_history(g.security, 5, '1d', ['close'])
# ... 交易逻辑 ...
order(g.security, 100)
写法 B:使用 run_daily (推荐)
逻辑清晰,无需判断时间,引擎会自动在 14:50 调用该函数。
def initialize(context):
g.security = '000001.XSHE'
# 直接注册定时任务,清晰明了
run_daily(my_trade_logic, time='14:50')
# 自定义函数,只接受 context 参数
def my_trade_logic(context):
# 这里不需要判断时间,进入这个函数时一定是 14:50
# 获取数据 (注意:这里不能用 data 参数,要用 API)
close_data = attribute_history(g.security, 5, '1d', ['close'])
# ... 交易逻辑 ...
order(g.security, 100)
4. 总结与建议
- 新策略请直接使用
run_daily。它能让你的代码结构更像专业的量化系统(初始化 -> 开盘前准备 -> 盘中交易 -> 收盘后处理)。 - 不要混用。虽然技术上可行,但官方文档建议不要在同一个策略中同时使用
run_daily和handle_data,以免执行顺序混乱。 - 数据获取习惯。使用
run_daily后,习惯使用attribute_history()、get_price()或get_current_data()来获取数据,而不是依赖handle_data传进来的data对象。
Q&A
Q: 如果我使用 run_daily,怎么实现“每分钟都运行”的逻辑?
A: 在 initialize 中设置 run_daily(your_func, time='every_bar')。这等同于 handle_data 的频率,但保持了代码结构的统一性。
Q: run_daily 定义的函数里没有 data 参数,我怎么获取当前价格?
A: 使用 get_current_data()[security].last_price 获取最新价,或者使用 attribute_history 获取历史序列。
Q: run_daily 可以设置多个时间点吗?
A: 可以。你可以多次调用 run_daily 注册不同的函数,或者注册同一个函数在不同的时间。例如:
run_daily(buy_func, time='10:00')
run_daily(sell_func, time='14:00')