- 金融科技
【免费下载链接】tqsdk-python
天勤量化开发包, 期货量化, 实时行情/历史数据/实盘交易
本文面向需要在图形界面程序中接入天勤量化开发包(TqSdk)的开发者,系统讲解 TqSdk 与 PySimpleGUI、Tkinter、PyQt、WxPython 等主流 Python GUI 库的组合方式。文章以官方文档 doc/advanced/gui.rst 为主线,结合 tqsdk/api.py 的事件循环、wait_update、register_update_notify等核心实现,给出「先 GUI 后 TqSdk」与「在 TqSdk 任务中驱动 GUI 消息循环」两种可直接落地的集成模式,帮助读者构建参数输入对话框、实时行情监控面板、策略启停控制台等实战应用。
为什么需要让 TqSdk 与 GUI 库共同工作
绝大多数量化策略以控制台脚本形态运行:行情驱动、下单、风控都发生在 TqSdk 的主循环里。但在实际生产中,很多场景要求程序带有人机交互界面:
- 策略启动前需要一个参数输入对话框,让用户填写合约代码、手数、止损比例等配置,而不是靠改代码或改配置文件;
- 策略运行期间需要一个监控面板,实时展示最新价、持仓、账户权益,并提供暂停、停止、调仓等操作按钮;
- 程序交付给非技术人员使用时,需要把整套逻辑封装成一个带界面的独立应用。
TqSdk 本身并不绑定任何 GUI 框架。官方文档明确说明:TqSdk 可以与 Tkinter、PyQt、WxPython、PySimpleGUI 等大多数常见 Python GUI 库配合工作(见 doc/advanced/gui.rst),并以下面两种方式组织两者关系:
- 先后使用 GUI 库和 TqSdk:先用 GUI 完成参数收集,关闭界面后再启动 TqSdk 主循环;
- 在 TqSdk 任务中驱动 GUI 消息循环:让 GUI 的事件刷新融入 TqSdk 的事件循环,两者并行工作。
需要说明的是,TqSdk 自身还提供基于浏览器的web_gui图形界面(用于 K 线、指标绘图,参见 tqsdk/demo/tutorial/t90.py),但那是 TqSdk 内建的可视化方案;本文讨论的是第三方桌面 GUI 库与 TqSdk 的集成,两者互不冲突。
前置理解:TqSdk 的主循环与事件模型
要把两个各自拥有"消息循环"的框架融合起来,首先必须理解 TqSdk 的运行模型。TqSdk 的顶层入口TqApi建立在 Pythonasyncio事件循环之上(见 tqsdk/api.py):
- 构造函数可通过
loop参数指定外部 IOLoop,默认情况下 TqSdk 会创建一个全新的 ioloop,源码注释为"创建一个新的 ioloop, 避免和其他框架/环境产生干扰"(tqsdk/api.py); - 所有网络请求、行情合并、后台任务推进都由这个事件循环驱动;
- 驱动它的核心方法是
api.wait_update()。
wait_update()是整个 TqSdk 主循环里最重要的函数,它在当前线程上阻塞,直到收到业务数据更新或到达指定的deadline截止时间(tqsdk/api.py)。每次调用它都会完成四件事:
- 实际发出待发送的网络请求(行情订阅、下单、撤单等);
- 推进正在运行的后台任务(如 TargetPosTask 等调仓任务);
- 尝试从服务器接收并合并新的业务数据包;
- 没有新数据时继续等待。
此外,wait_update还有一个重要约束:它不能在协程中调用(源码会抛出异常并提示改用register_update_notify,见 tqsdk/api.py)。这意味着当我们考虑"GUI 与 TqSdk 并行"时,不能简单地把wait_update塞进一个async函数里。
理解了"TqSdk 需要自己的阻塞式主循环"这一点,就能明白文档给出的两种集成模式分别解决了什么问题:模式一是避开冲突——把两个循环在时间上完全分离;模式二是统一循环——让 GUI 的刷新成为 TqSdk 主循环中的一个环节。
模式一:先后使用 GUI 库和 TqSdk(串行分离)
这是最简单、最稳妥的集成方式,也是官方文档推荐首先了解的做法:程序先用 GUI 库创建界面收集用户输入,用户确认后关闭界面,再进入 TqSdk 主循环。两个框架的生命周期完全不重叠,不存在事件循环冲突问题。
实现流程
- 用 PySimpleGUI 构建参数输入对话框(布局、读取事件);
- 用户点击确定后关闭窗口,取出参数;
- 用取到的参数创建
TqApi(选择模拟或实盘账户,传入快期账户认证TqAuth); - 进入
while True: api.wait_update()主循环执行策略; - 结束后调用
api.close()释放资源。
完整示例:参数输入对话框 + TqSdk 策略
以下代码演示了官方文档描述的param_input.py范式:先由 PySimpleGUI 收集合约与手数,再启动 TqSdk。
import time import PySimpleGUI as sg from tqsdk import TqApi, TqAuth, TqSim # ---------- 第一步:GUI 收集参数,之后关闭对话框 ---------- layout = [ [sg.Text("合约代码"), sg.Input("SHFE.rb2410", key="symbol")], [sg.Text("下单手数"), sg.Input("1", key="volume")], [sg.Button("确定"), sg.Button("取消")], ] window = sg.Window("策略参数输入", layout) event, values = window.read() # 阻塞等待用户操作 window.close() # 用户确认后立即关闭对话框 if event in (None, "取消"): raise SystemExit("用户取消,程序退出") symbol = values["symbol"].strip() volume = int(values["volume"]) print(f"参数确认: {symbol}, {volume} 手") # ---------- 第二步:关闭 GUI 后启动 TqSdk ---------- # TqSim() 为本地模拟账户,默认初始资金 1000 万(见 tqsdk/sim/__init__.py) # auth 需要替换为真实的天勤快期账户邮箱与密码 api = TqApi(TqSim(), auth=TqAuth("快期账户", "账户密码")) quote = api.get_quote(symbol) print("开始接收行情,Ctrl+C 退出") try: while True: api.wait_update() # 阻塞等待行情/数据更新 if api.is_changing(quote): print(symbol, quote.datetime, "最新价:", quote.last_price) finally: api.close() # 释放连接与资源关键点说明
- 账户与认证:示例使用
TqSim()(内部模拟账号,默认初始资金 10000000.0,定义见 tqsdk/sim/init.py);如连接实盘或快期模拟,可换成TqAccount/TqKq等账户类,并务必传入TqAuth("快期账户邮箱", "密码")完成权限认证(参数说明见 tqsdk/api.py)。 is_changing判变:quote是 TqSdk 返回的行情引用对象,行情更新后其字段自动刷新,用api.is_changing(quote)可判断最近一次wait_update中它是否被更新,避免无谓的输出(参见 doc/quickstart.rst)。- 下单请求是异步的:
api.insert_order()只是把下单请求加入待发送队列,实际报单会在下一次wait_update()时发出,因此下单后必须继续驱动主循环,不能立刻退出程序(doc/quickstart.rst 对此有明确提示)。 - 资源释放:程序结束前应调用
api.close(),它会关闭后台任务、HTTP 会话与事件循环(tqsdk/api.py)。
适用场景
参数输入型工具、一次性配置流程、脚本式策略的前置向导。优点是实现简单、两个框架完全解耦,缺点是无法在策略运行期间通过界面交互。
模式二:在 TqSdk 任务中驱动 GUI 消息循环(并行融合)
如果希望界面在策略运行期间保持响应——实时刷新行情、接收用户的下单或暂停指令——就必须解决一个核心矛盾:api.wait_update()与 GUI 的消息循环(如 PySimpleGUI 的window.read()、Tkinter 的mainloop())都是阻塞式调用,不能简单地先后串行。官方文档给出的第二种模式正是"在 TqSdk 任务中驱动 GUI 消息循环",即以 TqSdk 的主循环为主导,让 GUI 的刷新动作成为主循环里的一个环节。
方案 A:定时轮询式——用 deadline 让出时间片
wait_update()的deadline参数(Unix 时间戳秒数)使得它可以"超时返回":到达截止时间仍未收到数据时返回False(tqsdk/api.py)。利用这一点,可以让 TqSdk 主循环每运行一小段时间就"回头"处理一次 GUI 事件:
import time import PySimpleGUI as sg from tqsdk import TqApi, TqAuth, TqSim layout = [ [sg.Text("合约代码"), sg.Input("SHFE.rb2410", key="symbol")], [sg.Text("最新价"), sg.Text("-", key="price")], [sg.Button("暂停"), sg.Button("退出")], ] window = sg.Window("行情监控", layout, finalize=True) api = TqApi(TqSim(), auth=TqAuth("快期账户", "账户密码")) quote = api.get_quote(window["symbol"].get()) paused = False try: while True: # 每次最多等 100ms:有新数据立即返回 True,否则超时返回 False updated = api.wait_update(deadline=time.time() + 0.1) # 有行情更新时刷新界面 if updated and api.is_changing(quote): window["price"].update(str(quote.last_price)) # 无论是否有数据,都非阻塞地取一次 GUI 事件(timeout=0) event, _ = window.read(timeout=0) if event in (None, "退出"): break if event == "暂停": paused = not paused finally: window.close() api.close()这段代码的关键在于:GUI 事件读取使用window.read(timeout=0)的非阻塞形式,而不是默认的阻塞读取;整个程序只有一个wait_update循环在驱动,TqSdk 的数据推进与 GUI 的界面刷新交替执行,互不卡死。
方案 B:协程式——用 create_task + register_update_notify 做事件驱动
TqSdk 提供了api.create_task(coro)把协程注册进自身事件循环(tqsdk/api.py),并提供api.register_update_notify()返回一个数据更新通知 channel(tqsdk/api.py)。两者的组合可以构建更优雅的异步事件驱动模式:
import PySimpleGUI as sg from tqsdk import TqApi, TqAuth, TqSim async def monitor(api, quote, window): # 当 quote 有更新时,update_chan 会收到通知 async with api.register_update_notify(quote) as update_chan: async for _ in update_chan: window["price"].update(str(quote.last_price)) event, _ = window.read(timeout=0) if event in (None, "退出"): break api = TqApi(TqSim(), auth=TqAuth("快期账户", "账户密码")) quote = api.get_quote("SHFE.rb2410") layout = [[sg.Text("最新价"), sg.Text("-", key="price")]] window = sg.Window("行情监控", layout, finalize=True) try: api.create_task(monitor(api, quote, window)) # 主线程仍由 wait_update 驱动,后台协程的更新逻辑随之推进 while True: api.wait_update() finally: window.close() api.close()这里register_update_notify的作用是:当被监控的业务对象(quote、K 线序列、账户等)更新时,向注册的 channel 发送通知,协程内的async for循环随即被唤醒执行刷新逻辑。官方文档的用法示例在 tqsdk/api.py 中给出。注意wait_update不能在协程内调用(tqsdk/api.py),所以主线程的while True: api.wait_update()驱动循环必不可少,后台协程正是依托它获得推进时机。
方案 C:线程式——用 api.copy() 隔离事件循环
如果界面库强烈要求占据自己的主循环(例如 Tkinter 的mainloop()不便让位),TqSdk 也提供了线程级支持:TqApi.copy()可以创建当前实例的一个副本,这个副本可以在另一个线程中使用(tqsdk/api.py)。典型结构为:GUI 线程运行mainloop(),另一个线程运行api.copy()与wait_update()主循环,两个线程通过队列或事件对象交换数据。由于TqApi默认会创建独立 ioloop 避免与外部框架互相干扰(tqsdk/api.py),这种方式在多数常见 GUI 库下均可工作,但线程间的数据同步需要开发者自行管理。
三种方案如何选择
| 方案 | 实现难度 | 响应及时性 | 适用场景 |
|---|---|---|---|
| A. 定时轮询 | 低 | 取决于 deadline 间隔 | 简单监控面板、按钮控制 |
| B. 协程事件驱动 | 中 | 数据更新即响应 | 高频刷新、复杂交互逻辑 |
| C. 线程隔离 | 中高 | 完全独立 | 必须以 GUI 主循环为主的场景 |
集成过程中的常见问题与最佳实践
1. 不要把耗时计算写进 GUI 回调。无论采用哪种模式,界面回调(按钮事件处理、window.read返回后的分支)中的计算都应保持轻量,行情处理与策略计算尽量放在主循环中完成,否则会拖延wait_update的返回,导致数据更新堆积。wait_update的文档也提示:如果deadline设置得过小而本地计算任务较多,可能导致任务处理堆积(tqsdk/api.py)。
2. 注意wait_update与协程的边界。同步代码中用wait_update阻塞等待;协程中用register_update_notify获取更新通知,二者不可混用(tqsdk/api.py)。
3. 退出流程要完整。程序结束前先关闭 GUI 窗口,再调用api.close()释放连接;不要在协程中调用close(),而应在wait_update返回后于同步代码中关闭(tqsdk/api.py)。
4. 与策略打包场景衔接。TqSdk 的 Vs Code 插件提供"策略程序打包成独立应用程序"功能,可将含 GUI 的策略程序打包为可执行文件;官方在打包章节中明确建议使用该功能前先阅读本文所述的 GUI 集成文档(见 doc/devtools/vscode.rst)。若 GUI 与 TqSdk 的循环关系未处理好,打包后的程序同样会出现界面卡死或行情停止更新问题,因此集成模式的选型在打包前就应确定。
5. 界面更新频率要与业务频率匹配。行情数据可能每秒多次变化,不必每次更新都重绘界面;结合api.is_changing()判断关键字段变化,或对 GUI 刷新做节流,可显著降低界面开销。
小结
TqSdk 本身是事件循环驱动的异步库,与 Tkinter、PyQt、WxPython、PySimpleGUI 等 GUI 框架的集成关键在于协调两个消息循环:生命周期上完全错开的"串行模式"适合参数输入类场景;以 TqSdk 主循环为主导、通过wait_update(deadline=...)轮询或create_task+register_update_notify协程驱动的"并行模式"适合实时监控与交互控制;必要时还可借助api.copy()做线程隔离。无论哪种方式,都需遵循"主循环持续驱动、GUI 轻量刷新、退出前close()"的原则,即可构建稳定可用的图形化量化工具。
- 金融科技
【免费下载链接】tqsdk-python
天勤量化开发包, 期货量化, 实时行情/历史数据/实盘交易
相关推荐
React Easy State 错误处理与调试:常见问题及解决方案大全
React Easy State 错误处理与调试:常见问题及解决方案大全 React Easy State 是一个基于 ES6 Proxies 的简单 Reac
30分钟上手Python桌面应用开发:PyQt与Tkinter实战对比
30分钟上手Python桌面应用开发:PyQt与Tkinter实战对比 想要快速掌握Python桌面应用开发?本指南将对比两款主流GUI框架——PyQt与Tki
文档教程开发工具Tkinter-Designer扩展开发:集成PyQt与wxPython后端的可能性
Tkinter Designer扩展开发:集成PyQt与wxPython后端的可能性 痛点与挑战:单一GUI后端的局限 你是否在使用Tkinter Design
开发工具代码生成低代码
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考