🚀 AI 一键生成 qmt 策略代码

如何在 QMT 平台用 Python 编写一个简单的做市商 (Market Making) 策略?

qmt | 发布时间: 2025/12/15
以下内容由 EasyQuant 生成。

问题描述

请帮我编写一个简单的做市商策略,根据买一卖一价差进行双边挂单。

解决方案

这是一个基于 QMT Python API 的简单做市商(Market Maker)策略示例。

策略逻辑说明

这个策略的核心逻辑是**“盘口双边挂单”**,适用于 T+0 品种(如 ETF、可转债或期货)或持有底仓的股票。

  1. 获取行情:在每个 Tick(分笔)数据到来时,获取当前的买一价(Bid1)和卖一价(Ask1)。
  2. 撤单机制:在挂新单之前,先撤销当前标的未成交的所有挂单,防止挂单堆积占用资金或导致敞口过大。
  3. 计算价格
    • 买单价格 = 买一价 - 设定的价差(Spread)
    • 卖单价格 = 卖一价 + 设定的价差(Spread)
  4. 双边下单:同时发出限价买入和限价卖出指令。

QMT 策略代码

# -*- coding: gbk -*-
import time

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置交易标的 (例如: 510050.SH 上证50ETF)
    ContextInfo.stock_code = '510050.SH'
    
    # 2. 设置资金账号 (请替换为您真实的资金账号)
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
    # 设置账号类型: 'STOCK' (股票/ETF), 'FUTURE' (期货)
    ContextInfo.account_type = 'STOCK'
    
    # 3. 绑定账号
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 4. 策略参数设置
    ContextInfo.volume = 100       # 单笔挂单数量 (股票/ETF通常为100的倍数)
    ContextInfo.spread = 0.001     # 挂单价差 (基于盘口的偏移量)
    ContextInfo.tick_size = 0.001  # 最小变动价位 (根据标的调整,ETF通常是0.001)
    
    print("做市商策略初始化完成")

def handlebar(ContextInfo):
    """
    K线/Tick驱动函数
    """
    # 仅在实盘的最后一根K线(实时行情)运行,回测或历史K线不运行
    if not ContextInfo.is_last_bar():
        return

    # 获取当前标的
    stock_code = ContextInfo.stock_code
    
    # 1. 获取全推Tick数据 (获取盘口买一卖一)
    tick_data = ContextInfo.get_full_tick([stock_code])
    
    if not tick_data or stock_code not in tick_data:
        return
        
    current_tick = tick_data[stock_code]
    
    # 获取买一价和卖一价
    # bidPrice: [买1, 买2, ...], askPrice: [卖1, 卖2, ...]
    bid_1 = current_tick.get('bidPrice', [0])[0]
    ask_1 = current_tick.get('askPrice', [0])[0]
    
    # 简单的数据校验,防止行情异常
    if bid_1 <= 0 or ask_1 <= 0:
        return

    # 2. 撤销之前的未成交挂单 (风控:防止挂单堆积)
    cancel_all_orders(ContextInfo)
    
    # 3. 计算挂单价格
    # 策略:在买一价下方挂买单,在卖一价上方挂卖单,赚取流动性溢价
    # 注意:价格需要符合最小变动价位
    buy_price = round(bid_1 - ContextInfo.spread, 3) 
    sell_price = round(ask_1 + ContextInfo.spread, 3)
    
    # 4. 执行双边下单
    # 4.1 挂买单
    # opType: 23 (股票买入), orderType: 1101 (单股单账号普通下单), prType: 11 (限价)
    passorder(23, 1101, ContextInfo.account_id, stock_code, 11, buy_price, ContextInfo.volume, ContextInfo)
    print(f"挂买单: {stock_code}, 价格: {buy_price}, 数量: {ContextInfo.volume}")
    
    # 4.2 挂卖单
    # opType: 24 (股票卖出)
    # 注意:如果是T+1股票,必须确保账户里有底仓才能卖出
    passorder(24, 1101, ContextInfo.account_id, stock_code, 11, sell_price, ContextInfo.volume, ContextInfo)
    print(f"挂卖单: {stock_code}, 价格: {sell_price}, 数量: {ContextInfo.volume}")

def cancel_all_orders(ContextInfo):
    """
    辅助函数:撤销当前标的的所有未结委托
    """
    # 获取未结委托 (Order)
    # get_trade_detail_data 参数: 账号, 账号类型, 数据类型('ORDER')
    orders = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'ORDER')
    
    for order in orders:
        # 筛选条件:
        # 1. 属于当前策略操作的标的
        # 2. 状态为未成交 (48:未报, 49:待报, 50:已报, 51:已报待撤, 52:部成待撤, 55:部成)
        # 注意:具体状态码参考QMT文档,通常只要不是 56(已成), 54(已撤), 57(废单) 就需要撤单
        if order.m_strInstrumentID == ContextInfo.stock_code:
            if order.m_nOrderStatus in [48, 49, 50, 51, 52, 55]:
                # 执行撤单
                cancel(order.m_strOrderSysID, ContextInfo.account_id, ContextInfo.account_type, ContextInfo)
                # print(f"撤单: {order.m_strOrderSysID}")

代码关键点解析

  1. get_full_tick:

    • 这是做市策略的核心。相比于 get_market_dataget_full_tick 返回的是最新的快照数据,包含买一至买五、卖一至卖五的详细盘口,速度更快,适合高频操作。
  2. passorder 函数:

    • 这是 QMT 最底层的下单接口。
    • opType: 23 代表股票买入,24 代表股票卖出。如果是期货,需要改为 0 (开多) / 3 (开空) 等。
    • prType: 11 代表限价单(Limit Order),这是做市商必须使用的,因为我们需要指定价格挂单,而不是市价吃单。
  3. 撤单逻辑 (cancel_all_orders):

    • 做市商策略必须包含撤单逻辑。因为市场价格在不断变化,上一秒挂的单子如果偏离了当前盘口,就没有意义了,甚至可能造成风险。
    • 我们遍历 get_trade_detail_data 返回的订单列表,找到状态为“未成交”或“部分成交”的订单进行撤销。
  4. 风险提示:

    • 库存风险: 如果市场单边剧烈上涨或下跌,你可能会成交一边的单子(例如一直买入),导致库存积压产生亏损。
    • T+1 限制: 如果是 A 股股票,当天买入的无法当天卖出。此策略更适合 ETF可转债期货 等 T+0 品种,或者你本身持有大量底仓用于日内回转交易。

如何运行

  1. 打开 QMT 客户端,进入【模型研究】或【策略编辑器】。
  2. 新建一个 Python 策略,将上述代码粘贴进去。
  3. 修改 init 函数中的 ContextInfo.account_id 为你的真实资金账号。
  4. 修改 ContextInfo.stock_code 为你想交易的品种。
  5. 点击【运行】(建议先在模拟盘测试)。

Q&A

Q: 这个策略可以直接用于期货吗?
A: 代码逻辑通用,但需要修改 passorder 的参数。期货的 opType 不同(开仓、平今、平昨),且需要区分多空方向。此外,ContextInfo.account_type 需要改为 'FUTURE'

Q: 为什么使用 get_full_tick 而不是 handlebar 的传入数据?
A: handlebar 虽然由行情驱动,但在高频场景下,主动调用 get_full_tick 能确保获取到内存中最新的切片数据,减少数据延迟。

Q: 如何控制挂单频率?
A: 目前代码是每个 Tick(约3秒一次)触发一次。如果觉得太快,可以在 handlebar 里增加 time.time() 的判断逻辑,例如每隔 5 秒才执行一次下单逻辑。