问题描述
解决方案
模拟盘重启后数据库查询对象失效的原因及解决方法
原因分析
在聚宽(JoinQuant)的模拟交易中,系统会在每天收盘后保存策略的状态(即“休眠”),并在第二天开盘前恢复状态(即“重启”)。这个过程依赖于 Python 的 pickle 模块对全局对象 g 和 context 进行序列化(保存)和反序列化(恢复)。
数据库查询对象(通过 query() 函数生成的对象)包含底层的数据库连接信息或特定的内存状态,这些是**无法被序列化(Pickle)**的。
具体流程导致的问题如下:
- 序列化失败或忽略:当模拟盘保存状态时,系统无法保存
g中的query对象。 - 初始化不执行:模拟盘重启时,不会再次执行
initialize(context)函数(该函数在策略生命周期中只执行一次)。 - 对象丢失:因此,当第二天策略恢复运行时,
g中原本存储的query对象已经不存在或损坏,导致再次调用时报错(通常是AttributeError)。
解决方案
要解决这个问题,需要利用 process_initialize(context) 函数和特殊的变量命名规则。
1. 使用 process_initialize 函数
process_initialize(context) 是一个专门用于进程启动初始化的回调函数。无论是策略第一次启动,还是每天模拟盘重启,该函数都会被执行一次。 因此,它是重建数据库连接或查询对象的最佳位置。
2. 使用双下划线 __ 开头的变量名
在 g 对象中,以双下划线开头(例如 g.__query)的变量不会被系统尝试序列化保存。这告诉系统:“不要把这个变量存到硬盘上,我会在每次重启时重新创建它。”
代码示例
请参考以下代码修改您的策略:
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
# 这里只放可以被序列化的普通变量,如股票代码、计数器等
g.security = '000001.XSHE'
# 注意:不要在这里定义 query 对象,因为 initialize 重启后不会运行
def process_initialize(context):
'''
该函数会在每次模拟盘/回测进程重启时执行。
用于初始化不能持久化保存的对象(如 query 对象、文件句柄等)。
'''
# 1. 重新创建 query 对象
# 2. 变量名必须以两个下划线开头 (__),避免系统尝试对其进行序列化
g.__q = query(valuation).filter(valuation.code == g.security)
log.info("数据库查询对象已在 process_initialize 中重新初始化")
def handle_data(context, data):
# 使用 g.__q 进行查询
# 注意:这里使用的是 process_initialize 中定义的 g.__q
df = get_fundamentals(g.__q)
if df is not None and len(df) > 0:
log.info("查询成功,市值: %s" % df['market_cap'][0])
Q&A
Q: initialize 和 process_initialize 有什么区别?
A: initialize 在整个策略的生命周期(无论回测还是模拟交易)中只执行一次,用于设置初始参数。process_initialize 在每次底层进程启动时都会执行。在模拟交易中,进程每天都会重启,所以 process_initialize 每天都会运行,适合用于重建连接或重置非持久化对象。
Q: 除了 query 对象,还有哪些对象不能保存在 g 中?
A: 凡是涉及到 I/O 操作或底层连接的对象都不能被序列化保存,包括:
- 打开的文件句柄(
open(...)返回的对象) - 网络连接对象(如
requests的 session) - 数据库连接对象
- 线程或进程锁对象
Q: 如果我不加双下划线 __ 会怎么样?
A: 如果在 process_initialize 中定义了 g.q(没加下划线),系统在收盘保存状态时会尝试序列化它。由于 query 对象不支持序列化,可能会导致保存状态失败,或者在第二天恢复时该变量损坏,引发不可预知的错误。加上 __ 是显式告诉系统忽略该变量的保存。