问题描述
解决方案
编写赚取期权时间价值(Theta Decay)的策略,最经典的方法是卖出宽跨式组合(Short Strangle)或卖出跨式组合(Short Straddle)。
这类策略的核心逻辑是:做空期权。作为期权卖方,随着时间流逝,期权的时间价值会加速衰减,从而获利。为了控制风险,通常选择虚值(OTM)期权构建宽跨式组合,即:
- 卖出虚值看涨期权(Short OTM Call)
- 卖出虚值看跌期权(Short OTM Put)
以下是一个基于 QMT 平台的 Python 策略示例。该策略会自动选择当月合约,根据标的(如沪深300ETF)价格,选择虚值一定幅度的合约进行卖出开仓,并包含止损逻辑。
策略逻辑说明
- 标的:510300.SH(沪深300ETF)。
- 开仓条件:当前无持仓时,获取标的最新价,计算目标行权价(例如标的价格 $\pm$ 2%)。
- 合约选择:筛选当月到期的期权合约,找到行权价最接近目标的 Call 和 Put。
- 交易执行:卖出开仓(Short Open)这两个合约。
- 风控/止损:如果单腿亏损超过设定阈值(如 50%)或临近到期日,则平仓离场。
QMT 策略代码
# -*- coding: gbk -*-
import time
import datetime
def init(ContextInfo):
# ================= 策略参数设置 =================
ContextInfo.account_id = '您的资金账号' # 请替换为实际账号
ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:股票期权
ContextInfo.underlying = '510300.SH' # 标的:沪深300ETF
ContextInfo.otm_pct = 0.02 # 虚值幅度,例如 0.02 代表 2%
ContextInfo.trade_vol = 1 # 每次交易张数
ContextInfo.stop_loss_pct = 0.5 # 单腿止损比例 (50%)
ContextInfo.close_days_before = 2 # 到期前多少天强制平仓
# 设置账号
ContextInfo.set_account(ContextInfo.account_id)
# 缓存变量
ContextInfo.holdings = {'CALL': None, 'PUT': None}
print("策略初始化完成,准备赚取时间价值...")
def handlebar(ContextInfo):
# 获取当前K线索引
index = ContextInfo.barpos
# 获取当前时间
timetag = ContextInfo.get_bar_timetag(index)
current_date_str = timetag_to_datetime(timetag, '%Y%m%d')
# 1. 获取标的最新价格
market_data = ContextInfo.get_market_data_ex(
['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=True
)
if ContextInfo.underlying not in market_data:
return
underlying_price = market_data[ContextInfo.underlying].iloc[-1]['close']
# 2. 检查持仓与止损
check_positions_and_stop_loss(ContextInfo, underlying_price, current_date_str)
# 3. 如果没有持仓,且不在临近到期日,则开仓
if ContextInfo.holdings['CALL'] is None and ContextInfo.holdings['PUT'] is None:
# 获取当月合约到期日
expire_date = get_current_month_expire_date(ContextInfo, current_date_str)
if not expire_date:
return
# 检查是否临近到期(避险)
days_to_expire = days_between(current_date_str, str(expire_date))
if days_to_expire <= ContextInfo.close_days_before:
print(f"临近到期日 {expire_date},暂停开新仓")
return
open_short_strangle(ContextInfo, underlying_price, str(expire_date))
def open_short_strangle(ContextInfo, current_price, expire_date):
"""
构建卖出宽跨式组合
"""
# 计算目标行权价
target_call_strike = current_price * (1 + ContextInfo.otm_pct)
target_put_strike = current_price * (1 - ContextInfo.otm_pct)
# 获取期权列表
# get_option_list(标的, 到期日, 类型)
# 注意:get_option_list 的到期日参数如果是YYYYMMDD格式,isavailable=True返回可交易的
call_list = ContextInfo.get_option_list(ContextInfo.underlying, expire_date, "CALL", True)
put_list = ContextInfo.get_option_list(ContextInfo.underlying, expire_date, "PUT", True)
# 筛选最接近目标行权价的合约
target_call = find_closest_contract(ContextInfo, call_list, target_call_strike)
target_put = find_closest_contract(ContextInfo, put_list, target_put_strike)
if target_call and target_put:
print(f"标的价格: {current_price:.3f}")
print(f"目标Call行权价: {target_call_strike:.3f}, 选中合约: {target_call}")
print(f"目标Put行权价: {target_put_strike:.3f}, 选中合约: {target_put}")
# 下单:卖出开仓 (opType=52)
# 卖出 Call
passorder(52, 1101, ContextInfo.account_id, target_call, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.holdings['CALL'] = target_call
# 卖出 Put
passorder(52, 1101, ContextInfo.account_id, target_put, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.holdings['PUT'] = target_put
print("卖出宽跨式组合指令已发送")
def check_positions_and_stop_loss(ContextInfo, underlying_price, current_date_str):
"""
检查持仓,执行止损或到期平仓
"""
# 获取账户持仓信息
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
# 更新本地持仓状态(防止重启后丢失)
has_call = False
has_put = False
for pos in positions:
instrument = pos.m_strInstrumentID
# 检查是否需要到期平仓
detail = ContextInfo.get_instrumentdetail(instrument)
expire_date = detail.get('ExpireDate')
if expire_date:
days = days_between(current_date_str, str(expire_date))
if days <= ContextInfo.close_days_before:
print(f"合约 {instrument} 临近到期(剩{days}天),强制平仓")
close_position(ContextInfo, instrument, pos.m_nVolume)
continue
# 检查止损 (简单逻辑:如果期权价格上涨过多,代表亏损)
# 注意:卖方是价格上涨亏损。这里简化使用标的价格位移判断,实际应获取期权持仓盈亏
# 获取期权最新价
opt_data = ContextInfo.get_market_data_ex(['close'], [instrument], period='1d', count=1)
if instrument in opt_data:
current_opt_price = opt_data[instrument].iloc[-1]['close']
avg_cost = pos.m_dOpenPrice
# 卖方亏损计算:(现价 - 成本) / 成本 > 阈值
if avg_cost > 0:
loss_pct = (current_opt_price - avg_cost) / avg_cost
if loss_pct > ContextInfo.stop_loss_pct:
print(f"触发止损: {instrument}, 成本:{avg_cost}, 现价:{current_opt_price}, 亏损率:{loss_pct:.2%}")
close_position(ContextInfo, instrument, pos.m_nVolume)
continue
# 更新状态
if instrument == ContextInfo.holdings['CALL']: has_call = True
if instrument == ContextInfo.holdings['PUT']: has_put = True
# 如果持仓被平掉,更新内存状态
if not has_call: ContextInfo.holdings['CALL'] = None
if not has_put: ContextInfo.holdings['PUT'] = None
def close_position(ContextInfo, code, volume):
"""
买入平仓
"""
# opType=53: 买入平仓
passorder(53, 1101, ContextInfo.account_id, code, 5, -1, volume, ContextInfo)
print(f"平仓指令已发送: {code}, 数量: {volume}")
def find_closest_contract(ContextInfo, contract_list, target_strike):
"""
在合约列表中寻找行权价最接近目标的合约
"""
closest_contract = None
min_diff = float('inf')
for code in contract_list:
# 获取合约详细信息
detail = ContextInfo.get_option_detail_data(code)
strike_price = detail.get('OptExercisePrice')
if strike_price:
diff = abs(strike_price - target_strike)
if diff < min_diff:
min_diff = diff
closest_contract = code
return closest_contract
def get_current_month_expire_date(ContextInfo, current_date_str):
"""
获取当月或次月期权的到期日
"""
# 这里简化处理:获取标的对应的所有期权合约,解析出最近的一个到期日
# 实际应用中可以使用 get_option_list 传入 YYYYMM 格式获取
# 获取当前年月
year = int(current_date_str[0:4])
month = int(current_date_str[4:6])
# 尝试获取当月合约
month_str = f"{year}{month:02d}"
# 随便取一个类型获取列表,主要是为了拿到日期
# 注意:QMT API 中 get_option_list 第二个参数如果是月份,返回该月到期的合约
# 但我们需要具体的到期日YYYYMMDD
# 这种方式比较间接,更直接的是遍历几个合约看ExpireDate
# 为了演示方便,我们取标的的一个合约列表,找出大于当前日期的最小到期日
# 获取当前可交易的所有Call
all_calls = ContextInfo.get_option_list(ContextInfo.underlying, "", "CALL", True)
valid_dates = set()
current_date_int = int(current_date_str)
for code in all_calls[:50]: # 抽样检查,避免遍历太多
detail = ContextInfo.get_instrumentdetail(code)
edate = detail.get('ExpireDate')
if edate and edate > current_date_int:
valid_dates.add(edate)
if not valid_dates:
return None
# 返回最近的到期日
return min(valid_dates)
def days_between(d1, d2):
"""计算两个YYYYMMDD字符串之间的天数差"""
d1 = datetime.datetime.strptime(d1, "%Y%m%d")
d2 = datetime.datetime.strptime(d2, "%Y%m%d")
return (d2 - d1).days
代码关键点解析
-
passorder交易类型:- 期权交易与股票不同,必须指定具体的开平仓类型。
- 52:卖出开仓(Short Open),这是赚取时间价值的关键动作。
- 53:买入平仓(Buy Close),用于止损或获利了结。
1101:代表单股/单张、限价/市价委托(配合价格类型使用)。这里使用了5(最新价) 和-1(价格无效,配合最新价使用)。
-
合约选择 (
get_option_list&get_option_detail_data):get_option_list用于获取某标的下的期权合约列表。get_option_detail_data(或get_instrumentdetail) 用于获取合约的具体属性,最重要的是 行权价 (OptExercisePrice) 和 到期日 (ExpireDate)。- 策略通过计算
current_price * (1 ± otm_pct)来定位虚值合约,这样可以获得权利金的同时保留一定的安全边际。
-
风险控制:
- 临近到期平仓:期权在到期日附近 Gamma 风险巨大(标的小幅波动会导致 Delta 剧烈变化),且面临行权指派风险。策略设置了
close_days_before,在到期前 2 天强制平仓。 - 价格止损:作为卖方,收益有限(权利金),风险无限。代码中简单计算了期权价格涨幅,如果涨幅超过 50%(意味着亏损),则强制平仓。
- 临近到期平仓:期权在到期日附近 Gamma 风险巨大(标的小幅波动会导致 Delta 剧烈变化),且面临行权指派风险。策略设置了
-
数据获取:
- 使用
get_market_data_ex获取标的 ETF 的价格,这是判断 ATM(平值)位置的基础。
- 使用
风险提示
- 保证金风险:卖出期权需要缴纳保证金,如果标的向不利方向大幅波动,可能面临追加保证金或强平风险。
- 黑天鹅风险:宽跨式策略在市场横盘时表现最好,但如果市场出现单边暴涨或暴跌,卖方会面临巨大亏损。实盘中建议配合 Delta 对冲 或购买更深虚值的期权进行保护(即构建铁以此鹰式组合 Iron Condor)。
如何运行
- 在 QMT 策略编辑器中新建一个 Python 策略。
- 将上述代码复制进去。
- 修改
init中的account_id为您的真实期权账号。 - 确保已下载 510300.SH 及相关期权的历史数据(如果是回测)或确保行情连接正常(如果是实盘)。
- 点击运行。