问题描述
解决方案
在 QMT 中编写备兑开仓(Covered Call)自动化策略,核心逻辑是:持有标的证券(如 ETF 或股票) + 卖出相应数量的认购期权(Call)。
备兑开仓在 QMT 的交易接口中有专门的操作类型。与普通卖出开仓(保证金交易)不同,备兑开仓使用持有的现货作为担保,不需要占用现金保证金。
以下是详细的策略设计与代码实现。
1. 策略逻辑设计
- 初始化 (
init):- 设置标的证券(例如
510050.SH)。 - 设置期权账号。
- 设定备兑参数(例如:选择下个月到期、行权价高于当前价一档的虚值合约)。
- 设置标的证券(例如
- 行情与持仓检查 (
handlebar):- 获取标的证券的持仓数量。
- 获取当前标的价格。
- 检查是否已经持有期权空头持仓。
- 合约选择:
- 根据标的价格和到期时间,筛选出合适的认购期权合约(Call)。
- 交易执行:
- 如果持有标的且未持有期权:执行 备兑开仓 (
opType=54)。 - 如果期权临近到期:执行 备兑平仓 (
opType=55) 或等待行权/归零。
- 如果持有标的且未持有期权:执行 备兑开仓 (
2. 关键 API 说明
get_option_list: 获取期权合约列表。get_instrumentdetail: 获取合约详情(行权价、到期日等)。get_trade_detail_data: 获取账户持仓,判断是否有标的和期权持仓。passorder: 下单函数。opType=54: 备兑开仓 (Covered Open)。opType=55: 备兑平仓 (Covered Close)。opType=58: 证券锁定 (Lock Underlying)。部分券商要求先锁定现货才能备兑开仓,但大多数柜台支持直接发 54 指令自动锁定。
3. 策略代码实现
以下是一个完整的 Python 策略示例。该策略会检查账户中的 510050.SH 持仓,如果持有现货且没有期权持仓,则自动卖出下月到期的虚值一档认购期权。
# -*- coding: gbk -*-
import time
import datetime
def init(ContextInfo):
# 1. 设置基本参数
ContextInfo.account_id = '您的期权资金账号' # 请修改为实际账号
ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:股票期权
ContextInfo.underlying = '510050.SH' # 标的ETF
ContextInfo.target_month_offset = 0 # 0表示当月,1表示下月
ContextInfo.strike_offset = 1 # 行权价偏离档位:1表示虚值一档(行权价 > 标的价)
# 设置账号
ContextInfo.set_account(ContextInfo.account_id)
# 设定定时运行,例如每天上午10:00检查一次
# 实际盘中可以使用 handlebar 驱动,这里为了演示逻辑清晰使用定时器或在handlebar中控制频率
print("备兑策略初始化完成")
def get_target_contract(ContextInfo):
"""
根据规则筛选期权合约
"""
# 1. 获取标的最新价
last_price_data = ContextInfo.get_market_data_ex(
['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=False
)
if ContextInfo.underlying not in last_price_data:
print("无法获取标的行情")
return None
underlying_price = last_price_data[ContextInfo.underlying].iloc[-1]['close']
print(f"标的 {ContextInfo.underlying} 当前价格: {underlying_price}")
# 2. 确定目标到期月份 (格式 YYYYMM)
# 这里简单处理:获取当前日期,计算目标月份
today = datetime.date.today()
# 简单的月份推算逻辑,实际需考虑行权日
target_date = today + datetime.timedelta(days=30 * ContextInfo.target_month_offset)
target_month_str = target_date.strftime('%Y%m')
# 3. 获取期权列表 (Call)
# get_option_list(标的, 月份, 类型)
option_list = ContextInfo.get_option_list(ContextInfo.underlying, target_month_str, "CALL")
if not option_list:
print(f"未找到 {target_month_str} 的认购期权")
return None
# 4. 筛选行权价
# 获取合约详细信息,提取行权价
contracts_info = []
for opt_code in option_list:
detail = ContextInfo.get_instrumentdetail(opt_code)
if detail:
contracts_info.append({
'code': opt_code,
'strike_price': detail['OptExercisePrice']
})
# 按行权价排序
contracts_info.sort(key=lambda x: x['strike_price'])
# 寻找刚好大于标的价的合约(虚值)
target_contract = None
# 找到第一个行权价 > 标的价的位置
atm_index = -1
for i, info in enumerate(contracts_info):
if info['strike_price'] > underlying_price:
atm_index = i
break
if atm_index != -1:
# 根据偏离档位选择
target_index = atm_index + (ContextInfo.strike_offset - 1)
if target_index < len(contracts_info):
target_contract = contracts_info[target_index]['code']
print(f"选中合约: {target_contract}, 行权价: {contracts_info[target_index]['strike_price']}")
return target_contract
def handlebar(ContextInfo):
# 限制运行频率,避免每个Tick都触发,这里仅在K线结束或特定时间运行
if not ContextInfo.is_last_bar():
return
# 1. 获取账户持仓信息
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
# 统计标的持仓和期权持仓
underlying_vol = 0
has_short_call = False
short_call_code = ""
for pos in positions:
# 检查标的持仓
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.underlying:
underlying_vol = pos.m_nVolume
# 检查是否已有期权义务仓(卖出持仓)
# m_nDirection: 49 代表卖出(空头)
# 也可以通过 m_strInstrumentID 判断是否是期权
if ".SHO" in (pos.m_strInstrumentID + '.' + pos.m_strExchangeID) or ".SZO" in (pos.m_strInstrumentID + '.' + pos.m_strExchangeID):
# 简单判断:如果有期权持仓且是空头
if pos.m_nVolume > 0 and pos.m_nDirection == 49:
has_short_call = True
short_call_code = pos.m_strInstrumentID + '.' + pos.m_strExchangeID
print(f"当前标的持仓: {underlying_vol}, 是否持有期权空头: {has_short_call}")
# 2. 策略逻辑:有标的 + 无期权 -> 开仓
# 假设一张期权对应 10000 股标的 (ETF通常是10000)
contract_unit = 10000
can_open_lots = int(underlying_vol / contract_unit)
if can_open_lots > 0 and not has_short_call:
print(f"满足开仓条件,可开 {can_open_lots} 张备兑")
# 获取目标合约
target_code = get_target_contract(ContextInfo)
if target_code:
# 执行备兑开仓
# opType=54 (备兑开仓)
# orderType=1101 (单股单账号普通下单)
# prType=14 (对手价)
# price=0 (对手价模式下价格无效)
# volume=1 (张数,这里演示开1张,实际可开 can_open_lots)
print(f"正在对 {target_code} 执行备兑开仓...")
passorder(
54, # opType: 备兑开仓
1101, # orderType
ContextInfo.account_id,
target_code,
14, # prType: 对手价
0, # price
1, # volume: 开1张
ContextInfo
)
elif has_short_call:
print(f"已有期权持仓 {short_call_code},跳过开仓")
# 这里可以添加平仓逻辑:例如临近到期,或者获利达到预期进行平仓
# 平仓使用 passorder(55, ...) 备兑平仓
else:
print("标的持仓不足,无法进行备兑开仓")
4. 关键点与注意事项
-
备兑证券锁定 (Locking):
- 在 QMT 的
passorder中,opType=54(备兑开仓) 通常会自动处理证券锁定。 - 如果您的券商柜台不支持自动锁定,您可能需要先发送
opType=58(证券锁定) 指令,将持有的 ETF/股票 锁定为备兑证券,然后再发送opType=52(卖出开仓)。 - 推荐方式:直接使用
54,如果报错提示“备兑证券不足”,则说明需要先手动或代码调用58进行锁定。
- 在 QMT 的
-
合约单位:
- ETF 期权通常一张合约对应 10,000 份 ETF。
- 个股期权(深交所)一张合约通常对应 100 股或其他调整后的数量。
- 代码中
contract_unit = 10000是针对 50ETF/300ETF 的默认值,建议通过get_instrumentdetail获取VolumeMultiple属性来动态确定。
-
交易方向:
- 备兑策略本质是 卖出 期权。在
get_trade_detail_data返回的POSITION对象中,m_nDirection为49(ENTRUST_SELL) 表示空头持仓(义务仓)。
- 备兑策略本质是 卖出 期权。在
-
风险控制:
- 备兑策略虽然风险较低(有现货保护),但如果标的大涨,会限制收益(收益封顶)。
- 如果标的大跌,现货端会亏损,虽然期权端有权利金收入作为缓冲,但整体仍可能亏损。
-
实盘配置:
- 在 QMT 界面中,确保“交易设置”里的期权账号已登录。
- 确保 Python 库路径设置正确,且已下载必要的历史数据(如果使用了回测功能)。
Q&A
Q: 如何进行备兑平仓?
A: 将 passorder 的第一个参数 opType 改为 55。
Q: 如何查询合约的行权价?
A: 使用 ContextInfo.get_instrumentdetail(code),返回的字典中 OptExercisePrice 字段即为行权价。
Q: 为什么下单失败提示“可用不足”?
A: 备兑开仓不占用资金,但占用现货。请检查您的普通账户或信用账户中是否有足额的标的证券,并且该证券是否已经划转到了期权账户的“备兑证券”科目下(如果券商不支持自动划转)。