问题描述
解决方案
在聚宽(JoinQuant)的模拟交易环境中,策略进程通常会在每天收盘后暂停(序列化保存状态),并在第二天开盘前重启(反序列化恢复状态)。
由于这种机制,initialize、process_initialize 和 after_code_changed 在执行时机和主要用途上有明显的区别。
以下是详细的对比和用法说明:
1. 函数详解
initialize(context)
- 定义:初始化函数。
- 执行时机:
- 回测:在回测开始时执行一次。
- 模拟交易:仅在创建模拟交易的第一次启动时执行一次。后续每天重启时不会再执行。
- 用途:
- 设置基准(
set_benchmark)、手续费(set_order_cost)、滑点(set_slippage)。 - 开启真实价格模式(
set_option('use_real_price', True))。 - 定义全局变量
g的初始值(这些变量会被系统自动保存到磁盘,第二天自动恢复)。 - 设置定时运行函数(
run_daily等)。
- 设置基准(
process_initialize(context)
- 定义:进程启动初始化函数。
- 执行时机:
- 回测:在
initialize之后执行一次。 - 模拟交易:每次进程启动时都会执行(包括创建时的第一次启动,以及每天早上的重启)。
- 回测:在
- 用途:
- 重建无法被序列化(保存)的对象。
- 聚宽系统在保存模拟交易状态时,使用
pickle序列化g对象。但是,某些对象(如query查询对象、打开的文件句柄、数据库连接、网络连接等)是无法被序列化的。 - 如果将
query对象直接赋值给g.q,第二天重启时会报错或丢失。因此,必须在process_initialize中重新创建这些对象。 - 注意:为了避免系统尝试保存这些临时对象,通常建议变量名以双下划线开头(如
g.__q),系统会自动忽略以__开头的变量的序列化。
after_code_changed(context)
- 定义:代码变更回调函数。
- 执行时机:
- 模拟交易:当模拟交易重启(例如第二天早上或手动重启),且系统检测到策略代码与上一次运行时不同时执行。
- 用途:
- 更新全局变量的值。
- 在模拟交易中,
g变量的状态是从磁盘恢复的(即“昨天”的状态)。如果你修改了代码中的initialize(例如把g.stock = '000001.XSHE'改为'000002.XSHE'),由于initialize不会重跑,g.stock恢复后仍然是旧值'000001.XSHE'。 - 必须在
after_code_changed中显式重新赋值,才能让新的代码逻辑生效。
2. 对比总结表
| 特性 | initialize | process_initialize | after_code_changed |
|---|---|---|---|
| 执行频率 | 仅 1 次 (生命周期开始时) | 每天/每次重启都执行 | 仅在代码修改后的重启时执行 |
| 执行顺序 | 最先执行 | 在 initialize 或恢复状态后执行 |
在恢复状态后,process_initialize 之前 |
| 主要用途 | 基础设置、定义可保存的全局变量 | 重建不可保存的对象 (如 query) | 更新已保存的全局变量 |
| 典型场景 | 设置基准、手续费、初始股票池 | 初始化 g.__query 对象 |
修改持仓逻辑、更换标的池 |
3. 模拟交易启动流程图解
当模拟交易启动(或重启)时,系统的处理逻辑如下:
- 加载代码:编译并运行策略代码。
- 恢复状态:从磁盘读取并恢复
g和context对象(如果是第一次运行,则跳过此步,直接运行initialize)。 - 检查代码变更:
- 如果代码发生了变化,运行
after_code_changed(context)。
- 如果代码发生了变化,运行
- 进程初始化:运行
process_initialize(context)。 - 开始交易:进入
before_trading_start->handle_data等日常循环。
4. 代码示例
以下代码展示了三个函数的典型配合方式:
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
# 1. 基础设置(只运行一次)
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
# 2. 定义可持久化的全局变量
g.security = '000001.XSHE' # 平安银行
g.days = 0
log.info("initialize 运行:初始化完成")
def process_initialize(context):
# 3. 重建不可持久化的对象(每次重启都会运行)
# query对象不能被保存,必须每次重启时重新生成
# 使用双下划线开头,告诉系统不要尝试保存这个变量
g.__q = query(valuation).filter(valuation.code == g.security)
log.info("process_initialize 运行:Query对象已重建")
def after_code_changed(context):
# 4. 处理代码修改(仅在修改代码后重启时运行)
# 假设我们将代码中的 g.security 改为了 '000002.XSHE'
# 系统恢复的 g.security 还是 '000001.XSHE',必须在这里强制更新
g.security = '000002.XSHE'
# 如果有新的定时任务需要添加,也可以在这里处理
# unschedule_all()
# run_daily(func, '10:00')
log.info("after_code_changed 运行:全局变量已更新为万科A")
def handle_data(context, data):
# 使用在 process_initialize 中初始化的 query 对象
df = get_fundamentals(g.__q)
log.info("当前操作标的: %s" % g.security)
5. 常见误区
- 误区:修改了
initialize中的变量,重启模拟盘就会生效。- 事实:不会生效。因为
initialize不会重跑,系统会使用昨天保存的g变量覆盖你的新代码逻辑。必须使用after_code_changed更新。
- 事实:不会生效。因为
- 误区:在
initialize中定义g.q = query(...)。- 事实:模拟盘第二天会报错。因为
query对象包含数据库连接引用,无法被保存到磁盘。必须在process_initialize中定义,并建议命名为g.__q。
- 事实:模拟盘第二天会报错。因为
Q&A
Q: 如果我在模拟交易中修改了 run_daily 的定时时间,需要怎么做?
A: 你需要在 after_code_changed 中先调用 unschedule_all() 取消旧的定时任务,然后重新调用 run_daily 注册新的任务。因为定时任务的注册信息也是保存在 context 中被持久化的。
Q: process_initialize 在回测中会执行吗?
A: 会执行。在回测中,它会在 initialize 执行完毕后紧接着执行一次。虽然回测通常不涉及“重启”,但为了保证代码在回测和模拟中的兼容性,建议始终使用它来初始化 query 等对象。
Q: 为什么建议 process_initialize 中使用的变量名以 __ 开头?
A: 聚宽系统在收盘后会将 g 对象序列化保存到硬盘。以 __ 开头的变量会被系统视为“临时变量”而忽略保存。这既避免了保存不可序列化对象(如数据库连接)导致的报错,也减少了保存文件的大小,提高了系统稳定性。