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

手把手详细学习内容:wait_update 是程序心跳

今天不做计划表,而是跟着 TqSdk 文档把运行时模型真正跑通:一次 get_quote、持续 wait_update、用 is_changing 只处理 last_price 变化,并学会判断"为什么对象创建了但数据不动"。

今天学完你要能做到
能把 wait_update 的作用讲清楚:发送订阅或交易请求、运行后台任务、接收更新、合并到内存对象。
能写出一个最小 quote 订阅脚本,只在 last_price 变化时打印。
能解释为什么 get_quote、get_kline_serial、账户、持仓、委托、TargetPosTask 都离不开更新循环。
能分辨三种状态:对象已创建但未收到数据、只更新了一次、持续更新中。
能发现并修正两个典型错误:循环里反复 get_quote、用 sleep 代替 wait_update。

学习方式:你可以直接照着本文每一步做。看到"现在动手"就新建脚本运行;看到"停下来想一想"就先自己回答,再看参考答案。

0. 先接上第 1 天:今天只深入一个问题

第 1 天你已经能创建 TqApi、订阅 Quote、看到行情字段。第 2 天只深入一个核心问题:为什么 TqSdk 程序必须围绕 wait_update 运行?今天的目标不是写更多功能,而是把"对象如何被刷新"这件事彻底理解。

今天的边界
今天仍然只读行情,不下单,不连接期货公司实盘交易账户。
今天的重点是无 timeout 的基础更新循环;deadline 会作为提示出现,但不作为主线。
示例仍使用主连合约 KQ.m@SHFE.rb,避免具体交割月过期导致学习过程被打断。

1. 先把 wait_update 想成程序心跳

TqSdk 文档把 wait_update 放在运行模型的中心。你可以把它理解成程序心跳:每跳一次,程序就有机会把前面登记的订阅、委托、后台任务和最新业务数据向前推进一步。

一句话记忆
get_quote 是“我要订阅这个合约”。
wait_update 是“现在推进一次收发和刷新”。
is_changing 是“这一次刷新里,我关心的字段有没有变化”。

2. 现在动手:写最小 last_price 变化监听脚本

今天的主脚本只做一件事:订阅一个主连合约,只在最新价 last_price 变化时打印。请新建 day2_last_price_loop.py。

from tqsdk import TqApi, TqAuth

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

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

printed = 0
max_print = 10

try:
    print("第 2 天:只在 last_price 变化时打印")
    print("合约:", SYMBOL)

    while printed < max_print:
        api.wait_update()

        if api.is_changing(quote, "last_price"):
            printed += 1
            print(
                printed,
                "时间=", quote.datetime,
                "最新价=", quote.last_price,
            )
finally:
    api.close()
    print("API 已关闭。")

2.1 运行脚本

在命令行运行:python day2_last_price_loop.py

**你应该重点观察:**程序不是固定每秒打印一次,而是等到 TqSdk 收到业务更新后,再判断 last_price 是否变化。价格不变时,即使收到了别的更新,也不会打印。

**如果非交易时段运行:**last_price 可能长时间不变,脚本可能等待较久。今天先理解阻塞模型;需要超时控制时,下一天会专门使用 deadline。

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

为什么只监听 last_price
第 2 天的目标是把更新循环练清楚,所以只盯一个字段。
字段越少,越容易看出 is_changing 的作用。
后面再扩展到 bid_price1、ask_price1、volume、K 线和 Tick。

4. 停下来想一想:wait_update 后为什么还要 is_changing?

很多人会误以为:只要 wait_update 返回了,就说明我关心的价格一定变了。实际上,一次业务更新可能是其他字段、其他对象、账户状态或后台任务变化。为了避免重复执行策略逻辑,要用 is_changing 判断具体对象或字段。

对比两种写法

# 不推荐:每次 wait_update 后都打印,可能出现重复信息
while True:
    api.wait_update()
    print(quote.datetime, quote.last_price)

