问题描述
解决方案
编写一个期权做市商(Market Maker)策略是一个复杂的任务,涉及理论定价、库存管理、订单管理和风险控制。在QMT中,我们可以利用其内置的期权定价函数(BSM模型)和交易接口来实现一个基础的自动双边报价策略。
策略逻辑概述
- 理论定价:使用 Black-Scholes-Merton (BSM) 模型计算期权的理论合理价格(Fair Price)。
- 确定价差:在理论价格的基础上,加减设定的价差(Spread),形成买单价格(Bid)和卖单价格(Ask)。
- 撤单机制:在每一轮报价前,撤销之前未成交的挂单,防止订单堆积和价格滞后。
- 双边报价:同时发送买入开仓(Bid)和卖出开仓(Ask)的限价委托。
- 风控(简化版):检查标的价格有效性,避免在数据异常时下单。
核心 API 说明
ContextInfo.bsm_price(...): 计算期权理论价格。ContextInfo.get_market_data_ex(...): 获取标的资产实时行情。ContextInfo.get_trade_detail_data(...): 获取当前挂单状态。cancel(...): 撤单。passorder(...): 下单。
QMT 期权做市商策略代码
# -*- coding: gbk -*-
import time
import datetime
import math
def init(ContextInfo):
# ================= 策略参数设置 =================
# 资金账号 (请修改为您的实际期权账号)
ContextInfo.accID = 'YOUR_OPTION_ACCOUNT_ID'
# 目标期权合约 (例如: 50ETF购x月x.xxx)
# 请确保该合约在您的行情列表中存在
ContextInfo.option_code = '1000xxxx.SHO'
# 标的资产代码 (例如: 50ETF)
ContextInfo.underlying_code = '510050.SH'
# 期权类型: 'C' (认购) 或 'P' (认沽)
ContextInfo.option_type = 'C'
# 行权价 (Strike Price)
ContextInfo.strike_price = 3.000
# 到期日 (格式: YYYYMMDD)
ContextInfo.expire_date = '20231227'
# 无风险利率 (例如 2.5%)
ContextInfo.risk_free_rate = 0.025
# 预设波动率 (IV),实际交易中通常需要根据市场反推或使用波动率曲面
ContextInfo.sigma = 0.20
# 报价单边价差 (Spread的一半)
# Bid = FairPrice - half_spread
# Ask = FairPrice + half_spread
ContextInfo.half_spread = 0.0010
# 单笔报单数量 (张)
ContextInfo.volume = 1
# 策略运行间隔 (秒)
ContextInfo.run_interval = 5
ContextInfo.last_run_time = 0
# 设置账号
ContextInfo.set_account(ContextInfo.accID)
print("期权做市策略初始化完成")
def handlebar(ContextInfo):
# 仅在实盘的最后一根K线(实时行情)运行
if not ContextInfo.is_last_bar():
return
# 控制运行频率
current_time = time.time()
if current_time - ContextInfo.last_run_time < ContextInfo.run_interval:
return
ContextInfo.last_run_time = current_time
# 1. 获取标的资产最新价格
# 获取标的最近一笔Tick数据
tick_data = ContextInfo.get_full_tick([ContextInfo.underlying_code])
if ContextInfo.underlying_code not in tick_data:
print(f"未获取到标的 {ContextInfo.underlying_code} 行情")
return
underlying_price = tick_data[ContextInfo.underlying_code]['lastPrice']
if underlying_price <= 0:
return
# 2. 计算剩余天数 (Days to Maturity)
try:
today = datetime.datetime.now()
exp_date = datetime.datetime.strptime(ContextInfo.expire_date, '%Y%m%d')
delta = exp_date - today
days_to_maturity = delta.days + (delta.seconds / 86400.0)
if days_to_maturity <= 0:
print("期权已到期或过期,停止报价")
return
except Exception as e:
print(f"日期计算错误: {e}")
return
# 3. 计算 BSM 理论价格 (Fair Price)
# bsm_price(optionType, objectPrices, strikePrice, riskFree, sigma, days, dividend)
# 假设分红率为 0
fair_price = ContextInfo.bsm_price(
ContextInfo.option_type,
underlying_price,
ContextInfo.strike_price,
ContextInfo.risk_free_rate,
ContextInfo.sigma,
days_to_maturity,
0
)
if math.isnan(fair_price) or fair_price <= 0:
print("理论价格计算异常")
return
# 4. 计算双边报价价格
# 价格需要符合最小变动单位 (假设为 0.0001,具体视合约而定)
bid_price = round(fair_price - ContextInfo.half_spread, 4)
ask_price = round(fair_price + ContextInfo.half_spread, 4)
print(f"标的价:{underlying_price}, 理论价:{fair_price:.4f}, 拟报买:{bid_price}, 拟报卖:{ask_price}")
# 5. 撤销旧订单 (Cancel Logic)
# 获取当前未成交委托
orders = get_trade_detail_data(ContextInfo.accID, 'STOCK_OPTION', 'ORDER')
for order in orders:
# 仅撤销本策略针对该合约的挂单 (通过合约代码判断)
# 注意:这里会撤销该合约所有未成交单,如果有人工单需谨慎
if order.m_strInstrumentID == ContextInfo.option_code and order.m_nOrderStatus in [48, 49, 50, 51, 52, 55]: # 未成交或部成状态
cancel(order.m_strOrderSysID, ContextInfo.accID, 'STOCK_OPTION', ContextInfo)
print(f"撤单: {order.m_strOrderSysID}")
# 6. 发送新订单 (Place Orders)
# 这里的 opType 使用:
# 50: 买入开仓 (Buy Open)
# 52: 卖出开仓 (Sell Open)
# orderType 1101: 单股/单账号/普通/限价
# prType 11: 指定价 (配合 price 参数)
# 发送买单 (提供流动性 - Bid)
if bid_price > 0:
passorder(50, 1101, ContextInfo.accID, ContextInfo.option_code, 11, bid_price, ContextInfo.volume, ContextInfo)
print(f"报单: 买入开仓 {ContextInfo.option_code} 价格 {bid_price} 数量 {ContextInfo.volume}")
# 发送卖单 (提供流动性 - Ask)
if ask_price > 0:
passorder(52, 1101, ContextInfo.accID, ContextInfo.option_code, 11, ask_price, ContextInfo.volume, ContextInfo)
print(f"报单: 卖出开仓 {ContextInfo.option_code} 价格 {ask_price} 数量 {ContextInfo.volume}")
代码关键点解析
-
BSM 定价 (
ContextInfo.bsm_price):- 这是做市商的核心。我们根据标的资产(如50ETF)的实时价格,结合行权价、剩余时间、利率和波动率算出理论价格。
- 注意: 代码中的
ContextInfo.sigma(波动率) 是固定值。在进阶策略中,你需要根据市场隐含波动率(IV)实时更新这个值,或者维护一个波动率曲面。
-
撤单逻辑:
- 做市商必须紧跟市场。如果标的价格变动,旧的挂单价格可能已经偏离合理区间(被“打穿”),导致亏损。
- 因此,代码逻辑是:先撤旧单,再报新单。
- 使用
get_trade_detail_data获取ORDER对象,检查m_nOrderStatus状态码(如 50=已报, 55=部成)来决定是否撤单。
-
下单接口 (
passorder):- 期权交易的
opType与股票不同。 50: 买入开仓 (做多波动率/方向)。52: 卖出开仓 (做空波动率/方向,即作为卖方)。- 做市商通常双边开仓(既买又卖),赚取 Bid-Ask Spread。
- 注:实际做市中,如果手里有持仓,可能需要判断是“平仓”还是“开仓”。本示例为了简化,统一使用“开仓”指令。
- 期权交易的
-
时间与频率控制:
- 使用
ContextInfo.run_interval控制报价频率(例如每5秒一次),避免过于频繁的撤单报单被交易所限制或产生过多的废单。
- 使用
风险提示与改进建议
这是一个基础的演示策略,切勿直接用于实盘大资金交易,因为它缺少以下关键模块:
- Delta 对冲 (Delta Hedging):
- 做市商通常希望保持 Delta 中性。当成交后,持仓会有 Delta 敞口,需要买卖标的资产(如50ETF)来对冲风险。
- 库存管理:
- 如果买单成交多,卖单成交少,库存会积压。策略应根据当前持仓调整报价中心(例如库存多时,降低卖价以促进卖出)。
- 波动率拟合:
- 固定波动率会导致定价偏离市场。应使用
ContextInfo.get_option_iv获取市场隐含波动率,或自行计算实时 IV。
- 固定波动率会导致定价偏离市场。应使用
- 异常处理:
- 需要处理行情中断、涨跌停板、资金不足等异常情况。
Q&A
Q: 如何获取实时的隐含波动率而不是使用固定值?
A: 可以使用 ContextInfo.get_option_iv(optioncode) 接口获取交易所发布的或系统计算的实时隐含波动率,将其赋值给 ContextInfo.sigma。
Q: 为什么我的 passorder 下单没有反应?
A: 1. 检查 ContextInfo.accID 是否填写正确且已登录。2. 检查 opType 是否正确(期权是50/52)。3. 检查价格是否在涨跌停范围内。4. 检查是否有足够的保证金。
Q: 如何实现 Delta 对冲?
A: 你需要计算持仓的 Delta 总和。QMT 没有直接返回持仓 Delta 的接口,你需要遍历持仓,对每个合约调用 BSM 公式计算 Delta(BSM公式的一阶导数),加总后,如果 Delta 偏离 0 太多,就交易 50ETF 现货进行对冲。