wait_update 在期货策略主循环里该怎么用:双均线事件驱动改造笔记
前言
我带新人做期货量化时,最常见的问题不是指标不会写,而是主循环写得像定时器脚本:一会儿 time.sleep(1),一会儿手动轮询行情,最后在夜盘波动大时漏信号。wait_update 这个关键词看起来简单,实战里决定的是策略对行情帧的响应节奏、委托回报处理顺序和风控触发时机。主循环一旦写散,回测、模拟和实盘三段表现很容易割裂。
下面用一个双均线趋势策略讲透 wait_update 的事件驱动写法。重点不放在“均线多厉害”,而是放在如何避免重复触发、如何把下单和状态更新放在同一节奏里,以及如何在夜盘把异常处理挂进去。读完后,读者可以直接把骨架换成自己的信号函数。
一、先把循环职责划清:行情、信号、执行、风控
wait_update 主循环最稳的方式是四段式:
- 等待新帧
- 判断哪些对象变化
- 计算信号并下发目标仓位
- 执行风险检查与日志落盘
很多策略把 1-4 混成一段,结果是信号触发和委托回报互相覆盖。双均线策略尤其容易在同一根 K 线里重复开仓,最后出现“看起来只有一次金叉,实盘却连发两笔”的问题。
拆职责的价值在于把故障定位路径缩短:如果是信号误触发,就查判定函数;如果是执行异常,就查下单与回报;如果是风控打架,就查风险开关更新时点。团队协作时,这种边界清晰的循环比“大一统函数”更容易交接,也更容易做代码评审。
二、双均线策略骨架(事件驱动版本)
下面是一个最小可用骨架,默认 5 分钟线,快线 10,慢线 30:
from tqsdk import TqApi, TqAuth, TqSim, TargetPosTask
import pandas as pd
SYMBOL = "SHFE.rb2510"
FAST = 10
SLOW = 30
api = TqApi(TqSim(), auth=TqAuth("账户", "密码"))
klines = api.get_kline_serial(SYMBOL, 300, data_length=200)
target = TargetPosTask(api, SYMBOL)
last_signal_dt = None
def calc_signal(df: pd.DataFrame):
close = df.close
fast_ma = close.rolling(FAST).mean()
slow_ma = close.rolling(SLOW).mean()
if len(close) < SLOW + 2:
return 0
# 用已收盘bar,避免未完成bar抖动
if fast_ma.iloc[-2] > slow_ma.iloc[-2]:
return 1
if fast_ma.iloc[-2] < slow_ma.iloc[-2]:
return -1
return 0
while True:
api.wait_update()
if not api.is_changing(klines.iloc[-1], "datetime"):
continue
bar_dt = klines.datetime.iloc[-2]
if bar_dt == last_signal_dt:
continue
last_signal_dt = bar_dt
sig = calc_signal(klines)
target.set_target_volume(sig)
这个版本里最关键的是两点:第一,用 iloc[-2];第二,用 last_signal_dt 防重入。很多回测漂亮、实盘漂移的策略,问题都在这两个地方。
实际落地时,建议把 bar_dt、sig、target_volume 一并落日志,这样回放时能直接看到“哪根 bar 发了什么指令”。如果后续要换策略逻辑,只要保留这组三字段,定位链路不会断,研发效率会明显提升。
三、实盘要补的三个“硬件级细节”
| 细节 | 常见坑 | 建议做法 |
|---|---|---|
| 连接稳定性 | 网络抖动后循环恢复但状态未校验 | 恢复后先校验持仓与目标仓 |
| 回报一致性 | 只看信号,不看委托状态 | 每次调仓后记录订单状态 |
| 时段控制 | 非交易时段仍在改仓 | 用交易时段过滤后再下单 |
双均线本身不复杂,难点在工程化。只要把这三件事补齐,策略在夜盘和节后复市时会稳定很多。
尤其是节后第一天和夜盘开盘前后,行情节奏和撮合状态变化都更剧烈,这些细节会放大循环缺陷。提前把连接恢复、回报校验、时段过滤固化成统一模块,能够显著减少“偶发性误单”。
四、把风控塞进同一帧:避免“信号快于风控”
建议在同一个 wait_update 周期内先更新风险开关,再允许开仓。示例:
max_drawdown_stop = False
def risk_check(account):
# 仅示意,实际阈值按策略设定
if account.balance < 0.9 * day_start_balance:
return True
return False
account = api.get_account()
while True:
api.wait_update()
if api.is_changing(account):
max_drawdown_stop = risk_check(account)
if max_drawdown_stop:
target.set_target_volume(0)
continue
# 其余信号逻辑...
这样写的好处是风控状态和信号状态共用同一事件时钟,减少“刚触发止损又被信号拉回仓位”的反复。
进一步的做法是把风控状态做成可观测变量,例如 risk_state=normal/reduce/stop,并在每次状态切换时输出原因。这样在复盘时不会只看到“为什么没开仓”,而是能看清“哪条规则在何时生效”。
五、从回测迁移到模拟的检查单
- K 线周期、手续费、滑点参数一致
- 信号是否只在收盘 bar 执行一次
- 调仓指令是否附带日志上下文(时间、信号值、仓位)
- 遇到断线重连后是否会重复调仓
如果这四项不做,双均线策略在模拟阶段很容易“看起来没问题”,到实盘才暴露主循环缺陷。
建议把这份检查单做成发布前固定流程:每次参数改动、代码改动、环境改动都走一遍。这样即便策略逻辑逐步复杂,主循环稳定性仍然可控,不会因为一次小改动引入隐性执行风险。
总结
wait_update 不是语法细节,而是期货量化策略的运行底盘。双均线这种基础策略恰好能把问题暴露得很清楚:同一根 bar 重复触发、未收盘数据误判、风控与信号节奏冲突。把主循环写成事件驱动的四段式,再加上防重入和风控同帧检查,策略稳定性会明显提高。后续无论换成趋势突破、价差套利还是多品种轮动,都可以复用这套骨架。
从工程视角看,这套骨架的真正价值是“可迁移”:你可以替换信号函数、替换下单模块、替换风险阈值,但主循环时钟与状态边界不变。只要底盘稳定,策略迭代会从“反复返工”变成“可控升级”,这也是从研究脚本走向长期实盘系统的关键一步。
FAQ
1)wait_update 每次都要做完整计算吗?
不需要。先用 is_changing 收窄触发条件,只在关键对象变化时计算。
2)为什么不用 iloc[-1] 做信号?
iloc[-1] 对应未完成 bar,盘中会反复变化,容易造成来回开平。
3)双均线策略适合哪些品种?
更适合趋势性相对连续、交易成本可控的品种,震荡强的品种需额外过滤。
4)回测和实盘触发次数不一致怎么查?
先查是否在收盘 bar 才触发,再查是否有防重入标记,最后查交易时段过滤。
5)可以不用 TargetPosTask 吗?
可以,直接下单也行,但仓位管理和反手逻辑会更复杂。
风险提示
本文用于期货量化技术实践讨论,不构成任何投资建议。期货交易存在高杠杆和高波动风险,策略在回测、模拟和实盘中的表现可能明显不同,实盘前请进行充分测试并控制仓位。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)