问题描述
问题:
回测通过的代码,模拟时候出现几个问题,搞了几天都解决不掉, 烦请聚宽高人给看看.....
1. 用内存监测代码,程序运行结束时内存只有1.8042G,正常运行完之后,过了一段时间(大概2个小时),跳出“进程占用内存过大(3.8G),被强制杀掉!”。
1)试过多次,一直有这个问题,难道是内存监控代码有问题?代码是从社区下载的聚宽高人的代码:
def info_memo(context,time):
info = psutil.virtual_memory()
print ('memory_info:',time,round(psutil.Process(os.getpid()).memory_info().rss/1024.0/1024.0/1024.0,4))
2)如果真是内存占用过大,为什么不是在程序正常运行中爆掉,而是过了一段时间才出现强制杀掉的提示?
2.在提示进程被强制杀掉之后,过了若干时间,程序又自动开始执行,而且,时间显示是从更早的时间开始!如图,进程杀掉是在9:55之后, 过了一段时间后程序自动执行是从9:49:33开始,和被杀掉的进程的开始执行时间相同,是什么情况?
3.内存泄露
社区其他高人碰到的get_bars内存释放不掉的问题,也遇到了!
函数中调用get_bars取数据,用del,gc.collect()释放内存完全无用,代码反复优化,发现原因就是get_bars调用,没法继续优化下去了,很无力!代码如下:
for stock in security: #security是剔除ST,停牌等的当日股票列表,大概2400个
today_data = get_bars(stock, count = 229, unit='1d',fields='open','close','high','low','volume','money'],
include_now=True)
del today_data
gc.collect()
换成get_price,history,attribute_history尝试,差别不大,而且都del释放不掉!
解决方案
针对您在 JoinQuant 模拟交易中遇到的内存溢出、进程被杀以及 get_bars 内存泄漏问题,以下是详细的解答和优化方案:
1. 为什么程序运行结束后才爆内存被杀?
原因分析:模拟盘的状态保存与序列化机制
在 JoinQuant 的模拟交易中,每天的交易逻辑运行结束后,进程并不会立即退出,而是会进入状态保存阶段(相当于休眠)。系统会使用 pickle 将您的用户账户、持仓信息以及全局变量 g 对象序列化并保存到磁盘,以便第二天恢复。
- 如果您的
g对象中存储了大量的数据(例如缓存了大量的 DataFrame、历史行情数据等),在执行pickle.dumps序列化时,会瞬间消耗大量的内存。 - 这就是为什么您的程序在正常逻辑执行完时内存只有 1.8G,但在随后的状态保存阶段,内存飙升到 3.8G 从而触发了系统的内存限制(模拟盘进程占用内存不能超过 3G)被强制杀掉。
解决方案:
- 清理不必要的全局变量:在每天的
after_trading_end中,将g中不需要跨日保存的大型数据清理掉(如g.history_data = None)。 - 使用双下划线忽略序列化:如果某些变量不需要被保存,可以在命名时以
__开头(例如g.__temp_data),系统在序列化时会自动忽略这些变量。
2. 为什么进程被杀后会自动重启,且时间回退?
原因分析:系统的容错与恢复机制
当模拟盘进程因为内存超限或其他异常被系统强制杀掉后,JoinQuant 的底层调度系统会检测到任务失败,并尝试进行自动重试/恢复。
- 恢复时,系统会加载上一次成功保存的状态(通常是当天的初始状态或上一个成功的检查点)。
- 因此,程序重新启动时,逻辑时间(
context.current_dt)会回退到崩溃前的时间点(例如 9:49:33),重新执行未完成的逻辑。
3. get_bars 内存释放不掉(内存泄漏)怎么办?
原因分析:底层缓存机制与循环调用
- 缓存机制:为了加快运行速度,JoinQuant 系统默认启用了底层缓存机制。您通过
get_bars、get_price等 API 获取的数据会被缓存在内存中,因此使用del和gc.collect()是无法释放这部分内存的。 - 循环调用 API:您在
for循环中对 2400 只股票逐一调用get_bars,这不仅会产生大量的缓存对象,还会带来极大的网络和函数调用开销,导致内存迅速膨胀和运行缓慢。
解决方案:
方案 A:关闭系统缓存(治标)
如果您确实不需要缓存,可以在 initialize 函数中调用 disable_cache() 来关闭缓存机制。这样获取的数据在使用完毕后就可以被正常回收。
def initialize(context):
disable_cache() # 关闭缓存机制,防止内存占用过大
注意:关闭缓存会导致策略运行速度明显下降。
方案 B:批量获取数据(治本,强烈推荐)
get_bars 和 get_price 等 API 都是支持传入股票列表进行批量获取的。您完全不需要使用 for 循环单只获取,一次性获取 2400 只股票的数据不仅内存占用更集中、更易管理,而且运行速度会提升数十倍。
优化后的代码示例:
# security 是包含 2400 只股票的 list
# 直接传入 list,df=False 返回 dict (key为股票代码,value为 numpy.ndarray)
# df=True 返回 MultiIndex 的 DataFrame
today_data = get_bars(security, count=229, unit='1d',
fields=['open','close','high','low','volume','money'],
include_now=True, df=False)
# 随后您可以直接从字典中提取单只股票的数据进行处理
for stock in security:
stock_data = today_data.get(stock)
if stock_data is not None:
# 进行您的逻辑计算
pass
# 处理完毕后,清理大对象
today_data = None
总结建议:
- 检查并清理
g对象中的庞大缓存数据,避免序列化时爆内存。 - 坚决避免在
for循环中单只股票调用数据 API,改用传入list批量获取。 - 如果内存依然紧张,在
initialize中加入disable_cache()。