天勤TqSdk从零基础安装部署到独立策略编写【保姆喂饭级教程】 第 3 天

手把手详细学习内容:is_changing 精准过滤与 deadline 超时等待

今天继续跟着 TqSdk 文档做具体练习:在第 2 天的 wait_update 更新循环基础上,学会只在真正变化时处理数据,并给等待更新加上超时控制,避免程序在无行情或交互环境中一直卡住。

今天学完你要能做到
能区分 api.is_changing(obj) 与 api.is_changing(obj, field) 的使用场景。
能用字段列表同时监听 last_price、bid_price1、ask_price1、volume 等变化。
能写出 wait_update(deadline=...) 的超时等待脚本,知道返回 True 和 False 分别代表什么。
能在非活跃行情或 Notebook 调试时避免程序无限阻塞。
能把“收到一次业务更新”和“我关心的字段变化”分开判断。

学习方式:今天仍然不是计划表,而是边读边做。每个"现在动手"都给你一段可以保存运行的脚本;每个"错误实验"都帮助你把排错习惯练出来。

0. 先接上第 2 天:今天解决两个更细的问题

第 2 天你已经知道:TqSdk 程序靠 wait_update 推进,get_quote 返回的是会被更新循环刷新的对象引用。今天继续往下问两个细节:第一,更新来了以后,怎样判断是不是我关心的字段变了;第二,如果一直没有更新,怎样让程序按时返回。

今天的边界
今天仍然只读行情,不下单,不连接实盘交易账户。
今天主线是 Quote 对象;K 线和 Tick 只作为预告和小练习出现。
示例继续使用主连合约 KQ.m@SHFE.rb,避免交割月过期影响学习。

1. 先把 is_changing 分成三种用法

TqSdk 文档建议用 is_changing 来过滤工作量。你可以把它理解成"本轮 wait_update 之后,这个对象或字段有没有变化"。今天先练三种最常用写法。

一句话记忆
wait_update 回答:“这次有没有业务数据更新?”
is_changing(obj) 回答:“这个对象这次有没有变化?”
is_changing(obj, field) 回答:“这个字段这次有没有变化?”

2. 现在动手:比较对象变化和字段变化

请新建 day3_change_compare.py。这个脚本会统计三件事:收到多少次业务更新、Quote 对象变化多少次、last_price 字段变化多少次。

day3_change_compare.py

from tqsdk import TqApi, TqAuth

SYMBOL = "KQ.m@SHFE.rb"
USER = "你的快期账户"
PASSWORD = "你的账户密码"

api = TqApi(auth=TqAuth(USER, PASSWORD))
quote = api.get_quote(SYMBOL)

update_count = 0
quote_change_count = 0
price_change_count = 0
max_updates = 40

try:
    print("第 3 天:比较业务更新、对象变化、字段变化")
    print("合约:", SYMBOL)

    while update_count < max_updates:
        api.wait_update()
        update_count += 1

        if api.is_changing(quote):
            quote_change_count += 1

        if api.is_changing(quote, "last_price"):
            price_change_count += 1
            print(
                "update=", update_count,
                "quote_change=", quote_change_count,
                "price_change=", price_change_count,
                "time=", quote.datetime,
                "last_price=", quote.last_price,
            )

    print("总业务更新次数:", update_count)
    print("Quote 对象变化次数:", quote_change_count)
    print("last_price 变化次数:", price_change_count)
finally:
    api.close()
   print("API 已关闭。")

2.1 运行脚本

在命令行运行:

python day3_change_compare.py

**你应该重点观察:**update_count、quote_change_count、price_change_count 不一定相等。一次业务更新可能不是这个 Quote 的变化;Quote 变化也可能不是 last_price 变化。

**如果三者都变化很少:**可能是非交易时段,或者合约当前不活跃。今天不要急着改账户类型,先把判断逻辑跑通。

3. 逐行带你拆解这个脚本

今天的第一个核心结论
业务更新、对象变化、字段变化是三个层次。
策略代码通常不要只看 wait_update;要继续用 is_changing 过滤到自己真正依赖的对象或字段。

4. 现在动手:同时监听盘口和成交字段

