简介:面向量化交易初学者与编程开发者的迅投QMT自动化下单指南代码包,聚焦极简版本中二次开发的关键流程,帮助读者将交易策略转化为可执行的下单命令。这份代码包适合已有编程基础、希望进入专业量化交易领域的开发者,也可供券商、期货及私募机构的IT人员评估平台接口能力。压缩包内共4个文件,包含可直接运行的示例脚本、依赖管理清单、图文并茂的说明文档,以及一份开发环境配置文件;整套资料体积仅9KB,轻巧实用,便于阅读和复用。目前已有371人下载学习,尤其适合希望绕过冗长手册、从代码快速上手的学习者。配套示例展示了如何通过行情模块获取实时市场数据,并调用交易模块完成委托下单;依赖清单明确了所需运行库,说明文档理清了关键调用顺序,同时还补充了支持的编程语言版本与接口信息,读者据此可快速搭建本地环境,开展自动化交易的模拟演练。
1. QMT自动化下单:为什么说它是个人量化从想法到实盘的最短路径
如果你已经受够了手动盯盘、来回切换行情软件和交易终端,那 QMT自动化下单 这个话题大概率能帮你解决最后那一步:让策略代码直接连上真实账户,按条件、按时间、按行情事件把单子发出去。我这边说的 QMT 是迅投在券商端广泛部署的量化交易终端,它既是行情软件也是交易通道,常见的用法是直接用 Python 写策略,跑在终端配套的 xtquant/xtdata 模块上,实现从行情订阅、信号计算到委托回报的整条链路。适合的人群很明确:写过 Python 但不想接 C++ 柜台接口的个人投资者、小型私募、以及想把自己手工交易规则固化成代码的交易员。它最反直觉的一点是:和很多人以为的不同,自动化下单并不是一个严肃到必须买服务器的工程问题,一台普通 Windows 电脑、一个开通了 QMT 权限的账户、一段能看懂回报的代码,就可以跑出第一笔程序化委托。这篇文章不打算把界面菜单讲一遍,而是从工程落地角度,给你一条可以直接复现、知道参数含义、也清楚边界在哪的上手路径。
2. QMT自动化下单的环境与架构:先确认你的终端版本、Python位数和交易通道
2.1 为什么必须先分清 Ptrader、极速柜台和本地Python环境
最常见的翻车起点,是拿到券商给的 QMT 安装包后,发现里面既有图形化的交易界面,又有一堆 Python 示例文件,但不知道哪些代码跑得起来。其实 QMT 的终端通常叫 Ptrader,它是一个带行情和交易界面的客户端。而自动化下单真正依赖的是终端自带的 Python 环境,以及安装在客户端目录下的 xtquant 包。常见部署情况是:券商营业部开通私募或量化权限后,给你一个账号和极速柜台地址,Ptrader 登录后,同目录下会出现一个 python.exe 和对应的 xtquant 库。
需要注意,这个 Python 环境在多数券商版本里是 32 位的,即便你 Windows 装了 64 位 Python,也不一定能直接借用。你在网上找代码时,经常会看到别人用python -c "import struct; print(struct.calcsize('P') * 8)"来确认当前解释器位数,这在 QMT 场景下不是矫情,而是血泪经验:如果位数不匹配,import xtquant 会直接失败或报出奇怪的接口错误。另外,行情和交易是两条通道:行情走 xtdata,交易走 xttrader。这两个模块在 xtquant 包里,但连接方式不同。自动化下单指的是走 xttrader 把委托发到券商柜台,而不是在终端界面里手动点按钮。
2.2 检查本机是否满足运行条件:账号权限、目录权限、网络连通性
动手写自动化下单第一件事,是打开 Ptrader 终端,登录你的资金账号,确认两点:第一,你的账号有没有开通程序化交易权限,这通常需要向营业部申请,部分券商还要求做一次视频认证或签署承诺书;第二,终端左下角或关于页面里显示的版本号,不同版本对 xtquant 的接口函数名和参数有细微差异。这里给一个检查脚本,放在 Ptrader 对应的 Python 环境下运行:
import sys import struct print("Python 位数:", struct.calcsize("P") * 8) print("Python 版本:", sys.version) try: import xtquant print("xtquant 路径:", xtquant.__file__) from xtquant import xttrader print("xttrader 导入成功") except Exception as e: print("导入失败:", repr(e))这段代码的作用是快速验证解释器架构和 xtquant 包是否可用。如果导入失败,先看错误是不是No module named xtquant。如果是,说明你运行代码的 Python 不是终端自带那个,换到 Ptrader 安装目录下的 python.exe 再试。这个检查在每次换了电脑、重装了终端后都应该跑一遍,不要凭感觉继续写策略。注意,部分券商版本对 XtQuant 的运行环境有额外要求,比如必须保持 Ptrader 登录状态,否则交易接口会拒绝初始化。
2.3 代码文件放哪里,才能避免终端权限拦截
关于本地文件读写这一点,有个常被忽略的细节:有些券商版本的 QMT 会限制 python 进程对任意目录的读写,常见现象是策略脚本提示 “FileNotFoundError” 但文件明明存在,或者日志写不进去。稳妥做法是,把策略代码和日志目录放在 Ptrader 的安装目录以内,比如userdata_mini下的某个子目录,或者终端在首次启动时生成的home目录。高性能读行情时,xtdata 也倾向于把缓存数据放在自己的目录里。所以从一开始就不要把项目根目录建在桌面或系统盘深处,而应该建在终端能访问到的位置。常见做法是建一个D:/qmt_strategy之类的目录,但先确认 Ptrader 的进程用户有权写入,否则后面跑模拟盘时会很痛苦。
3. 用 x qmt 做最小可行自动化下单:一个能打印回报的订单生命周期
3.1 初始化交易通道:connect 的返回值和 session_id 的作用
写第一版自动化下单,目标不是复杂策略,而是用代码走通“连接 → 查资金 → 下买单 → 收到回报”的最小闭环。这里用到的类是xtquant.xttrader.XtQuantTrader。初始化时需要传两个参数:终端安装路径和 session_id。路径通常可以直接取 Ptrader 的根目录,session_id 是一个自己生成的整数,用来区分当前会话,如果在同一台机器上跑了多个策略,每个进程最好用不同的 session_id,避免回报事件串掉。下面是最小连接代码:
from xtquant.xttrader import XtQuantTrader from xtquant.xttype import StockAccount path = r"D:/qmt/迅投QMT" # 换成你的Ptrader根目录 session_id = 123456 # 自己随意生成,建议每次启动随机化 account_id = "你的资金账号" xt_trader = XtQuantTrader(path, session_id) connect_result = xt_trader.connect() print("连接结果:", connect_result) if connect_result != 0: raise RuntimeError("连接失败,检查终端是否登录、路径是否正确") stock_account = StockAccount(account_id) subscribe_result = xt_trader.subscribe(stock_account) print("订阅账户结果:", subscribe_result)这段代码里connect()返回 0 才算成功。很多新手一上来就调用query_stock_asset但忘记订阅账户,结果永远拿不到资金数据。实际上subscribe就是把账户信息同步到本地,之后你才能用query_stock_asset和query_stock_positions查询账户状态。这个动作在每次进程启动时都要执行。如果 connect 失败,优先看终端是否已登录,以及当前的 path 是否真的指向了含有xtquant的目录。这里不讲玄学,所有自动化下单连接问题,90% 出在路径和 session_id 冲突上。
3.2 查询资金和持仓:p y t h o n 里的字段名与常见的坑
连接完成后,下一步是拿到账户的可用资金。这一步最大的坑是字段名与内存类型。用query_stock_asset返回的不是字典,而是一个StockAsset对象,字段用.访问。不少新手把这个对象打出来发现字段是cash、market_value、total_asset等,以为和 C++ 结构体一样,于是到处找字符串键,这是白费力气。示例代码如下:
asset = xt_trader.query_stock_asset(stock_account) if asset: print("总资产:", asset.total_asset) print("可用资金:", asset.cash) print("持仓市值:", asset.market_value) else: print("没有查询到资产信息,请确认账户订阅成功")如果 asset 是 None,先检查是不是券商测试环境里资金账号没绑好,或者账户被锁。另一个容易忽略的是query_stock_positions返回的列表里,每个元素有volume(总持仓)、can_use_volume(可用数量)、open_price(成本价)等字段。写条件单前,最好先用持仓数据做一次可用数量校验,因为 QMT 的撤单、卖出的可用量计算和券商柜台之间存在微小的时延,直接用volume下单,可能出现超量废单。
3.3 真正下一笔买单:order_stock 的参数结构与回报回调
下单的核心函数是order_stock,它接受账户、委托参数对象和回调函数。委托参数对象用xttrader.OrderStock构造,需要传入stock_code、order_type、order_volume、price_type、price等字段。这里最容易搞混的是股票代码格式:场内证券代码必须带上交易所前缀,比如600000.SH、000001.SZ,不能裸写600000,否则柜台直接拒单。下面的代码完成一笔按最新价买入 100 股浦发银行的操作:
from xtquant.xttrader import OrderStock, StockAccount def on_order_callback(order): print("委托回报:", order.order_id, order.order_status, order.volume_left) def on_trade_callback(trade): print("成交回报:", trade.order_id, trade.traded_volume, trade.traded_price) xt_trader.order_callback = on_order_callback xt_trader.deal_callback = on_trade_callback order = OrderStock( stock_account=stock_account, stock_code="600000.SH", order_type=23, # 23 表示股票买入 order_volume=100, price_type=11, # 11 表示最新价 price=0, # 最新价时价格可以填 0 ) order_id = xt_trader.order_stock(order) print("委托编号:", order_id)order_type和price_type是新手最容易问的常量。常规约定是:股票买入23,股票卖出24;价格类型11对应最新价,14对应对手价,12对应指定价,13对应限价。如果用的是指定价,price字段就不能为 0。下单成功后返回的order_id可以用于后续查单或撤单,但如果返回值为 -1 或一个很大但无意义的数字,说明参数校验没过。注意,order_callback不是在下单后立即触发,而是等委托状态变化才回调,所以想要「实时」回报,代码必须保持进程不退出,并让事件循环一直跑,这对后面写策略循环很关键。
4. 让下单自动化:从行情订阅、条件生成到委托状态对账
4.1 用 xtdata 订阅实时行情,把 tick 数据喂进你的策略
很多策略不依赖定时器,而是依赖行情事件。QMT 的xtdata模块提供subscribe_quote接口,可以在本地维护一个行情缓存,并通过回调函数通知新数据的到来。订阅行情之前,必须调用xtdata.connect()连接行情服务。这里的连接是独立于交易通道的,很多人在同一个进程里同时跑xtdata和XtQuantTrader,两者互不干扰,但都需要在终端登录后才能启动成功。一个订阅实时行情的例子如下:
from xtquant import xtdata xtdata.connect() xtdata.subscribe_quote("600000.SH", callback=on_quote_callback) def on_quote_callback(quote): print("最新价:", quote.lastPrice) print("成交量:", quote.volume) if quote.lastPrice > some_threshold: place_buy_order("600000.SH", 100, quote.lastPrice)这里的回调里直接执行place_buy_order,最直观也最简单。但有个性能陷阱:回调函数里不能做重计算,比如训练模型或遍历全市场,否则行情数据会积压在本地缓存里,导致延迟越来越大。常见做法是回调里只做条件判断和价格记录,把真正需要算的东西扔给另一个线程或进程去算。另外,subscribe_quote的第二个参数可能因版本不同而有所不同,有的版本要求传入周期,有的自动按 tick 推送,写代码时先打印一行quote.__dict__看看字段名再继续,能省掉很多调试时间。这属于典型的「先探测再编码」,是接任何闭源 SDK 时的自保手段。
4.2 轮询还是回调:从稳定性和代码复杂度两个角度选型
当你尝试把行情和交易揉进一个循环时,会立刻面临一个决策:是用while True + time.sleep拉数据,还是走回调。两者的差别不只是写法,而是程序生命周期。回调模式适合事件驱动,行情一变就触发策略逻辑,代码自然,但需要保证回调线程里的异常能全部捕获,否则一旦线程崩了,程序看起来还活着,实际已经不再交易。轮询模式适合做对账,比如每 3 秒查一次委托状态,相比回调更容易理解,也更能容忍网络延迟。实战中我一般会拆成两个部分:行情驱动用回调,但回调里只做策略计算;委托对账用一个独立的轮询循环,比如每 2 秒查一次query_stock_orders,检查是否有长时间未成交的委托。这样的好处是两类故障被隔离,不会因为一个行情回调异常影响了撤单逻辑。要注意的是,xtdata的行情线程和xttrader的回报线程本来就是独立线程,不要在回调里直接调用阻塞式接口,否则线程互相卡住,这属于多线程编程的基本功。
4.3 订单状态何时间步:撤单、改单和对账的代码实现
自动化下单绕不开一个场景:价格没到,单子挂着,你计划 3 秒后撤掉重新下。QMT 提供cancel_order_stock接口,参数是账户和order_id,但这里的order_id必须是order_stock返回的那个编号。很多人在对账时拿回调里的order.order_id去撤单,却发现撤不掉,原因就是回调对象的order_id和返回值可能是同一个,也可能因版本而异,最好每次都先从order_stock返回的 id 建立映射。一个典型的撤单逻辑如下:
pending_orders = {} # 下单时记录 order_id = xt_trader.order_stock(order) pending_orders[order_id] = {"stock_code": "600000.SH", "time": time.time()} # 轮询对账时检查是否超时 for oid, info in list(pending_orders.items()): orders = xt_trader.query_stock_orders(stock_account) for o in orders: if o.order_id == oid and o.order_status == 48: # 48 表示未成交 xt_trader.cancel_order_stock(stock_account, oid) del pending_orders[oid]这里order_status的数字含义需要查你当前版本的常量表,常见的有 48(未成交)、49(部分成交)、50(已成)等。数值在不同券商版本间可能有差异,建议先打印全部委托对象的字段看看。有了这个对账机制,你才算真正从「下单」走向了「自动化下单」,因为程序能对自己的未完成委托负责,而不是发完单就撒手不管。自动撤单和重新下单之间最好加一个小 sleep,比如 0.3 秒到 0.5 秒,避免撤单指令和重下指令在同一时刻被柜台拒绝。
5. QMT自动化下单避坑指南:从路径权限到订单编码冲突的四条现场记录
5.1 现象:连接返回成功后,下单总是提示资金不足或账户不存在
这是很典型的翻车现场:connect()返回 0,资金查询也能查到,结果一order_stock就报错,提示账户不存在或者资金不足。原因通常是StockAccount构造时填的账户类型不对。QMT 里股票账户用StockAccount,信用账户用CreditAccount,期货账户用FuturesAccount。如果你把股票资金账号当成普通账户传入,而柜台那边识别为信用账户,可能连接正常但下单通道不匹配。解决方式是核对营业部给的账户类型,然后显式创建对应账户对象。另外,部分模拟盘环境里,资金账号要填柜台返回的投资者账号而不是登录账号,比如登录账号是 8 位,投资者账号是 10 位,需要看文档确认。这种问题排查起来最耗时间,我的建议是第一件事打印query_stock_asset的返回值,如果为 None,就说通道根本没同步账户,而不是账户余额真为 0。
5.2 现象:撤单时提示委托编号不存在,或者撤掉的不是自己那笔单
这个问题多发于同时跑多个策略进程,或者在一个进程里用同一个 session_id 反复重连。QMT 的委托编号有一定的自增特点,如果两个进程共享同一个 session_id,委托回报会串。你查单时看到的是别的进程下的单,自然「委托编号不存在」。解决方法是按进程区分 session_id,每次启动时用random.randint(10000, 99999)生成,而不是固定写 123456。还有一个衍生坑:query_stock_orders返回的列表如果不按照order_id排序,而你循环里直接break,可能会漏掉待撤的委托。正确做法是先建立一个字典映射,循环里先收集再统一处理,不要在遍历列表时直接执行撤单操作,否则容易出现列表一边遍历一边修改的运行时错误。
5.3 现象:行情回调收不到,但手动刷新有数据
行情订阅最无语的问题是:回调函数定义了,也打印了一句「订阅成功」,但价格变动时没有反应。造成这个现象的常见原因是回调函数本身抛异常了,而 SDK 的线程静默吞掉了异常。解决方式是在回调函数最外层包一个 try-except,并把异常信息写到日志文件。另外一个老坑是subscribe_quote请求的量比较大时,会有丢弃或延迟,尤其是首次订阅时,本地缓存为空,需要先调用xtdata.get_market_data拉一次历史数据,把缓存放好,后续 tick 才正常推送。这里没有官方统一开关,属于很多版本共通行为,调通后稳定性一般都还不错。调试时不要只盯着 print,尤其是终端输出不显示线程 print 的情形,建议统一用logging模块写文件,不然你会错过真正的错误原因。
5.4 现象:回测时感觉策略没问题,实盘却频繁下单失败
回测环境的数据和实时行情的字段差异是个常被忽略的黑匣子。我在写条件单时,经常用quote.lastPrice判断价格触发,但回测脚本里用的是close,两者在 tick 粒度上的数值并不相等。如果你在回测代码里使用了一个策略模块,模块内部统一读close,而实盘代码读lastPrice,那测试结果没有任何参考意义。解决方式是把策略计算部分抽象成独立函数,只接收(price, volume, time)这样的标准参数,无论是回测还是实盘,都先转成同一种结构再算。这样至少能保证逻辑一致,剩下的误差就只是数据源本身的问题。另外,实盘下单价格类型和回测撮合逻辑也要对齐,回测默认按收盘价撮合,实盘用最新价,两者对滑点的假设差别很大,不要指望回测胜率能在实盘里复现。
5.5 现象:在本地 IDE 里运行代码时,提示动态库加载失败或连接被拒绝
很多熟手习惯把自己写的策略脚本丢在 PyCharm 或 VS Code 里跑,调通后才想着部署。但 QMT 的xtquant模块和终端安装路径有强绑定,尤其依赖同目录下的 DLL 文件。如果你拷贝了xtquant文件夹到别的 Python 环境,运行时报load library failed,那不是玄学,而是 DLL 依赖没打包进来。正确方式是:直接用 Ptrader 自带的 python 运行你的脚本,或者设置PYTHONPATH指向终端目录。还有一点,券商的客户端会校验登录状态,如果你在本地 IDE 里连接时 Ptrader 已经退出,连接会失败。所以工作流应该是:先登录 Ptrader,再用终端自带 Python 解释器运行策略。如果非要在自己的 IDE 里写代码,至少要在运行配置里把解释器指到终端的 python.exe,并且保证 Ptrader 一直挂着。这个坑不解决,后面每写一个功能都会被环境问题打断,效率极低。
6. 进阶用法与验证技巧:如何用最小成本验证你的自动化下单脚本真的可靠
当你照着前面的代码把最小闭环跑通后,下一步不是急着上策略,而是做一轮单调回归测试。我的习惯是准备一张只有几千块的模拟账户,然后连续几天在同一时段用同一套下单脚本反复执行如下步骤:小额买入 100 股,等待回报,主动撤单,再买入,再全撤。每跑一次,记下下单返回码、委托状态变化时间、撤单结果。这样做的目的是确认三件事:第一,回报回调在频繁撤挂下不会丢失;第二,撤单后的可用资金不会出现短暂冻结偏差;第三,长时间挂机运行的内存占用不会暴涨。另一件值得做的事是把每笔委托的日志做成 CSV,包含时间、价格、状态、回报延迟这几列。有了这些数据,你后续做任何策略优化都有据可依,而不是靠感觉说“好像没问题”。
进阶用法的核心,是把这套自动化下单能力切成两层:策略层和交易层。策略层只算价格、信号、目标仓位;交易层只负责执行、撤单、回报。中间用一个简单的消息队列或者只用一个共享字典加锁来传递指令。代码组织上,我不推荐把下单函数散落在策略文件里,更稳妥的做法是把所有下单入口收拢成一个trader.py,它向外暴露buy(stock_code, volume, price_type, price)和sell(...)两个函数,内部自己处理账户订阅、回报回调、撤单重试。这样你后续换任何券商终端或者改柜台通道时,只需要替换trader.py内部实现,策略逻辑一行不用动。我自己在这条路上交过不少学费,最深刻的一个教训是:不要在回调里做任何涉及磁盘写入或网络请求的操作,否则行情一密,日志延迟会反过来拖累下单时机。多线程之间的数据传递,优先用队列,而不是在回调里直接调用另一个线程的函数。
最后给你一个建议:如果第一次跑脚本就出现搞不懂的状态码,停下来先把query_stock_orders的所有字段原样打印一遍,对照常量表确认含义再继续。不要靠猜。这是最笨但最快的方法。希望帮到你。
本文还有配套的精品资源,点击获取