# 推荐:只在 last_price 变化时处理
while True:
    api.wait_update()
    if api.is_changing(quote, "last_price"):
        print(quote.datetime, quote.last_price)

**你要形成的判断:**wait_update 负责推进数据,is_changing 负责筛选你真正关心的变化。一个负责"数据来了没有",一个负责"我关心的字段变没变"。

5. 错误实验一:只 get_quote,不 wait_update

现在故意写一个错误脚本,看看问题在哪里。请新建 day2_wrong_no_update.py。

day2_wrong_no_update.py

from tqsdk import TqApi, TqAuth

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

print("最新价:", quote.last_price)
api.close()

这个脚本可能打印空值、NaN 或者不是你期待的数据。问题不在 print,也不在 last_price,而在于你创建了 Quote 对象,却没有让 TqSdk 继续接收和合并数据。

正确修复方式
至少先调用一次 api.wait_update(),再读取关键字段。
如果要持续监听,就把 wait_update 放进循环里。
不要为了“等一下”去写 time.sleep(1),sleep 不会驱动 TqSdk 收发业务数据。

修复后的最小写法

from tqsdk import TqApi, TqAuth

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

try:
    api.wait_update()
    print("最新价:", quote.last_price)
finally:
    api.close()

6. 错误实验二:在循环里反复 get_quote

第二个常见错误是把 get_quote 放进循环,误以为每次都要重新获取行情。这样会让订阅和对象管理变混乱,也违背文档推荐的"get 一次,保留引用,循环 wait_update"。

错误写法

# 不推荐:循环里反复创建订阅和引用
while True:
    quote = api.get_quote("KQ.m@SHFE.rb")
    api.wait_update()
    print(quote.last_price)

正确写法

# 推荐:循环外 get_quote 一次,循环里只推进更新和读取字段
quote = api.get_quote("KQ.m@SHFE.rb")

while True:
    api.wait_update()
    if api.is_changing(quote, "last_price"):
        print(quote.datetime, quote.last_price)
今天必须背下来的结构
第一步:get_* 一次,拿到对象引用。
第二步:while 循环里 api.wait_update()。
第三步:api.is_changing(...) 判断变化。
第四步:只在变化时读取字段或执行逻辑。

7. 错误实验三:用 sleep 代替 wait_update

sleep 只会让 Python 暂停,它不会发送订阅,也不会接收行情,也不会刷新 quote。TqSdk 的推进动作在 wait_update 里,不在 sleep 里。

错误写法

# 不推荐:sleep 不是 TqSdk 的更新循环
import time

quote = api.get_quote("KQ.m@SHFE.rb")
while True:
    time.sleep(1)
    print(quote.last_price)

如果你想"不要刷太快",也不要把 wait_update 删掉。正确思路是继续让 wait_update 运行,然后用 is_changing 或计数条件控制打印和计算。

正确写法

# 推荐:让 wait_update 继续推进,只在字段变化时打印
quote = api.get_quote("KQ.m@SHFE.rb")
while True:
    api.wait_update()
    if api.is_changing(quote, "last_price"):
        print(quote.datetime, quote.last_price)

8. 从 Quote 推广到整个 TqSdk:谁都离不开更新循环

今天用 Quote 练习,是因为行情最容易观察。但同一套模型会贯穿后面所有章节。以后你看到 K 线、Tick、账户、持仓、委托、成交、调仓任务,都要先问:更新循环有没有持续运行?

排错时的第一反应
行情不动:先看 wait_update 有没有持续运行。
账户不动:先看 wait_update 有没有持续运行。
订单状态不变:先看下单后有没有继续 wait_update。
调仓任务不动作:先看 set_target_volume 后循环有没有继续。

9. 加一个观察工具:统计 wait_update 次数和 price 变化次数

为了更直观地看到"收到更新"和"最新价变化"不是一回事,我们写一个统计脚本。请新建 day2_update_counter.pfrom tqsdk import TqApi, TqAuth

