MiniQMT停用?HTTP桥接拯救旧策略

一、背景: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 模型交易沙箱

这样做的好处非常明显:

  1. 策略代码 90% 不用动:指标、信号、风控逻辑继续留在 python 跑
  2. 下单走合规通道:所有 passorder 都在大 QMT 内执行,不触碰”外接系统”红线
  3. 行情双通道:miniQMT 本地 xtdata 仍可作主源,大 QMT HTTP 作 fallback
  4. 平滑过渡:MiniQMT 真正停用那天,只需把”分析”也迁到大 QMT,桥接代码可保留复用

三、架构总览

1
2
3
4
5
6
7
8
+----------------------+          +----------------------+
| 外部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

  1. 打开大 QMT 客户端 → 模型交易新建策略,把 qmt_http_server.py 内容粘贴进去
  2. 在右侧设置:
    • 资金账号:选择已登录的实盘/模拟账号(先用模拟账号验证!
    • 运行模式:实盘模式
    • 周期:随意(本策略不依赖 K 线)
  3. 启动策略,输出框看到以下两行即服务就绪:
1
2
[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

  1. 编辑 miniqmt_client.py,确认 QMT_HTTP_HOST 指向大 QMT 所在机器:
1
QMT_HTTP_HOST = 'http://127.0.0.1:8899'  # 跨机时改大 QMT 的实际 IP
  1. miniqmt_client.py 复制到策略目录
  2. python 客户端 → 策略列表新建策略 → 选择本文件 → 运行
  3. 看到以下输出即桥接打通:
1
2
[Bridge] 大 QMT 已连接,account=XXX
[Bridge] xtquant 加载成功

五、代码详解

5.1 大 QMT 端:HTTP 服务 + 任务队列

qmt_http_server.py 的核心是三件事:

  1. init 钩子里启动 daemon HTTP 线程 + 注册 200ms 定时器
  2. HTTP Handler 接收请求:下单/撤单进队列,查询/行情直接返回
  3. consume_tasks 定时器回调:在策略主线程从队列取出任务调 passorder

下面是关键代码片段:

启动 HTTP 服务 + 定时器(init 钩子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 线程,不做真实下单)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 端演示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 端取行情有两种方式,互为补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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|deal|account"}
/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 投资备注,用于后续查询/匹配委托

下单响应

1
{"ok": true, "msg": "order queued", "queued": 3}

queued = 当前队列里还没消费的任务数。

撤单 /cancel 的 body 字段

字段 类型 说明
orderId str 必填,委托号
accountid str 可选,不填用默认账号
accountType str 可选,'STOCK'/'FUTURE'/'CREDIT'

行情 /quote 响应字段(get_full_tick 返回)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"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 反代
  • (c) 或在 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.pyQMTHandler.do_GET/do_POST 里加 header 校验(如 X-Token),或者在 miniQMT 端的 QMTClient 里统一加 header。


十一、源代码下载

完整源代码(含 qmt_http_server.pyminiqmt_client.pyREADME.py)已打包上传至夸克网盘:

🔗 https://pan.quark.cn/s/27baf28d2c89

下载后按本文第四节的部署步骤操作即可。三个文件均为 GBK 编码,可直接在 QMT 编辑器中打开。


十二、写在最后:迁移路径建议

这套 HTTP 桥接方案的本质,是给老策略争取了一段**”过渡期”**:

1
2
3
4
5
[阶段 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日