返回列表 发布新帖

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

13 0

一、背景: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,桥接代码可保留复用

三、架构总览

```

+----------------------+ +----------------------+

| 外部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. 启动策略,输出框看到以下两行即服务就绪:

```

[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 所在机器:

```python

QMT_HTTP_HOST = 'http://127.0.0.1:8899' # 跨机时改大 QMT 的实际 IP

```

  1. 把 `miniqmt_client.py` 复制到策略目录
  2. python 客户端 → **策略列表** → **新建策略** → 选择本文件 → 运行
  3. 看到以下输出即桥接打通:

```

[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 钩子)**

```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` 是立即下单模式,端口暴露在公网会导致真实资金损失
  • 因使用本文代码、文档或建议造成的任何盈亏,由使用者自行承担全部责任
  • QMT、迅投 为迅**司或其关联公司的商标

*本文更新于2026年8月16日*

回复

您需要登录后才可以回帖 登录 | 立即注册

客服专线

400-080-8112

用思考的速度交易,用真诚的态度合作,我们是认真的!
  • 关注公众号
  • 添加微信客服
Copyright © 2001-2026 迅投QMT社区 版权所有 All Rights Reserved. 京ICP备2025122616号-3
关灯 快速发帖
扫一扫添加微信客服
QQ客服返回顶部
快速回复 返回顶部 返回列表