问题描述
解决方案
这是一个关于如何在QMT平台上实现**蝶式价差(Butterfly Spread)**期权策略的详细指南。
蝶式价差是一种中性策略,通常用于预期标的资产价格在到期日将停留在某个特定价格附近(即波动率下降)。
策略逻辑(以买入看涨蝶式为例)
- 观点:预期标的资产价格变化不大,波动率降低。
- 构建:
- 买入 1张低行权价($K_1$)的看涨期权(实值)。
- 卖出 2张中间行权价($K_2$)的看涨期权(平值)。
- 买入 1张高行权价($K_3$)的看涨期权(虚值)。
- 条件:$K_2 - K_1 = K_3 - K_2$ (行权价间距相等)。
QMT Python 策略代码实现
以下代码实现了一个自动化的买入看涨蝶式策略。它会自动获取标的当前价格,寻找平值合约(ATM),并选取左右相邻档位的合约作为翅膀。
# -*- coding: gbk -*-
import pandas as pd
import time
def init(ContextInfo):
# 1. 设置资金账号 (请修改为您的实际账号)
ContextInfo.accID = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.accID)
# 2. 策略参数设置
ContextInfo.underlying = '510050.SH' # 标的资产:上证50ETF
ContextInfo.gap_level = 1 # 档位间隔:1表示选取相邻的行权价
ContextInfo.trade_executed = False # 交易标志位,防止重复下单
ContextInfo.expiry_month = '202312' # 目标到期月份,格式YYYYMM (需根据实际情况修改)
print("策略初始化完成,标的:{}, 到期月:{}".format(ContextInfo.underlying, ContextInfo.expiry_month))
def handlebar(ContextInfo):
# 仅在最后一根K线(实时行情)运行,且只交易一次
if not ContextInfo.is_last_bar() or ContextInfo.trade_executed:
return
# 1. 获取标的最新价格
market_data = ContextInfo.get_market_data_ex(
['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=True
)
if ContextInfo.underlying not in market_data:
print("未获取到标的行情")
return
current_price = market_data[ContextInfo.underlying].iloc[-1]['close']
print("标的当前价格: {:.4f}".format(current_price))
# 2. 获取期权链 (指定标的、到期月、看涨Call)
# get_option_list(标的代码, 到期月YYYYMM, 类型CALL/PUT)
option_list = ContextInfo.get_option_list(ContextInfo.underlying, ContextInfo.expiry_month, "CALL")
if not option_list:
print("未获取到期权合约列表,请检查到期月份设置")
return
# 3. 筛选并排序合约
# 我们需要获取每个合约的行权价
contracts_info = []
for code in option_list:
detail = ContextInfo.get_instrumentdetail(code)
if detail:
contracts_info.append({
'code': code,
'strike_price': detail['OptExercisePrice'],
'name': detail['InstrumentName']
})
# 按行权价从小到大排序
sorted_contracts = sorted(contracts_info, key=lambda x: x['strike_price'])
if len(sorted_contracts) < 3:
print("合约数量不足,无法构建蝶式策略")
return
# 4. 寻找平值合约 (K2) - 行权价最接近标的价格的合约
# 使用 min 函数寻找绝对差值最小的合约索引
atm_index = min(range(len(sorted_contracts)), key=lambda i: abs(sorted_contracts[i]['strike_price'] - current_price))
# 5. 确定 K1 (低行权价) 和 K3 (高行权价)
gap = ContextInfo.gap_level
k1_index = atm_index - gap
k3_index = atm_index + gap
# 检查索引是否越界
if k1_index < 0 or k3_index >= len(sorted_contracts):
print("无法构建策略:平值合约过于接近边界,无法选取 K1 或 K3")
return
leg1 = sorted_contracts[k1_index] # K1: 买入实值
leg2 = sorted_contracts[atm_index] # K2: 卖出平值
leg3 = sorted_contracts[k3_index] # K3: 买入虚值
print("构建蝶式组合:")
print("Leg 1 (买入): {} 行权价: {}".format(leg1['name'], leg1['strike_price']))
print("Leg 2 (卖出): {} 行权价: {}".format(leg2['name'], leg2['strike_price']))
print("Leg 3 (买入): {} 行权价: {}".format(leg3['name'], leg3['strike_price']))
# 6. 执行下单 (使用 passorder)
# opType说明: 50:买入开仓, 52:卖出开仓
# orderType: 1101 (单股、单账号、普通、股/手方式)
# prType: 5 (最新价), 14 (对手价) - 这里演示使用对手价以保证成交
# 下单 Leg 1: 买入 1 张 K1
passorder(50, 1101, ContextInfo.accID, leg1['code'], 14, -1, 1, ContextInfo)
print("下单: 买入开仓 1张 {}".format(leg1['code']))
# 下单 Leg 2: 卖出 2 张 K2
passorder(52, 1101, ContextInfo.accID, leg2['code'], 14, -1, 2, ContextInfo)
print("下单: 卖出开仓 2张 {}".format(leg2['code']))
# 下单 Leg 3: 买入 1 张 K3
passorder(50, 1101, ContextInfo.accID, leg3['code'], 14, -1, 1, ContextInfo)
print("下单: 买入开仓 1张 {}".format(leg3['code']))
ContextInfo.trade_executed = True
代码关键点解析
-
合约选择逻辑 (
handlebar第2-5步):- 获取期权链:使用
ContextInfo.get_option_list获取指定月份的所有Call合约。 - 获取行权价:通过
ContextInfo.get_instrumentdetail获取每个合约的详细信息,提取OptExercisePrice(行权价)。 - 定位ATM:计算所有行权价与标的现价的差值绝对值,最小的即为平值合约(K2)。
- 确定翅膀:根据
gap_level(间隔档位),向左找K1,向右找K3。
- 获取期权链:使用
-
下单逻辑 (
handlebar第6步):passorder函数:QMT的核心下单函数。opType参数:50:买入开仓(用于K1和K3)。52:卖出开仓(用于K2)。
prType参数:这里使用了14(对手价),在实盘中为了保证三条腿同时成交,通常建议使用对手价或更激进的价格,或者使用算法交易接口。- 数量:严格遵循 1:2:1 的比例(买1张K1,卖2张K2,买1张K3)。
-
风险控制:
- 代码中加入了
ContextInfo.trade_executed标志位,确保策略在演示时只运行一次,防止重复开仓。 - 加入了边界检查,防止ATM合约是列表中的第一个或最后一个,导致无法选取K1或K3。
- 代码中加入了
注意事项
- 保证金:卖出开仓(Leg 2)需要占用保证金,请确保账户资金充足。
- 滑点风险:蝶式策略涉及三个合约,如果分别下单(Leging in),可能会面临价格变动的风险。在QMT中,如果资金量大,建议使用组合交易功能或算法交易功能来尽量同步成交。
- 到期日管理:代码中硬编码了
expiry_month = '202312',实际使用时请务必修改为当前的有效月份。 - 编码格式:QMT Python脚本必须在第一行加上
# -*- coding: gbk -*-,否则中文注释会报错。
Q&A
Q: 如何将此策略改为卖出蝶式(做多波动率)?
A: 只需反转下单方向:
- Leg 1 (K1): 卖出开仓 (
opType=52) - Leg 2 (K2): 买入开仓 (
opType=50),数量为2 - Leg 3 (K3): 卖出开仓 (
opType=52)
Q: 如何获取当前最近的到期月份而不是手动输入?
A: 可以使用 ContextInfo.get_option_list 获取所有合约后,解析其到期日,或者通过标的ETF的详细信息查找关联的期权月份。简单的做法是获取当前日期,格式化为 YYYYMM。
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是QMT较新的接口,返回的数据结构(DataFrame)更标准,且支持订阅模式,处理实时数据更稳定。