一、背景: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\|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 | 投资备注,用于后续查询/匹配委托 |
**下单响应**:
```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 反代
- **(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.py` 的 `QMTHandler.do_GET/do_POST` 里加 header 校验(如 `X-Token`),或者在 miniQMT 端的 `QMTClient` 里统一加 header。
十一、源代码下载
完整源代码(含 `qmt_http_server.py`、`miniqmt_client.py`、`README.py`)已打包上传至夸克网盘:
🔗 **https://pan.quark.cn/s/27baf28d2c89**
下载后按本文第四节的部署步骤操作即可。三个文件均为 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` 是立即下单模式,端口暴露在公网会导致真实资金损失
- 因使用本文代码、文档或建议造成的任何盈亏,由使用者自行承担全部责任
*本文更新于2026年8月16日*