真实策略通常不只依赖 last_price。比如你可能同时关心最新价、买一价、卖一价和成交量。TqSdk 支持给 is_changing 传入字段列表。请新建 day3_multi_field_watch.py。

day3_multi_field_watch.py

from tqsdk import TqApi, TqAuth

SYMBOL = "KQ.m@SHFE.rb"
api = TqApi(auth=TqAuth("你的快期账户", "你的账户密码"))
quote = api.get_quote(SYMBOL)

watch_fields = ["last_price", "bid_price1", "ask_price1", "volume"]
printed = 0
max_print = 15

try:
    print("第 3 天:同时监听最新价、买一、卖一、成交量")

    while printed < max_print:
        api.wait_update()

        if api.is_changing(quote, watch_fields):
            printed += 1
            spread = quote.ask_price1 - quote.bid_price1
            print(
                printed,
                "time=", quote.datetime,
                "last=", quote.last_price,
                "bid1=", quote.bid_price1,
                "ask1=", quote.ask_price1,
                "spread=", spread,
                "volume=", quote.volume,
            )
finally:
    api.close()
    print("API 已关闭。")

4.1 运行后你要看什么

· 如果 last_price 没变,但 bid_price1 或 ask_price1 变了,也可能触发打印。

· 如果 volume 变化,说明累计成交量发生变化,也可能触发打印。

· spread = ask_price1 - bid_price1 是盘口价差示例,后面写交易逻辑时会很常见。

注意
字段列表不是“所有字段都变才返回 True”,而是列表中任一字段变化就返回 True。
字段名要写成 TqSdk 对象真实字段,例如 last_price、bid_price1、ask_price1、volume。

5. 错误实验一:把所有逻辑都放在 wait_update 后面

下面这个写法能运行,但容易让程序重复计算。请先看错误在哪里。

错误写法

# 不推荐:每次有业务更新都重新计算,不管目标字段有没有变化
while True:
    api.wait_update()
    spread = quote.ask_price1 - quote.bid_price1
    print(quote.datetime, quote.last_price, spread)

问题在于:wait_update 只说明这次有业务数据更新,不保证 quote 的盘口字段变化。你把计算无条件放在 wait_update 后,可能会因为别的更新而重复计算。

正确写法

# 推荐:只在相关字段变化时重新计算
watch_fields = ["bid_price1", "ask_price1", "last_price"]

while True:
    api.wait_update()
    if api.is_changing(quote, watch_fields):
        spread = quote.ask_price1 - quote.bid_price1
        print(quote.datetime, quote.last_price, spread)
排错口令
计算逻辑放在 wait_update 后面,不等于计算逻辑应该每次都执行。
先问:这个计算依赖哪些字段?再把这些字段放进 is_changing 的字段列表。

6. 认识 deadline:让 wait_update 不再无限等

默认情况下,wait_update 会阻塞等待业务更新。学习和调试时这很合理,但在交互式环境、非交易时段、或者你希望程序定期做别的检查时,就需要给等待设置时间上限。TqSdk 文档给出的做法是使用 wait_update(deadline=…)。

deadline 的关键点
deadline 传入的是绝对时间点,通常用 time.time() + 秒数。
它不是 sleep;wait_update(deadline=...) 仍然是在驱动 TqSdk 更新,只是最多等到指定时间。
不要在忙循环里使用极小 deadline,除非你确实需要轮询式行为。

7. 现在动手:写一个 5 秒超时等待脚本

请新建 day3_deadline_once.py。这个脚本只等待一次,最多等 5 秒。如果等到更新,就打印当前行情;如果没等到,就告诉你超时。

day3_deadline_once.py

import time
from tqsdk import TqApi, TqAuth

SYMBOL = "KQ.m@SHFE.rb"
api = TqApi(auth=TqAuth("你的快期账户", "你的账户密码"))
quote = api.get_quote(SYMBOL)

try:
    print("最多等待 5 秒,看看是否有业务数据更新...")
    updated = api.wait_update(deadline=time.time() + 5)

    if updated:
        print("收到更新:", quote.datetime, quote.last_price)
    else:
        print("5 秒内没有收到新的业务数据更新。")
finally:
    api.close()
    print("API 已关闭。")

