前言

我带新人做期货量化时,最常见的问题不是指标不会写,而是主循环写得像定时器脚本:一会儿 time.sleep(1),一会儿手动轮询行情,最后在夜盘波动大时漏信号。wait_update 这个关键词看起来简单,实战里决定的是策略对行情帧的响应节奏、委托回报处理顺序和风控触发时机。主循环一旦写散,回测、模拟和实盘三段表现很容易割裂。

下面用一个双均线趋势策略讲透 wait_update 的事件驱动写法。重点不放在“均线多厉害”,而是放在如何避免重复触发、如何把下单和状态更新放在同一节奏里,以及如何在夜盘把异常处理挂进去。读完后,读者可以直接把骨架换成自己的信号函数。

一、先把循环职责划清:行情、信号、执行、风控

wait_update 主循环最稳的方式是四段式:

  1. 等待新帧
  2. 判断哪些对象变化
  3. 计算信号并下发目标仓位
  4. 执行风险检查与日志落盘

很多策略把 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_dtsigtarget_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 吗?

可以,直接下单也行,但仓位管理和反手逻辑会更复杂。

风险提示

本文用于期货量化技术实践讨论,不构成任何投资建议。期货交易存在高杠杆和高波动风险,策略在回测、模拟和实盘中的表现可能明显不同,实盘前请进行充分测试并控制仓位。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