问题描述
解决方案
这是一个关于 Gamma Scalping(伽马剥头皮) 策略的实现请求。
策略逻辑分析
Gamma Scalping 是一种利用期权 Gamma 特性的波动率交易策略。其核心思想是:
- 建立底仓:通常买入跨式组合(Long Straddle,同时买入 ATM Call 和 ATM Put),构建一个 Delta 中性但 Gamma 为正的投资组合。
- 动态对冲:
- 当标的价格上涨时,Call 的 Delta 增加(变得更正),Put 的 Delta 增加(变得不那么负),总 Delta 变为正数。为了保持中性,需要卖出标的资产。
- 当标的价格下跌时,Call 的 Delta 减少,Put 的 Delta 减少(变得更负),总 Delta 变为负数。为了保持中性,需要买入标的资产。
- 获利来源:通过“高抛低吸”标的资产来覆盖期权的时间价值损耗(Theta),并赚取标的实际波动率大于隐含波动率时的差价。
QMT 实现关键点
- 期权希腊字母计算:QMT 提供了
bsm_iv(隐含波动率) 和bsm_price(理论价),但没有直接提供实时 Delta 的接口。我们需要基于 BSM 公式手写一个 Delta 计算函数。 - 合约筛选:需要自动获取当前标的(如 510050.SH)的期权链,并筛选出平值(ATM)合约。
- 对冲阈值:为了避免频繁交易导致手续费过高,需要设置一个 Delta 偏离阈值(Threshold),只有当 Net Delta 超过该值时才进行对冲。
策略代码实现
# -*- coding: gbk -*-
import numpy as np
from scipy.stats import norm
import datetime
import math
'''
Gamma Scalping 策略
标的:510050.SH (上证50ETF)
逻辑:
1. 初始买入 ATM 跨式组合 (Long Straddle)
2. 实时计算组合 Delta
3. 当 Delta 偏离超过阈值时,交易标的 ETF 进行对冲,使其回归 Delta Neutral
'''
def init(ContextInfo):
# --- 策略参数设置 ---
ContextInfo.underlying = '510050.SH' # 标的资产
ContextInfo.account_id = '6000000000' # 请替换为您的资金账号
ContextInfo.account_type = 'STOCK' # 账号类型
ContextInfo.hedge_threshold = 0.1 # Delta 对冲阈值 (绝对值)
ContextInfo.option_qty = 10 # 期权手数
ContextInfo.risk_free_rate = 0.03 # 无风险利率
# 交易相关设置
ContextInfo.set_account(ContextInfo.account_id)
ContextInfo.set_universe([ContextInfo.underlying])
# 全局变量
ContextInfo.call_contract = None
ContextInfo.put_contract = None
ContextInfo.multiplier = 10000 # 50ETF期权合约乘数通常为10000
print("Gamma Scalping 策略初始化完成")
def handlebar(ContextInfo):
# 获取当前标的价格
if ContextInfo.is_last_bar():
# 获取标的最新价
tick_data = ContextInfo.get_full_tick([ContextInfo.underlying])
if not tick_data:
return
S = tick_data[ContextInfo.underlying]['lastPrice']
# 1. 如果没有持仓,构建初始跨式组合 (Long Straddle)
if ContextInfo.call_contract is None or ContextInfo.put_contract is None:
open_straddle(ContextInfo, S)
return
# 2. 计算当前组合的 Net Delta
net_delta = calculate_portfolio_delta(ContextInfo, S)
# 3. 检查是否需要对冲
check_and_hedge(ContextInfo, net_delta, S)
def open_straddle(ContextInfo, current_price):
"""
构建跨式组合:寻找最近月、最平值的 Call 和 Put
"""
# 获取期权列表 (这里简化逻辑,获取当月合约,实际需根据日期判断)
# 获取当前日期
current_date = datetime.datetime.now().strftime('%Y%m%d')
# 获取标的对应的期权列表 (Call 和 Put)
# 注意:get_option_list 第三个参数为空表示获取所有类型,第四个参数True表示仅获取可交易的
options = ContextInfo.get_option_list(ContextInfo.underlying, current_date, "", True)
if not options:
print("未找到可用期权合约")
return
# 筛选出 Call 和 Put
calls = [opt for opt in options if opt.endswith('C.SHO') or '购' in ContextInfo.get_instrumentdetail(opt)['InstrumentName']]
puts = [opt for opt in options if opt.endswith('P.SHO') or '沽' in ContextInfo.get_instrumentdetail(opt)['InstrumentName']]
# 寻找行权价最接近当前价格的合约 (ATM)
atm_call = min(calls, key=lambda x: abs(ContextInfo.get_instrumentdetail(x)['OptExercisePrice'] - current_price))
# 对应的 Put 通常行权价相同
target_strike = ContextInfo.get_instrumentdetail(atm_call)['OptExercisePrice']
atm_put = min(puts, key=lambda x: abs(ContextInfo.get_instrumentdetail(x)['OptExercisePrice'] - target_strike))
print(f"构建跨式组合: Call={atm_call}, Put={atm_put}, 行权价={target_strike}, 标的价={current_price}")
# 下单买入
# 50: 买入开仓
passorder(50, 1101, ContextInfo.account_id, atm_call, 5, -1, ContextInfo.option_qty, ContextInfo)
passorder(50, 1101, ContextInfo.account_id, atm_put, 5, -1, ContextInfo.option_qty, ContextInfo)
ContextInfo.call_contract = atm_call
ContextInfo.put_contract = atm_put
def calculate_bsm_delta(S, K, T, r, sigma, option_type):
"""
计算 BSM Delta
S: 标的价格
K: 行权价
T: 剩余期限 (年)
r: 无风险利率
sigma: 隐含波动率
option_type: 'C' or 'P'
"""
if T <= 0 or sigma <= 0:
return 0
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
if option_type == 'C':
return norm.cdf(d1)
else:
return norm.cdf(d1) - 1
def calculate_portfolio_delta(ContextInfo, S):
"""
计算组合总 Delta (包括期权和标的持仓)
"""
# 1. 获取期权详细信息
call_detail = ContextInfo.get_instrumentdetail(ContextInfo.call_contract)
put_detail = ContextInfo.get_instrumentdetail(ContextInfo.put_contract)
K_call = call_detail['OptExercisePrice']
K_put = put_detail['OptExercisePrice']
# 计算剩余时间 T (年化)
expire_date_str = str(call_detail['ExpireDate'])
expire_date = datetime.datetime.strptime(expire_date_str, '%Y%m%d')
now = datetime.datetime.now()
days_to_expire = (expire_date - now).days
T = max(days_to_expire / 365.0, 0.0001)
# 2. 获取期权市场价格用于计算 IV
call_tick = ContextInfo.get_full_tick([ContextInfo.call_contract])
put_tick = ContextInfo.get_full_tick([ContextInfo.put_contract])
if not call_tick or not put_tick:
return 0
call_price = call_tick[ContextInfo.call_contract]['lastPrice']
put_price = put_tick[ContextInfo.put_contract]['lastPrice']
# 3. 计算隐含波动率 (IV)
# bsm_iv(optionType, objectPrices, strikePrice, optionPrice, riskFree, days, dividend)
# 注意:QMT的bsm_iv days参数通常指天数
iv_call = ContextInfo.bsm_iv('C', S, K_call, call_price, ContextInfo.risk_free_rate, days_to_expire, 0)
iv_put = ContextInfo.bsm_iv('P', S, K_put, put_price, ContextInfo.risk_free_rate, days_to_expire, 0)
# 4. 计算单张合约 Delta
delta_call_unit = calculate_bsm_delta(S, K_call, T, ContextInfo.risk_free_rate, iv_call, 'C')
delta_put_unit = calculate_bsm_delta(S, K_put, T, ContextInfo.risk_free_rate, iv_put, 'P')
# 5. 获取当前持仓数量
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
pos_call = 0
pos_put = 0
pos_stock = 0
for pos in positions:
if pos.m_strInstrumentID == ContextInfo.call_contract:
pos_call = pos.m_nVolume
elif pos.m_strInstrumentID == ContextInfo.put_contract:
pos_put = pos.m_nVolume
elif pos.m_strInstrumentID == ContextInfo.underlying:
pos_stock = pos.m_nVolume # 股票/ETF持仓 Delta 为 1
# 6. 计算总 Delta
# 总 Delta = (Call Delta * Call 数量 * 乘数) + (Put Delta * Put 数量 * 乘数) + (标的 Delta * 标的数量)
# 注意:Put Delta 为负数
total_delta = (delta_call_unit * pos_call * ContextInfo.multiplier) + \
(delta_put_unit * pos_put * ContextInfo.multiplier) + \
(1.0 * pos_stock)
print(f"Delta监控: Call_IV={iv_call:.2f}, Put_IV={iv_put:.2f}, UnitDelta(C)={delta_call_unit:.2f}, UnitDelta(P)={delta_put_unit:.2f}, StockPos={pos_stock}, TotalDelta={total_delta:.2f}")
return total_delta
def check_and_hedge(ContextInfo, net_delta, current_price):
"""
检查并执行对冲
"""
threshold = ContextInfo.hedge_threshold * ContextInfo.option_qty * ContextInfo.multiplier
# 如果 Net Delta 绝对值超过阈值 (例如 0.1 * 10 * 10000 = 10000 Delta,即相当于10000股ETF)
# 这里阈值设置需要根据实际资金量和风险偏好调整,示例中 threshold 设为 0.1 股是不合理的,
# 通常阈值设为几百或几千 Delta (对应几百或几千股标的)
# 修正阈值逻辑:假设我们容忍 0.2 的 Delta 偏离 (相对于1手期权)
# 实际容忍 Delta = 0.2 * 持仓手数 * 10000
real_threshold = 0.2 * ContextInfo.option_qty * ContextInfo.multiplier
print(f"当前 Net Delta: {net_delta:.2f}, 对冲触发阈值: +/-{real_threshold:.2f}")
if abs(net_delta) > real_threshold:
# 计算需要交易的标的数量
# 如果 Delta 为正 (Too Long),需要卖出标的 -> 数量为负
# 如果 Delta 为负 (Too Short),需要买入标的 -> 数量为正
hedge_qty = -1 * net_delta
# 调整为 100 的整数倍 (A股/ETF 手数限制)
hedge_qty = int(hedge_qty / 100) * 100
if hedge_qty == 0:
return
print(f"执行对冲交易: 目标数量 {hedge_qty} 股")
if hedge_qty > 0:
# 买入标的 (23: 买入)
passorder(23, 1101, ContextInfo.account_id, ContextInfo.underlying, 5, -1, abs(hedge_qty), ContextInfo)
else:
# 卖出标的 (24: 卖出)
# 注意:如果是ETF,且没有持仓,可能需要融券卖出(28),这里假设是持有现货进行对冲或已有底仓
# 简单的 Gamma Scalping 通常在 Delta 变负时买入,变正时卖出刚才买入的
passorder(24, 1101, ContextInfo.account_id, ContextInfo.underlying, 5, -1, abs(hedge_qty), ContextInfo)
代码核心功能解析
-
init初始化:- 设置了标的为
510050.SH。 - 定义了
hedge_threshold(对冲阈值),这是策略的关键参数,决定了对冲的频率。阈值越小,对冲越频繁,Gamma 捕获越精确,但手续费越高。
- 设置了标的为
-
open_straddle建仓:- 使用
get_option_list获取期权链。 - 通过比较
OptExercisePrice和当前标的价格,自动筛选出最接近平值(ATM)的 Call 和 Put。 - 使用
passorder发送买入开仓指令。
- 使用
-
calculate_bsm_delta希腊字母计算:- 这是 QMT API 中缺失的部分。我们引入
scipy.stats.norm来计算标准正态分布的累积分布函数,实现了 Black-Scholes 模型中的 Delta 公式。 - $Delta_{Call} = N(d_1)$
- $Delta_{Put} = N(d_1) - 1$
- 这是 QMT API 中缺失的部分。我们引入
-
calculate_portfolio_delta组合 Delta 计算:- 关键步骤:利用
ContextInfo.bsm_iv反推实时的隐含波动率。因为 Delta 对波动率敏感,使用实时 IV 比使用历史波动率更准确。 - 计算公式:$Total Delta = \sum (Option Delta \times Position \times Multiplier) + (Stock Position \times 1)$。
- 注意期权乘数(通常是 10000),这意味着 1 手期权的 Delta 变动相当于 10000 股标的。
- 关键步骤:利用
-
check_and_hedge对冲逻辑:- Delta > 0:代表组合偏多头(通常是因为标的大涨,Call Delta 逼近 1,Put Delta 逼近 0)。此时需要卖出标的来减少 Delta。
- Delta < 0:代表组合偏空头(通常是因为标的大跌,Put Delta 逼近 -1)。此时需要买入标的来增加 Delta。
- 这种“跌买涨卖”的行为正是 Gamma Scalping 获利的本质。
常见问题与注意事项 (Q&A)
Q: 为什么代码中要计算 IV 而不是直接用固定波动率?
A: Delta 的值会随着波动率的变化而变化。使用 ContextInfo.bsm_iv 利用当前期权市场价格反推 IV,能得到更符合当前市场预期的 Delta 值,使对冲更精准。
Q: 如何处理卖出对冲时的券源问题?
A: 在 A 股市场,如果你没有标的底仓,当 Delta 变为正数需要卖出标的时,你必须进行融券卖出(OpType 28)。上述代码使用的是普通卖出(OpType 24),这假设你已经持有标的或者是在平掉之前买入的对冲仓位。如果是纯粹的 Gamma Scalping,建议使用信用账户(Margin Account)。
Q: 什么时候平仓?
A: 该策略代码主要展示了动态对冲逻辑。实际应用中,你需要在 handlebar 中加入止盈止损逻辑,或者在期权临近到期日(Gamma 风险剧增)时平仓。
Q: 为什么需要 scipy 库?
A: QMT 的 Python 环境自带了 numpy 和 scipy。计算正态分布 CDF ($N(x)$) 是计算 BSM Delta 的必要步骤。
Q: 阈值 real_threshold 应该设为多少?
A: 这是一个权衡。如果设得太低(如 0),你会每一笔 Tick 都在交易,手续费会吞噬所有利润。如果设得太高,你的 Delta 敞口会很大,变成方向性投机。通常建议设置为期权总 Delta 的 10%-20% 左右的偏离度。