**你要重点观察:**updated 是 wait_update 的返回值。True 只代表收到了业务更新,不代表 last_price 一定变化;如果要判断字段变化,仍然要使用 is_changing。

8. 现在动手:deadline + is_changing 组合写法

实际写脚本时,deadline 通常和 is_changing 一起用:先用 deadline 避免无限等待,再用 is_changing 判断目标字段是否变化。请新建 day3_deadline_loop.py。

day3_deadline_loop.py

import time
from tqsdk import TqApi, TqAuth

SYMBOL = "KQ.m@SHFE.rb"
api = TqApi(auth=TqAuth("你的快期账户", "你的账户密码"))
quote = api.get_quote(SYMBOL)

round_count = 0
max_rounds = 12

try:
    print("第 3 天:deadline + is_changing 组合写法")

    while round_count < max_rounds:
        round_count += 1
        updated = api.wait_update(deadline=time.time() + 3)

        if not updated:
            print(round_count, "3 秒内没有新业务更新,继续下一轮。")
            continue

        if api.is_changing(quote, ["last_price", "bid_price1", "ask_price1"]):
            print(
                round_count,
                "time=", quote.datetime,
                "last=", quote.last_price,
                "bid1=", quote.bid_price1,
                "ask1=", quote.ask_price1,
            )
        else:
            print(round_count, "有业务更新,但目标行情字段未变化。")
finally:
    api.close()
    print("API 已关闭。")

8.1 逐行拆解组合写法

9. 错误实验二:把 False 当成价格不变

下面这类理解很常见,但不准确:wait_update(deadline=…) 返回 False,不是"价格没有变化",而是"deadline 前没有收到新的业务数据更新"。

容易误解的写法

updated = api.wait_update(deadline=time.time() + 3)

# 不准确的理解:False = 价格没变
if not updated:
    print("价格没变")

更准确的写法

updated = api.wait_update(deadline=time.time() + 3)

if not updated:
    print("3 秒内没有收到新的业务数据更新")
elif api.is_changing(quote, "last_price"):
    print("最新价变化:", quote.last_price)
else:
    print("收到业务更新,但 latest price 没有变化")
今天的第二个核心结论
wait_update 的返回值回答“有没有业务更新”。
is_changing 回答“对象或字段有没有变化”。
不要把这两个问题混成一个问题。

10. 把今天的方法迁移到 K 线:只处理新 K 线

虽然今天主线是 Quote,但 TqSdk 文档里 K 线 DataFrame 也遵循同一模型:get_kline_serial 返回的 DataFrame 会在 wait_update 后原地更新。常见写法是只在最后一根 K 线的 datetime 变化时处理新 K 线。

预告脚本:只处理新 K 线

from tqsdk import TqApi, TqAuth

api = TqApi(auth=TqAuth("你的快期账户", "你的账户密码"))
klines = api.get_kline_serial("KQ.m@SHFE.rb", 60, data_length=200)

try:
    while True:
        api.wait_update()
        if api.is_changing(klines.iloc[-1], "datetime"):
            last_bar = klines.iloc[-1]
            print("新 1 分钟 K 线:", last_bar.datetime, last_bar.close)
finally:
    api.close()

**今天先记住:**K 线的重点不是反复 get_kline_serial,而是 get 一次、保留 DataFrame、循环 wait_update、用 is_changing(klines.iloc[-1], “datetime”) 判断是否出现新 K 线。后面会专门深入 K 线和 Tick。

11. 常见错误排查:第 3 天专用清单

12. 课堂练习:你现在来写,写完再看答案

练习 1:只在 ask_price1 变化时打印

要求:基于 day3_change_compare.py,只在卖一价 ask_price1 变化时打印。

参考答案片段

if api.is_changing(quote, "ask_price1"):
    print(
        "time=", quote.datetime,
        "ask1=", quote.ask_price1,
    )

练习 2:监听最新价、买一价、卖一价任一变化

要求:只要 last_price、bid_price1、ask_price1 任一字段变化,就重新计算价差。

参考答案片段

watch_fields = ["last_price", "bid_price1", "ask_price1"]

if api.is_changing(quote, watch_fields):
    spread = quote.ask_price1 - quote.bid_price1
    print(quote.datetime, quote.last_price, spread)

