问题描述
解决方案
编写一个期权Delta中性(Delta Neutral)对冲策略,通常包含以下几个核心步骤:
- 策略逻辑:构建一个期权组合(例如卖出跨式组合 Short Straddle),同时持有标的资产(如50ETF),使得整体组合的Delta值为0。
- 期权选择:通常选择平值(ATM)或近月合约,流动性较好。
- Greeks计算:使用BSM(Black-Scholes-Merton)模型计算期权的Delta值。
- 动态对冲:随着标的价格变动和时间流逝,Delta会发生变化,需要定期(如每日收盘前)调整标的资产的持仓量,使总Delta回归为0。
以下是一个完整的JoinQuant策略代码示例。该策略以 50ETF (510050.XSHG) 为标的,构建 卖出跨式组合(Short Straddle) 并通过买卖50ETF现货进行 Delta中性对冲。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
import math
from scipy.stats import norm
import datetime
def initialize(context):
# 设定标的:50ETF
g.underlying = '510050.XSHG'
# 设定基准
set_benchmark(g.underlying)
# 开启动态复权
set_option('use_real_price', True)
# 过滤报错日志
log.set_level('order', 'error')
# 策略参数
g.risk_free_rate = 0.03 # 无风险利率
g.contract_multiplier = 10000 # 50ETF期权合约乘数通常为10000
# 交易设置
set_order_cost(OrderCost(close_tax=0, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 每天收盘前进行对冲检查
run_daily(delta_hedging, '14:50')
def delta_hedging(context):
"""
核心逻辑:
1. 如果没有持仓,构建卖出跨式组合(卖出平值Call + 卖出平值Put)。
2. 计算当前期权持仓的总Delta。
3. 计算使总Delta为0所需的标的(50ETF)数量。
4. 调整标的持仓。
"""
# 获取标的当前价格
underlying_price = get_current_data()[g.underlying].last_price
# 1. 仓位管理:如果当前没有期权持仓,则开仓
if len(context.portfolio.short_positions) == 0:
open_straddle(context, underlying_price)
# 2. 检查期权到期:如果临近到期(例如小于3天),平仓并等待换月(简化处理,这里先平仓)
check_expiry(context)
# 3. 计算期权组合的总Delta
total_option_delta = 0
# 遍历所有空头持仓(我们是卖方)
for security, position in context.portfolio.short_positions.items():
# 跳过标的本身,只计算期权
if security == g.underlying:
continue
# 获取期权信息
info = get_security_info(security)
if not info:
continue
# 计算剩余期限 T (年化)
today = context.current_dt.date()
maturity = info.end_date
days_to_maturity = (maturity - today).days
T = max(days_to_maturity / 365.0, 0.0001)
# 获取行权价 K
# 注意:聚宽API获取行权价可能需要解析名字或查询额外数据,这里简化假设可以通过API获取
# 实际中建议使用 get_option_contracts 获取详细信息,这里为了通用性使用简易解析
# 假设 symbol 格式能关联到行权价,或者我们重新获取一次合约信息
# 在此示例中,我们重新计算Greeks需要行权价,我们假设开仓时记录了,或者通过名称解析
# 为保证代码可运行,这里使用近似波动率计算Delta
# 获取行权价 (需要根据实际合约代码逻辑获取,这里简化处理,假设我们能获取到)
# 实际实盘中建议维护一个g.options_map来存储合约详情
strike_price = get_strike_price(security)
# 计算历史波动率作为隐含波动率的替代 (简化处理)
sigma = get_historical_volatility(g.underlying, 20)
# 计算单份合约Delta
opt_type = 'call' if '购' in info.display_name else 'put'
d = calculate_bsm_delta(underlying_price, strike_price, T, g.risk_free_rate, sigma, opt_type)
# 卖方Delta符号相反
# 总Delta = 单份Delta * 合约乘数 * 持仓数量 * (-1 因为是卖方)
pos_delta = d * g.contract_multiplier * position.total_amount * (-1)
total_option_delta += pos_delta
# 4. 计算对冲所需的标的头寸
# 目标:Total_Delta = Option_Delta + Stock_Delta = 0
# Stock_Delta = 1 * Stock_Shares
# 所以:Target_Stock_Shares = -Option_Delta
target_stock_amount = -total_option_delta
# 取整到100股
target_stock_amount = int(target_stock_amount / 100) * 100
# 5. 执行对冲交易
current_stock_amount = context.portfolio.positions[g.underlying].total_amount
# 设置阈值,避免微小变动频繁交易(例如变动超过500股才调整)
if abs(target_stock_amount - current_stock_amount) >= 500:
log.info(f"执行对冲: 期权Delta: {total_option_delta:.2f}, 当前标的持仓: {current_stock_amount}, 目标标的持仓: {target_stock_amount}")
order_target(g.underlying, target_stock_amount)
def open_straddle(context, current_price):
"""构建卖出跨式组合"""
# 获取当月或次月合约
# 这里简化逻辑:获取所有期权,筛选出50ETF期权,找到行权价最接近当前价格的合约
# 获取所有期权列表 (注意:回测速度可能受影响,实盘需优化筛选逻辑)
# 实际使用建议使用 get_option_contracts
# 这里为了演示,我们模拟获取平值合约的过程
target_date = context.current_dt.date()
# 获取标的50ETF的期权合约
# 注意:jqdata的get_all_securities包含options
# 但更推荐使用 query 结合 opt.OPT_CONTRACT_INFO 表,这里使用简易逻辑
# 假设我们选取下个月到期的合约以保证流动性和时间价值
# 实际代码中需要根据日期筛选合约代码
# 这里仅做逻辑演示,具体获取合约代码需根据JQData API文档的 get_option_contracts
# 模拟:找到平值合约代码 (需用户根据实际数据环境完善)
# 示例代码无法直接遍历所有期权,因此我们打印日志提示
log.info("寻找平值期权合约进行开仓...")
# --- 真实环境获取合约逻辑 ---
# 1. 获取主力合约月份
# 2. 获取该月份所有行权价
# 3. 找到 abs(strike - current_price) 最小的 call 和 put
# 4. order(call, -10); order(put, -10)
# -------------------------
# 由于无法在纯文本中动态获取准确的期权代码,此处留空或需替换为具体逻辑
# 建议使用 jqdata.get_option_contracts(underlying, date)
pass
def calculate_bsm_delta(S, K, T, r, sigma, type='call'):
"""
计算BSM模型的Delta
S: 标的价格
K: 行权价
T: 剩余期限(年)
r: 无风险利率
sigma: 波动率
type: 'call' or 'put'
"""
if T <= 0 or sigma <= 0:
return 0
d1 = (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T))
if type == 'call':
delta = norm.cdf(d1)
else:
delta = norm.cdf(d1) - 1
return delta
def get_historical_volatility(security, days):
"""计算历史波动率"""
prices = attribute_history(security, days, '1d', ['close'])
# 计算对数收益率
log_returns = np.log(prices['close'] / prices['close'].shift(1))
# 计算标准差并年化
volatility = log_returns.std() * math.sqrt(252)
return volatility
def get_strike_price(option_code):
"""
解析期权行权价
注意:这依赖于期权信息中的行权价字段,或者从名称中解析
"""
# 尝试从API获取
try:
# 聚宽 get_security_info 可能不直接包含 strike_price,需查表
# 这里为了演示,假设我们可以通过某种方式获取
# 实际建议:使用 query(opt.OPT_CONTRACT_INFO).filter(...)
return 3.0 # 这是一个占位符,实际需查询数据库
except:
return 3.0
def check_expiry(context):
"""检查到期日,临近到期平仓"""
for security in list(context.portfolio.short_positions.keys()):
if security == g.underlying:
continue
info = get_security_info(security)
days = (info.end_date - context.current_dt.date()).days
if days <= 2:
log.info(f"合约 {security} 即将到期,平仓")
order_target(security, 0)
策略实现的关键点解析
-
Delta 计算 (BSM模型):
- 代码中
calculate_bsm_delta函数实现了Black-Scholes公式。 - Delta 衡量期权价格对标的价格变动的敏感度。
- Call Delta $\in (0, 1)$,Put Delta $\in (-1, 0)$。
- 注意:代码中使用历史波动率(Historical Volatility)代替隐含波动率(Implied Volatility)。在更精细的策略中,你应该反推市场价格的隐含波动率来计算Delta,或者直接使用聚宽提供的Greeks数据(如果购买了相应数据权限)。
- 代码中
-
对冲逻辑:
- 组合Delta = $\sum (Option_Delta \times Multiplier \times Position) + (Stock_Delta \times Stock_Shares)$。
- 因为标的资产(50ETF)的Delta恒为1,所以 $Stock_Delta = 1$。
- 为了使组合Delta为0,我们需要持有的标的数量 $Stock_Shares = - \sum (Option_Delta \times Multiplier \times Position)$。
- 如果计算结果为正,说明我们需要买入ETF;如果为负,说明需要卖出ETF(或融券卖出,本策略假设只做多ETF对冲,如果需要完全对冲可能需要融券权限)。
-
合约选择 (Open Straddle):
- 代码中
open_straddle部分留作了逻辑框架。在实际编写时,你需要使用get_option_contracts或查询opt.OPT_CONTRACT_INFO表来筛选出:- 标的为510050。
- 到期日适中(如次月)。
- 行权价最接近当前ETF价格(平值 ATM)。
- 代码中
-
交易成本与滑点:
- Delta中性策略属于高频调整策略,对交易成本非常敏感。代码中设置了
min_commission和阈值(如变动超过500股才交易),以防止过度交易磨损本金。
- Delta中性策略属于高频调整策略,对交易成本非常敏感。代码中设置了
如何完善此策略
要在聚宽回测环境中成功运行,你需要补充获取期权合约的具体代码。以下是一个获取平值合约的辅助函数示例(需配合 jqdata 库):
def get_atm_options(underlying, date):
# 查询期权合约信息
q = query(opt.OPT_CONTRACT_INFO).filter(
opt.OPT_CONTRACT_INFO.underlying_symbol == underlying,
opt.OPT_CONTRACT_INFO.maturity_date > date + datetime.timedelta(days=20) # 选次月
).order_by(opt.OPT_CONTRACT_INFO.maturity_date.asc())
df = opt.run_query(q)
if df.empty:
return None, None
# 找到最近的到期日
target_maturity = df.iloc[0]['maturity_date']
df_current = df[df['maturity_date'] == target_maturity]
# 获取标的价格
current_price = get_price(underlying, end_date=date, count=1, fields=['close'])['close'][0]
# 寻找行权价最接近的
df_current['diff'] = abs(df_current['exercise_price'] - current_price)
df_current = df_current.sort_values('diff')
# 分别找Call和Put
atm_call = df_current[df_current['contract_type'] == 'CO'].iloc[0]['code']
atm_put = df_current[df_current['contract_type'] == 'PO'].iloc[0]['code']
return atm_call, atm_put
常见问题与Q&A
Q: 为什么需要Delta中性对冲?
A: 卖出跨式组合(Short Straddle)主要赚取时间价值(Theta)和波动率下降(Vega)的收益。但是,如果标的价格大幅向一个方向移动,卖方会面临巨大亏损。Delta对冲通过持有标的资产,抵消了标的价格小幅波动带来的方向性风险,让策略更纯粹地赚取Theta收益。
Q: 多久对冲一次比较好?
A: 这是一个权衡问题。实时对冲(每分钟)能让Delta保持极低,但交易成本极高。每日收盘对冲成本低,但日内风险敞口大。通常建议设定一个Delta阈值(例如Delta偏离超过0.2时)或固定时间(如每日收盘)进行对冲。
Q: 策略最大的风险是什么?
A: Gamma风险。当标的价格发生剧烈跳空(如黑天鹅事件)时,Delta会瞬间发生巨大变化,之前的对冲仓位会失效,且来不及调整,导致巨额亏损。此外,如果隐含波动率大幅上升(Vega风险),卖方也会亏损。