问题描述
解决方案
这是一个基于 QMT 平台的期权 Delta 对冲(Delta Neutral)策略实现。
策略逻辑说明
- 目标:保持投资组合的 Delta 值接近于 0,以规避标的资产价格小幅变动的风险,主要赚取时间价值(Theta)或波动率(Vega)的收益。
- 核心步骤:
- 获取持仓:读取当前账户中持有的期权合约数量和标的资产(如 ETF 或 股票)数量。
- 计算 Delta:利用 BSM(Black-Scholes-Merton)模型计算期权的实时 Delta 值。
- 需要参数:标的价格 ($S$)、行权价 ($K$)、无风险利率 ($r$)、剩余期限 ($T$)、隐含波动率 ($\sigma$)。
- 计算对冲量:
- 期权总 Delta = 期权持仓量 $\times$ 合约乘数 $\times$ 单个期权 Delta。
- 为了使总 Delta 为 0,标的资产的目标持仓量应为:$-1 \times$ 期权总 Delta。
- 执行交易:比较标的资产的“目标持仓量”与“当前持仓量”,当偏差超过设定的阈值时,进行买入或卖出操作进行再平衡。
代码实现
# -*- coding: gbk -*-
import numpy as np
import math
from scipy.stats import norm
import datetime
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置账号 (请替换为您的实际资金账号)
# 账号类型:'STOCK'股票, 'STOCK_OPTION'期权
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.account_type = 'STOCK_OPTION' # 假设在一个期权账户中同时交易标的和期权,如果是组合账户需分别设置
ContextInfo.set_account(ContextInfo.account_id)
# 2. 策略参数设置
# 期权代码 (示例:50ETF购9月3000)
ContextInfo.option_code = '10005439.SHO'
# 标的资产代码 (示例:510050.SH)
ContextInfo.underlying_code = '510050.SH'
# 无风险利率 (年化,可设为国债收益率)
ContextInfo.risk_free_rate = 0.025
# 对冲阈值 (Delta偏差超过此数量的标的股数时才交易,避免频繁消耗手续费)
# 例如:偏差超过 500 股才调仓
ContextInfo.hedge_threshold = 500
# 3. 设定运行周期
# 建议在分钟线或Tick级别运行
print("策略初始化完成,监控标的: {}, 期权: {}".format(ContextInfo.underlying_code, ContextInfo.option_code))
def calculate_bsm_delta(S, K, T, r, sigma, option_type):
"""
计算 BSM 模型的 Delta 值
S: 标的现价
K: 行权价
T: 剩余期限(年)
r: 无风险利率
sigma: 波动率
option_type: 'CALL' 或 'PUT'
"""
if T <= 0:
return 0.0
d1 = (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T))
if option_type == 'CALL':
delta = norm.cdf(d1)
else:
delta = norm.cdf(d1) - 1
return delta
def get_days_to_expiration(expire_date_str):
"""
计算距离到期日的剩余年化时间
expire_date_str: 'YYYYMMDD' 格式
"""
try:
exp_date = datetime.datetime.strptime(str(expire_date_str), '%Y%m%d')
today = datetime.datetime.now()
days = (exp_date - today).days
# 如果是当天到期,给予极小的时间值防止除零
if days <= 0:
return 0.0001
return days / 365.0
except Exception as e:
print("日期计算错误:", e)
return 0
def handlebar(ContextInfo):
"""
K线/行情驱动函数
"""
# 1. 获取当前持仓信息
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
option_pos = 0 # 期权持仓数量 (张)
underlying_pos = 0 # 标的持仓数量 (股)
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.option_code:
option_pos = pos.m_nVolume
# 如果是卖方持仓,数量为负 (QMT部分接口返回可能是正数配合方向,这里简化处理,假设净持仓)
# 实际需根据 m_nDirection 判断,这里假设 m_nVolume 已经处理好或者是净持仓
# 如果是义务仓(卖方),通常需要乘以 -1,具体视QMT版本返回对象属性而定
# 这里假设 m_nVolume 为净持仓,多头为正
elif pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.underlying_code:
underlying_pos = pos.m_nVolume
# 如果没有期权持仓,则不需要对冲 (或者策略逻辑是先开期权)
if option_pos == 0:
# print("无期权持仓,无需对冲")
return
# 2. 获取期权详细信息 (行权价、到期日、类型、合约乘数)
opt_detail = ContextInfo.get_option_detail_data(ContextInfo.option_code)
if not opt_detail:
print("获取期权详情失败")
return
K = opt_detail['OptExercisePrice'] # 行权价
expire_date = opt_detail['ExpireDate'] # 到期日 YYYYMMDD
opt_type = opt_detail['optType'] # 'CALL' or 'PUT'
multiplier = opt_detail['VolumeMultiple'] # 合约乘数 (通常ETF期权为10000)
# 3. 获取市场数据
# 获取标的最新价 S
last_tick = ContextInfo.get_full_tick([ContextInfo.underlying_code])
if not last_tick:
return
S = last_tick[ContextInfo.underlying_code]['lastPrice']
# 获取期权隐含波动率 Sigma
# QMT 提供了直接获取 IV 的接口,如果获取不到可以使用历史波动率替代
iv = ContextInfo.get_option_iv(ContextInfo.option_code)
if iv <= 0.0001:
# 如果获取失败,使用默认值或历史波动率,这里暂用 20% 代替演示
sigma = 0.2
else:
sigma = iv
# 计算剩余期限 T
T = get_days_to_expiration(expire_date)
# 4. 计算 Delta
unit_delta = calculate_bsm_delta(S, K, T, ContextInfo.risk_free_rate, sigma, opt_type)
# 5. 计算对冲所需的标的头寸
# 投资组合总 Delta = (期权持仓 * 合约乘数 * 单个Delta) + (标的持仓 * 1)
# 目标是 总 Delta = 0
# 所以:目标标的持仓 = -1 * (期权持仓 * 合约乘数 * 单个Delta)
total_option_delta = option_pos * multiplier * unit_delta
target_underlying_pos = -1 * total_option_delta
# 6. 计算偏差并交易
diff = target_underlying_pos - underlying_pos
# 输出状态日志
print(f"时间:{ContextInfo.get_bar_timetag(ContextInfo.barpos)} | 标的价:{S:.3f} | IV:{sigma:.3f} | Delta:{unit_delta:.4f}")
print(f"期权持仓:{option_pos} | 当前标的:{underlying_pos} | 目标标的:{int(target_underlying_pos)} | 需调整:{int(diff)}")
# 检查是否超过阈值
if abs(diff) > ContextInfo.hedge_threshold:
trade_vol = int(abs(diff))
# 调整为100的倍数 (A股/ETF通常按手交易)
trade_vol = (trade_vol // 100) * 100
if trade_vol == 0:
return
if diff > 0:
# 需要买入标的
print(f"执行对冲:买入 {trade_vol} 股 {ContextInfo.underlying_code}")
passorder(23, 1101, ContextInfo.account_id, ContextInfo.underlying_code, 5, -1, trade_vol, ContextInfo)
else:
# 需要卖出标的
print(f"执行对冲:卖出 {trade_vol} 股 {ContextInfo.underlying_code}")
passorder(24, 1101, ContextInfo.account_id, ContextInfo.underlying_code, 5, -1, trade_vol, ContextInfo)
关键点解析
-
BSM 模型计算 Delta:
- 代码中
calculate_bsm_delta函数实现了标准的 Black-Scholes Delta 计算公式。 norm.cdf用于计算标准正态分布的累积分布函数。- Call Option Delta $\in (0, 1)$,Put Option Delta $\in (-1, 0)$。
- 代码中
-
数据获取:
get_option_detail_data:这是 QMT 特有的接口,非常方便地获取期权的静态数据(行权价、合约乘数、到期日等)。get_option_iv:直接获取交易所或券商计算好的隐含波动率,这比自己计算要准确且高效。get_full_tick:获取标的资产最新的实时价格。
-
对冲逻辑:
- 公式:$Target_Underlying = -(Option_Pos \times Multiplier \times Delta)$。
- 例如:持有 1 张认购期权(Call),Delta 为 0.6,合约乘数 10000。
- 期权端 Delta 敞口 = $1 \times 10000 \times 0.6 = 6000$ 股。
- 为了中性化,我们需要持有 -6000 股标的(即卖空 6000 股)。
- 注意:如果账户不能融券(卖空标的),则该策略只能用于对冲义务仓(卖出期权,Delta为负,需要买入标的)或者在已有标的持仓基础上进行减持。
-
交易执行:
- 使用了
passorder函数。 opType23 代表买入,24 代表卖出。prType5 代表最新价下单。- 加入了
hedge_threshold(阈值)控制,防止因为 Delta 的微小跳动导致频繁交易,消耗手续费。
- 使用了
使用前注意事项
- 账号设置:请务必将
ContextInfo.account_id替换为您真实的资金账号。 - 合约代码:期权合约代码会随时间过期,请替换为当前存续的主力合约代码。
- Python 库:确保 QMT 的 Python 环境中安装了
scipy库(QMT 内置 Python 通常已包含)。 - 实盘风险:Delta 是动态变化的(Gamma 风险),且 IV 也会变化。此策略仅为 Delta 中性,未考虑 Gamma 和 Vega 风险,实盘请务必先在模拟盘测试。
Q&A
Q: 如何获取期权的隐含波动率 (IV)?
A: 在 QMT 中,可以使用 ContextInfo.get_option_iv(option_code) 接口直接获取。如果返回值为 0 或异常,建议自行计算或使用历史波动率作为替代。
Q: 为什么需要设置对冲阈值 (hedge_threshold)?
A: 市场价格是时刻波动的,Delta 值也会随之微小跳动。如果不设置阈值,策略可能会在每一根 K 线都进行微量的买卖(例如买 100 股,下一分钟卖 100 股),这会导致巨大的交易成本(手续费和滑点)。
Q: 这个策略支持 Put 期权吗?
A: 支持。代码中的 calculate_bsm_delta 函数已经根据 option_type ('CALL' 或 'PUT') 进行了区分处理。Put 的 Delta 为负数,计算出的目标标的持仓将为正数(买入标的来对冲)。