问题描述
解决方案
这是一个基于 QMT Python API 的简单做市商(Market Maker)策略示例。
策略逻辑说明
这个策略的核心逻辑是**“盘口双边挂单”**,适用于 T+0 品种(如 ETF、可转债或期货)或持有底仓的股票。
- 获取行情:在每个 Tick(分笔)数据到来时,获取当前的买一价(Bid1)和卖一价(Ask1)。
- 撤单机制:在挂新单之前,先撤销当前标的未成交的所有挂单,防止挂单堆积占用资金或导致敞口过大。
- 计算价格:
- 买单价格 = 买一价 - 设定的价差(Spread)
- 卖单价格 = 卖一价 + 设定的价差(Spread)
- 双边下单:同时发出限价买入和限价卖出指令。
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}")
代码关键点解析
-
get_full_tick:- 这是做市策略的核心。相比于
get_market_data,get_full_tick返回的是最新的快照数据,包含买一至买五、卖一至卖五的详细盘口,速度更快,适合高频操作。
- 这是做市策略的核心。相比于
-
passorder函数:- 这是 QMT 最底层的下单接口。
opType: 23 代表股票买入,24 代表股票卖出。如果是期货,需要改为 0 (开多) / 3 (开空) 等。prType: 11 代表限价单(Limit Order),这是做市商必须使用的,因为我们需要指定价格挂单,而不是市价吃单。
-
撤单逻辑 (
cancel_all_orders):- 做市商策略必须包含撤单逻辑。因为市场价格在不断变化,上一秒挂的单子如果偏离了当前盘口,就没有意义了,甚至可能造成风险。
- 我们遍历
get_trade_detail_data返回的订单列表,找到状态为“未成交”或“部分成交”的订单进行撤销。
-
风险提示:
- 库存风险: 如果市场单边剧烈上涨或下跌,你可能会成交一边的单子(例如一直买入),导致库存积压产生亏损。
- T+1 限制: 如果是 A 股股票,当天买入的无法当天卖出。此策略更适合 ETF、可转债 或 期货 等 T+0 品种,或者你本身持有大量底仓用于日内回转交易。
如何运行
- 打开 QMT 客户端,进入【模型研究】或【策略编辑器】。
- 新建一个 Python 策略,将上述代码粘贴进去。
- 修改
init函数中的ContextInfo.account_id为你的真实资金账号。 - 修改
ContextInfo.stock_code为你想交易的品种。 - 点击【运行】(建议先在模拟盘测试)。
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 秒才执行一次下单逻辑。