手把手详细学习内容: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 触发式循环。
提示:本文档中的代码用于学习行情读取、更新循环和调试方法,不构成交易建议,也不包含实盘下单逻辑。