练习 3:说出下面代码的问题

问题代码

updated = api.wait_update(deadline=time.time() + 5)
if updated:
    print("价格变了", quote.last_price)
else:
    print("价格没变")

**参考答案:**updated=True 只表示收到了业务更新,不一定是价格变化;updated=False 只表示 5 秒内没有收到业务更新,也不能准确说成价格没变。要判断价格是否变化,应在 updated=True 后再使用 api.is_changing(quote, “last_price”)。

练习 4:把下面循环改成带超时控制

原始代码

while True:
    api.wait_update()
    if api.is_changing(quote, "last_price"):
        print(quote.datetime, quote.last_price)

参考答案

import time

while True:
    updated = api.wait_update(deadline=time.time() + 3)
    if not updated:
        print("3 秒内无新业务更新")
        continue
    if api.is_changing(quote, "last_price"):
        print(quote.datetime, quote.last_price)

13. 今日复盘:你要能独立说出来的 10 句话

wait_update 的返回值表示是否等到了业务数据更新。

wait_update 返回 True 不等于 last_price 一定变化。

wait_update 返回 False 表示 deadline 前没有新业务更新。

api.is_changing(obj) 判断对象本轮是否变化。

api.is_changing(obj, field) 判断具体字段本轮是否变化。

api.is_changing(obj, [field1, field2]) 表示任一字段变化就返回 True。

计算逻辑应该由它真正依赖的字段变化来触发。

deadline 通常写成 time.time() + 秒数。

超时等待不是 sleep,wait_update(deadline=…) 仍然在驱动 TqSdk。

K 线新 bar 常用 is_changing(klines.iloc[-1], “datetime”) 判断。

14. 今天的完整最终版脚本

把下面脚本保存为 day3_final.py。它把今天所有重点合在一起:deadline 控制等待时间,is_changing 字段列表控制处理逻辑,并明确区分"没有业务更新"和"有更新但目标字段未变化"。

day3_final.py

import time
from tqsdk import TqApi, TqAuth

SYMBOL = "KQ.m@SHFE.rb"
USER = "你的快期账户"
PASSWORD = "你的账户密码"

api = TqApi(auth=TqAuth(USER, PASSWORD))
quote = api.get_quote(SYMBOL)

watch_fields = ["last_price", "bid_price1", "ask_price1", "volume"]
round_count = 0
max_rounds = 20
signal_count = 0

try:
    print("TqSdk 第 3 天最终脚本:deadline + is_changing")
    print("合约:", SYMBOL)

    while round_count < max_rounds:
        round_count += 1
        updated = api.wait_update(deadline=time.time() + 3)

        if not updated:
            print(round_count, "超时:3 秒内没有新业务更新")
            continue

        if api.is_changing(quote, watch_fields):
            signal_count += 1
            spread = quote.ask_price1 - quote.bid_price1
            print(
                "round=", round_count,
                "signal=", signal_count,
                "time=", quote.datetime,
                "last=", quote.last_price,
                "bid1=", quote.bid_price1,
                "ask1=", quote.ask_price1,
                "spread=", spread,
                "volume=", quote.volume,
            )
        else:
            print(round_count, "收到业务更新,但目标字段未变化")

    print("循环结束。目标字段触发次数:", signal_count)
finally:
    api.close()
    print("API 已关闭。")

15. 明天开始前的准备

完成今天后,把 day3_final.py 保留好。明天会正式进入 K 线和 Tick 序列:你会用 get_kline_serial、get_tick_serial 获取 pandas DataFrame,并学习 data_length、K 线周期、最后一行更新、只处理新 bar 等细节。

今日完成标准
你成功运行 day3_change_compare.py,能看到业务更新、对象变化、字段变化不是一回事。
你成功运行 day3_deadline_loop.py,能看到超时分支与字段变化分支。
你能解释 wait_update(deadline=...) 返回 True/False 的意义。
你能用字段列表同时监听多个 Quote 字段。
你能把不带过滤的重复计算循环改成 is_changing 触发式循环。

提示:本文档中的代码用于学习行情读取、更新循环和调试方法,不构成交易建议,也不包含实盘下单逻辑。