title: 'MiniQMT 停用后的迁移实践:用 HTTP 桥接最大化复用旧策略代码'
date: 2026-08-16 11:00:00
tags:
- qmt
- miniQMT
- 迁移
- HTTP 桥接
- 策略代码
- 迅投
categories: "qmt"
一、背景:MiniQMT 退出舞台,旧策略怎么办?
在上一篇 《网传 MiniQMT 将被关闭?事实核查与应对指南(2026 版)》 里已经说过:自 2026 年 7 月起,多家券商已陆续收紧 MiniQMT 权限,存量客户大概率会在 1~2 个月内被清退,本地原生 Python + xtquant 直连模式即将成为历史。
但现实是:大多数个人量化的策略代码已经跑了很多年 ------指标计算、信号判断、行情订阅都依赖 xtquant,迁移成本极高。重写一遍不仅费时,还容易把原本稳定的策略写崩。
那有没有办法既不放弃 MiniQMT 端成熟的策略代码,又能把"真实下单"放到合规的大 QMT 里跑 ?答案是:HTTP 桥接。
二、方案核心思想:分析在 miniQMT,下单在大 QMT
把整个交易流程拆成两段:
| 角色 | 职责 | 运行环境 |
|---|---|---|
| QMT 外 python端 | 用python 代码计算指标、产生信号、通过 HTTP 发起下单 | Python |
| 大 QMT 端 | 起一个 HTTP 服务,接收下单/撤单/查询请求,在 QMT 主线程通过 passorder 执行 |
大 QMT 模型交易沙箱 |
这样做的好处非常明显:
- ✅ 策略代码 90% 不用动:指标、信号、风控逻辑继续留在 python 跑
- ✅ 下单走合规通道 :所有
passorder都在大 QMT 内执行,不触碰"外接系统"红线 - ✅ 行情双通道 :miniQMT 本地
xtdata仍可作主源,大 QMT HTTP 作 fallback - ✅ 平滑过渡:MiniQMT 真正停用那天,只需把"分析"也迁到大 QMT,桥接代码可保留复用
三、架构总览
+----------------------+ +----------------------+
| 外部python端 | | 大 QMT 端 |
| +------------------+ | HTTP | +------------------+ |
| | xtquant 取行情 | | --POST-->| | passorder 下单 | |
| | 计算指标 / 信号 | | <-JSON--| | get_full_tick 行情 | |
| | QMTClient 下单 | | | | get_trade_detail | |
| +------------------+ | | +------------------+ |
+----------------------+ +----------------------+
关键设计点:
- 下单/撤单走任务队列 + 200ms 定时器消费 :避免 HTTP 线程直接调
passorder的多线程风险(QMT 交易 API 不保证线程安全) - 查询/行情直接在 HTTP 线程读取本地缓存 :
get_full_tick/get_trade_detail_data是只读本地缓存,可安全跨线程调用 - HTTP 服务用 daemon 线程:策略停止时自动退出,不留僵尸进程
四、部署步骤
4.1 大 QMT 端(qmt_http_server.py)
-
打开大 QMT 客户端 → 模型交易 → 新建策略 ,把
qmt_http_server.py内容粘贴进去 -
在右侧设置:
- 资金账号 :选择已登录的实盘/模拟账号(先用模拟账号验证!)
- 运行模式:实盘模式
- 周期:随意(本策略不依赖 K 线)
-
启动策略,输出框看到以下两行即服务就绪:
[Bridge] HTTP server started on 0.0.0.0:8899
[Bridge] init done. account=XXX type=STOCK consume_interval=200ms
4.2 外部 python 端(miniqmt_client.py)
- 编辑
miniqmt_client.py,确认QMT_HTTP_HOST指向大 QMT 所在机器:
python
QMT_HTTP_HOST = 'http://127.0.0.1:8899' # 跨机时改大 QMT 的实际 IP
-
把
miniqmt_client.py复制到策略目录 -
python 客户端 → 策略列表 → 新建策略 → 选择本文件 → 运行
-
看到以下输出即桥接打通:
[Bridge] 大 QMT 已连接,account=XXX
[Bridge] xtquant 加载成功
五、代码详解
5.1 大 QMT 端:HTTP 服务 + 任务队列
qmt_http_server.py 的核心是三件事:
- 在
init钩子里启动 daemon HTTP 线程 + 注册 200ms 定时器 - HTTP Handler 接收请求:下单/撤单进队列,查询/行情直接返回
consume_tasks定时器回调 :在策略主线程从队列取出任务调passorder
下面是关键代码片段:
启动 HTTP 服务 + 定时器(init 钩子)
python
def init(ContextInfo):
global G_CONTEXT, G_ACCOUNT, G_ACCOUNT_TYPE
G_CONTEXT = ContextInfo
G_ACCOUNT = account
G_ACCOUNT_TYPE = accountType
# 起 daemon HTTP 线程,策略停止时自动退出
t = threading.Thread(target=_start_http_server, daemon=True)
t.start()
# 注册 200ms 定时器消费任务队列
period = '%dnMilliSecond' % CONSUME_INTERVAL_MS
ContextInfo.run_time('consume_tasks', period, '2000-01-01 00:00:00')
print('[Bridge] init done. account=%s type=%s consume_interval=%dms'
% (G_ACCOUNT, G_ACCOUNT_TYPE, CONSUME_INTERVAL_MS))
下单入队(HTTP 线程,不做真实下单)
python
def _handle_order(self, body):
required = ['opType', 'orderType', 'orderCode', 'prType', 'price', 'volume']
for r in required:
if r not in body:
self._send(400, {'error': 'missing field: ' + r})
return
task = {
'kind': 'order',
'opType': int(body['opType']),
'orderType': int(body['orderType']),
'accountid': body.get('accountid', G_ACCOUNT),
'orderCode': body['orderCode'],
'prType': int(body['prType']),
'price': float(body['price']),
'volume': float(body['volume']),
'strategyName': body.get('strategyName', 'http_bridge'),
'quickTrade': int(body.get('quickTrade', 2)),
'userOrderId': body.get('userOrderId', ''),
'enqueued_at': time.time(),
}
with TASK_LOCK:
TASK_QUEUE.append(task)
queued = len(TASK_QUEUE)
self._send(200, {'ok': True, 'msg': 'order queued', 'queued': queued})
定时器消费任务(主线程,安全调 passorder)
python
def consume_tasks(ContextInfo):
batch = []
with TASK_LOCK:
for _ in range(MAX_BATCH_PER_TICK):
if not TASK_QUEUE:
break
batch.append(TASK_QUEUE.popleft())
if not batch:
return
for task in batch:
try:
if task['kind'] == 'order':
# 11 参数完整版 passorder
passorder(
task['opType'], task['orderType'], task['accountid'],
task['orderCode'], task['prType'], task['price'],
task['volume'], task['strategyName'],
task['quickTrade'], task['userOrderId'], ContextInfo
)
elif task['kind'] == 'cancel':
cancel(task['orderId'], task['accountid'],
task['accountType'], ContextInfo)
except Exception as e:
print('[Bridge] task error: %s | task=%s' % (e, task))
为什么必须用队列 + 定时器,不能直接在 HTTP 线程里调 passorder?
passorder 是 QMT 客户端交易主线程的同步调用,从其他线程直接调可能导致:
- 委托号错乱
- 客户端崩溃
- 柜台连接断开
QMT 官方文档明确要求:所有交易相关 API 必须在策略主线程调用。所以最稳的做法是 HTTP 线程只入队,主线程定时器出队执行。
5.2 miniQMT 端:策略 + HTTP 客户端封装
miniqmt_client.py 提供了一个 QMTClient 类,把所有 HTTP 调用封装成 Python 方法,使用起来就像本地 API:
python
class QMTClient:
def __init__(self, host=QMT_HTTP_HOST, timeout=HTTP_TIMEOUT):
self.host = host.rstrip('/')
self.timeout = timeout
def _post(self, path, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(
self.host + path, data=data,
headers={'Content-Type': 'application/json; charset=utf-8'},
method='POST'
)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
return json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
try:
return json.loads(e.read().decode('utf-8'))
except Exception:
return {'error': 'HTTP %d' % e.code, 'detail': str(e)}
except Exception as e:
return {'error': str(e)}
def buy_stock(self, code, volume, price=0, prType=5,
strategyName='', userOrderId=''):
"""股票买入:opType=23 orderType=1101(按数量)"""
return self.order(23, 1101, code, prType, price, volume,
strategyName, 2, userOrderId)
def sell_stock(self, code, volume, price=0, prType=5,
strategyName='', userOrderId=''):
"""股票卖出:opType=24 orderType=1101(按数量)"""
return self.order(24, 1101, code, prType, price, volume,
strategyName, 2, userOrderId)
策略主循环(miniQMT 端演示)
python
def on_strategy_tick(ContextInfo):
if cli is None:
return
now = time.strftime('%H%M%S')
if now < '093000' or now > '150000':
return
ticks = _get_ticks(STOCK_POOL)
if not ticks:
return
for code, tick in ticks.items():
last = tick.get('lastPrice', 0)
pre_close = tick.get('lastClose', 0)
if not (last > 0 and pre_close > 0):
continue
ratio = last / pre_close
print('[miniQMT] %s last=%s preClose=%s ratio=%.4f'
% (code, last, pre_close, ratio))
# ====== 策略信号演示(!!实盘慎用!! 先用模拟账号) ======
# 信号:涨幅 >= 2% 触发买入 100 股(最新价)
# if ratio >= BUY_THRESHOLD:
# r = cli.buy_stock(code, 100, strategyName='demo_buy')
# print('[miniQMT] 下单结果:', r)
行情双通道(重点)
miniQMT 端取行情有两种方式,互为补充:
python
def _get_ticks(codes):
"""优先 xtquant 取行情,否则走大 QMT HTTP"""
# 方式 A:xtquant(miniQMT 本地行情源,无网络往返)
if xt is not None:
try:
data = xt.get_full_tick(codes)
return data or {}
except Exception as e:
print('[miniQMT] xtquant get_full_tick 失败,转 HTTP:', e)
# 方式 B:大 QMT HTTP(兜底)
r = cli.quote(codes)
if r.get('ok'):
return r.get('data', {})
print('[miniQMT] HTTP quote 失败:', r.get('error'))
return {}
策略代码里这一段是迁移的核心红利------MiniQMT 真正停用后,把方式 A 注释掉,方式 B 自动接管,几乎零修改就能继续跑。
六、HTTP 接口文档
所有响应均为 JSON。
| 路径 | 方法 | 用途 |
|---|---|---|
/ping |
GET | 健康检查,返回 account / accountType |
/quote?code=A,B,C |
GET | 取最新 tick 快照(多码逗号分隔) |
/quote |
POST | body: {"codes": ["A","B"]},同上 |
/position |
GET | 查询持仓(get_trade_detail_data) |
/order |
GET | 查询当日委托 |
/deal |
GET | 查询当日成交 |
/account |
GET | 查询账号资金 |
/query |
POST | body: `{"type":"position |
/order |
POST | 下单(入队,200ms 内消费) |
/cancel |
POST | 撤单(入队,200ms 内消费) |
下单 /order 的 body 字段
必填:
| 字段 | 类型 | 说明 |
|---|---|---|
opType |
int | 23=股票买 24=股票卖 0=期货开多 ...(见枚举速查) |
orderType |
int | 1101=按数量 1102=按金额 |
orderCode |
str | '000001.SZ' / 'rb2401.SF' 等 |
prType |
int | 5=最新价 11=指定价 14=对手价 42=市价 |
price |
float | prType=11/49 时填具体价格,其余填 0 |
volume |
float | 股数 / 手数 / 金额(视 orderType 而定) |
可选:
| 字段 | 类型 | 说明 |
|---|---|---|
accountid |
str | 不填则用策略配置的账号 |
strategyName |
str | 策略名,用于区分不同来源的委托 |
quickTrade |
int | 默认 2(立即下单,定时器场景必须用 2);0=K 线结束触发 1=最新 K 线触发 2=立即触发 |
userOrderId |
str | 投资备注,用于后续查询/匹配委托 |
下单响应:
json
{"ok": true, "msg": "order queued", "queued": 3}
queued = 当前队列里还没消费的任务数。
撤单 /cancel 的 body 字段
| 字段 | 类型 | 说明 |
|---|---|---|
orderId |
str | 必填,委托号 |
accountid |
str | 可选,不填用默认账号 |
accountType |
str | 可选,'STOCK'/'FUTURE'/'CREDIT' 等 |
行情 /quote 响应字段(get_full_tick 返回)
json
{
"000001.SZ": {
"lastPrice": 10.60,
"lastClose": 10.50,
"open": 10.55, "high": 10.70, "low": 10.40,
"volume": 100000,
"amount": 1060000,
"askPrice": [10.61, 10.62, 10.63, 10.64, 10.65],
"bidPrice": [10.59, 10.58, 10.57, 10.56, 10.55],
"askVol": [1000, 2000, 1500, 800, 600],
"bidVol": [500, 800, 1200, 900, 400]
}
}
持仓 /position 字段(节选)
| 字段 | 含义 |
|---|---|
m_strInstrumentID |
证券代码 |
m_strExchangeID |
市场 |
m_nVolume |
持仓量 |
m_nCanUseVolume |
可用数量 |
m_dOpenPrice |
成本价 |
m_dInstrumentValue |
市值 |
m_dPositionCost |
持仓成本 |
m_dPositionProfit |
持仓盈亏 |
账号 /account 字段(节选)
| 字段 | 含义 |
|---|---|
m_dBalance |
总资产 |
m_dAssureAsset |
净资产 |
m_dInstrumentValue |
总市值 |
m_dTotalDebit |
总负债 |
m_dAvailable |
可用金额 |
m_dPositionProfit |
持仓盈亏 |
七、常用枚举速查
opType(操作类型)
| 值 | 含义 | 值 | 含义 |
|---|---|---|---|
| 23 | 股票买入 | 24 | 股票卖出 |
| 0 | 期货开多 | 3 | 期货开空 |
| 2 | 期货平今多 | 5 | 期货平今空 |
| 33 | 两融担保品买入 | 34 | 两融担保品卖出 |
| 27 | 融资买入 | 28 | 融券卖出 |
| 50 | 期权买入开仓 | 51 | 期权卖出平仓 |
| 60 | ETF 申购 | 61 | ETF 赎回 |
prType(价格类型)
| 值 | 含义 |
|---|---|
| 5 | 最新价 |
| 11 | 指定价(必须填 price) |
| 14 | 对手价 |
| 42 | 市价(沪市有保护限价) |
| 49 | 五档即时剩转限 |
八、可调参数(qmt_http_server.py 顶部)
| 参数 | 默认值 | 说明 |
|---|---|---|
HTTP_HOST |
'0.0.0.0' |
监听网卡;只本机访问改 '127.0.0.1' |
HTTP_PORT |
8899 |
HTTP 端口 |
CONSUME_INTERVAL_MS |
200 |
任务消费周期(毫秒),越小越实时 |
MAX_BATCH_PER_TICK |
20 |
定时器单次最多消费任务数 |
九、安全 / 风险注意事项
9.1 务必先用模拟账号验证
qmt_http_server.py 默认 quickTrade=2(立即下单),任何能访问该端口的请求都会真实下单。生产环境务必:
- (a)
HTTP_HOST改成'127.0.0.1',只允许本机访问 - (b) 或在内网用 nginx + basic auth 反代
- © 或在 miniQMT 端的
QMTClient里统一加X-Token鉴权 header
9.2 价格笼子(2% 规则)
沪深主板/创业板委托价超出基准价 ±2% 会废单 ,下单前最好自查 price 是否在笼子内。
9.3 委托数量上限
| 板块 | 限价单上限 |
|---|---|
| 主板 | 100 万股 |
| 创业板 | 30 万股 |
| 科创板 | 10 万股 |
超量会产生废单。
9.4 废单查询延迟
下单后约 50ms~6s 才能通过 /order 接口查到(柜台推送周期),不要在 200ms 内反复查询。
9.5 策略阻塞
qmt_http_server.py 的 HTTP 线程是 daemon,策略停止时自动退出;定时器 200ms 周期不会阻塞其他策略。
9.6 GBK 编码
三个 .py 文件本身是 GBK 编码(QMT 内置 Python 3.6 的硬性要求)。如果在 macOS/Linux 编辑器里中文乱码,请用支持 GBK 的编辑器(VS Code、Notepad++)打开。
十、常见问题 FAQ
Q1:启动报 Address already in use?
A:端口 8899 被占。改 HTTP_PORT 或杀掉占用进程。
Q2:miniQMT 端 ping 返回 error?
A:大 QMT 端策略未启动 / 防火墙拦截 / IP 错。先在大 QMT 本机跑 test_client.py 验证本地服务。
Q3:行情返回空 / lastPrice=0?
A:大 QMT 客户端右下角行情源未选「五档」级别;或非交易时段。
Q4:下单后 /position 查不到新持仓?
A:柜台推送有 50ms~6s 延迟,等几秒再查;委托状态先查 /order。
Q5:想做鉴权?
A:在 qmt_http_server.py 的 QMTHandler.do_GET/do_POST 里加 header 校验(如 X-Token),或者在 miniQMT 端的 QMTClient 里统一加 header。
十一、源代码下载
完整源代码(含 qmt_http_server.py、miniqmt_client.py、README.py)已打包上传至夸克网盘:
下载后按本文第四节的部署步骤操作即可。三个文件均为 GBK 编码,可直接在 QMT 编辑器中打开。
十二、写在最后:迁移路径建议
这套 HTTP 桥接方案的本质,是给老策略争取了一段**"过渡期"**:
[阶段 1] miniQMT 还能用 → 分析 + 下单都在 miniQMT(旧代码不动)
↓ MiniQMT 收紧
[阶段 2] MiniQMT 还能用 + 大 QMT → 分析在 mini,下单走桥接(本文方案)
↓ MiniQMT 完全停用
[阶段 3] 只有大 QMT → 分析也迁大 QMT,QMTClient 改成本地调用
到了阶段 3,QMTClient 这个类仍然有用 ------把它的 _post 方法替换成对大 QMT 本地 passorder 的直接调用,上层策略代码(信号、风控、订单管理)一行都不用改。
这才是这套设计真正的价值:让迁移成本分摊到多年,而不是被迫在一个周末里全部重写。
⚠️ 风险提醒与免责声明
量化交易涉及真实资金风险。本文仅提供 MiniQMT 到大 QMT 迁移的代码方案与技术分享:
- 文中代码仅供学习参考,不构成任何投资建议
- 任何策略在实盘前必须先在 QMT「模拟信号模式」或「模拟柜台」中充分验证
quickTrade=2是立即下单模式,端口暴露在公网会导致真实资金损失- 因使用本文代码、文档或建议造成的任何盈亏,由使用者自行承担全部责任
- QMT、迅投 为迅投公司或其关联公司的商标
本文更新于2026年8月16日