from tqsdk import TqApi, TqAuth

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

update_count = 0
price_change_count = 0
max_updates = 30

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

        if api.is_changing(quote, "last_price"):
            price_change_count += 1
            print(
                "更新次数=", update_count,
                "价格变化次数=", price_change_count,
                "时间=", quote.datetime,
                "最新价=", quote.last_price,
            )

    print("总更新次数:", update_count)
    print("last_price 变化次数:", price_change_count)
finally:
    api.close()

**观察重点:**update_count 代表循环推进次数;price_change_count 代表 last_price 真正变化的次数。它们不一定相等。

**如果价格变化次数是 0:**可能处于非活跃时段,或者该合约最新价暂时没有变化。先不要改账户类型,先确认合约、时段和行情权限。

10. 常见错误排查:第 2 天专用清单

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

练习 1:把监听字段从 last_price 改成 bid_price1

要求:修改 day2_last_price_loop.py,只在买一价 bid_price1 变化时打印。

参考答案片段

if api.is_changing(quote, "bid_price1"):
    printed += 1
    print(
        printed,
        "时间=", quote.datetime,
        "买一价=", quote.bid_price1,
    )

练习 2:同时监听 last_price 和 volume

要求:只要最新价或成交量变化,就打印一次。

参考答案片段

if api.is_changing(quote, ["last_price", "volume"]):
    printed += 1
    print(
        printed,
        "时间=", quote.datetime,
        "最新价=", quote.last_price,
        "成交量=", quote.volume,
    )

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

问题代码

quote = api.get_quote("KQ.m@SHFE.rb")
api.wait_update()
print(quote.last_price)
print(quote.last_price)
print(quote.last_price)

**参考答案:**这段代码只推进了一次更新,后面连续打印三次读到的是同一次更新后的对象状态。想要观察后续变化,必须继续调用 wait_update。

练习 4:把下面错误循环改成正确结构

错误代码

while True:
    quote = api.get_quote("KQ.m@SHFE.rb")
    print(quote.last_price)

参考答案

uote = api.get_quote("KQ.m@SHFE.rb")

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

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

wait_update 是 TqSdk 程序的运行心跳。

get_quote 只是订阅和取得对象引用,不代表行情字段已经到达。

第一次读取关键字段前,至少要先调用一次 wait_update。

持续监听行情时,wait_update 应该放在循环里。

is_changing 用来判断本轮更新里某个对象或字段是否变化。

一次 wait_update 不等于 last_price 一定变化。

不要在循环里反复 get_quote。

不要用 sleep 代替 wait_update。

下单、撤单、账户、持仓、K 线、Tick、TargetPosTask 都依赖更新循环。

排查"对象不动"时,第一步检查循环是否持续调用 wait_update。

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

把下面脚本保存为 day2_final.py。它比第 1 天更专注:只研究 wait_update 与 last_price 变化之间的关系。

day2_final.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
price_change_count = 0
max_updates = 50

try:
    print("TqSdk 第 2 天最终脚本:观察 wait_update 与 last_price")
    print("合约:", SYMBOL)
    print("等待业务数据更新...")

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

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

    print("循环结束。")
    print("总 update 次数:", update_count)
    print("last_price 变化次数:", price_change_count)
finally:
    api.close()
    print("API 已关闭。")

14. 明天开始前的准备

完成今天后,把 day2_final.py 保留好。明天会在这个基础上加入 deadline,让等待更新的循环具备超时控制,适合交互式学习、调试和避免长时间阻塞。

今日完成标准
你成功运行 day2_last_price_loop.py 或 day2_final.py。
你能解释 wait_update 每次可能做的事情。
你能说明为什么只 get_quote 不 wait_update 会导致字段不可靠。
你能把“循环里反复 get_quote”的错误改成正确结构。
你能说清 sleep 为什么不能替代 wait_update。

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