问题描述
用tick模拟交易的时候,一个交易日内,交易时间越长,模拟交易发出信号的时间与实际时间偏差越大,不知道大家有没有发现这个问题?
解决方案
聚宽 Tick 模拟交易延迟问题深度解析与解决方案
在 JoinQuant(聚宽)平台上进行 Tick 级别的模拟交易时,随着交易时间的推移,信号发出时间与实际时间偏差越来越大,这是一个在高频量化交易中非常典型的**“Tick 积压(Tick Backlog)”**现象。你观察到的这个问题非常敏锐,很多向高频策略转型的量化开发者都会遇到。
以下是导致该问题的核心原因分析以及相应的代码优化解决方案。
一、 为什么会产生“时差”与延迟?
产生这种现象的根本原因在于:策略处理单个 Tick 的耗时,超过了 Tick 数据推送的时间间隔。
- 数据推送频率极高:在聚宽平台,股票的 Tick 快照是每 3 秒一次,而期货的 Tick 快照高达每 0.5 秒一次。这意味着你的
handle_tick(context, tick)函数必须在极短的时间内(例如 0.5 秒内)执行完毕。 - Tick 任务排队积压:如果你的
handle_tick逻辑执行需要 0.8 秒,而期货 Tick 每 0.5 秒来一个,那么每过 1 秒钟,就会有未处理的 Tick 在队列中积压。随着交易日时间的延长,队列越来越长,你处理到的 Tick 实际上是几分钟甚至几十分钟前的历史数据,从而导致发出的交易信号严重滞后于实际时间。 - 模拟盘的系统机制:根据聚宽 API 文档说明,模拟盘本身存在约 10s 的系统延迟,且日志中的时间是“逻辑时间”而非实际物理时间。当积压发生时,逻辑时间与物理时间的脱节会更加明显。
二、 解决与优化方案
要解决这个问题,核心思路是极致压缩 handle_tick 函数的执行时间。以下是几种行之有效的优化手段:
1. 剥离复杂计算,实现“长短周期分离”
不要在 handle_tick 中进行复杂的指标计算或历史数据获取。
将耗时的计算(如均线、MACD、全市场选股等)放到分钟级的 handle_data 或定时运行函数 run_daily 中执行,并将计算结果存入全局变量 g 中。handle_tick 只负责极轻量级的价格比对和触发下单。
# 错误示范:在 tick 中获取历史数据并计算
def handle_tick(context, tick):
# 极其耗时!每个tick都去拉取历史数据
hist = attribute_history(tick.code, 60, '1m', ['close'])
ma60 = hist['close'].mean()
if tick.current > ma60:
order_target(tick.code, 100)
# 正确示范:长短周期分离
def initialize(context):
g.ma60 = 0
# 每分钟计算一次均线
run_daily(calc_ma, time='every_bar')
def calc_ma(context):
hist = attribute_history('000001.XSHE', 60, '1m', ['close'])
g.ma60 = hist['close'].mean()
def handle_tick(context, tick):
# Tick回调中只做简单的数值比较,极速执行
if tick.current > g.ma60:
order_target(tick.code, 100)
2. 减少不必要的 API 调用与 IO 操作
- 减少日志输出:频繁调用
log.info()或print()是非常耗时的 IO 操作。在 Tick 级别策略中,建议关闭非必要的日志,或者通过log.set_level('order', 'error')提高日志打印门槛。 - 避免重复查询账户信息:频繁调用
context.portfolio.available_cash或context.portfolio.positions也会消耗时间。如果可能,在分钟级别缓存这些状态,或者仅在确实需要下单前才进行查询。
3. 优化数据结构与算法
- 使用原生 Python 类型或 Numpy:在极高频的判断中,尽量使用 Python 原生的
dict、list或numpy数组进行状态记录,避免在 Tick 循环中频繁创建和操作pandas.DataFrame,因为 Pandas 的开销相对较大。 - 避免在 Tick 中使用 for 循环遍历大列表:如果你订阅了多个标的,尽量通过字典键值对
O(1)的时间复杂度来定位标的状态,而不是遍历列表。
4. 评估是否真的需要 Tick 级别
很多时候,策略的逻辑其实只需要“分钟级”甚至“秒级”即可满足。如果你的策略对 0.5 秒的微小波动并不敏感,可以考虑:
- 改用分钟级回测/模拟(
frequency='minute')。 - 在
handle_tick中人为降频。例如,设置一个计数器,每收到 6 个 Tick(约3秒)才执行一次核心逻辑。
def initialize(context):
g.tick_count = 0
def handle_tick(context, tick):
g.tick_count += 1
# 降频处理:每 6 个 tick 处理一次
if g.tick_count % 6 != 0:
return
# 执行核心逻辑...
三、 总结
在 JoinQuant 进行 Tick 级模拟交易时,“时差”本质上是代码执行效率跟不上数据推送频率导致的队列积压。高频策略对代码的性能要求极其苛刻。通过将复杂计算转移到低频函数、减少 API 调用、优化 IO 操作(如日志),可以有效缓解甚至消除这种延迟现象。建议利用聚宽提供的 enable_profile() 性能分析工具,精准定位你代码中最耗时的行,进行针对性优化。