
“该策略每月卖出原油、黄金和天然气期货的一月期平值跨式期权(straddles),头寸按等权分配,并通过每日Delta对冲来保持主要敞口集中于市场波动率的变化。”
资产类别:期权 | 地域:全球 | 频率:每月 | 市场:大宗商品 | 关键词:波动率
I. 策略概述
该策略每月持续卖出原油、黄金和天然气期货的一月期平值跨式期权(straddles)。跨式期权由相同执行价和到期日的看跌期权与看涨期权组成。各头寸按等权分配,并通过每日Delta对冲(针对标的资产价格变动进行对冲)来保持对市场波动率变化的主要敞口。
II. 策略合理性
该策略通过卖出原油、黄金和天然气期货的跨式期权捕捉大宗商品中的波动率风险溢价。跨式期权每日进行Delta对冲,以确保主要敞口来自市场波动率变化。尽管方差互换(variance swaps)也能捕捉波动率风险溢价,并且自带Delta对冲功能,但该策略未涉及此类工具。
波动率风险溢价源于结构性失衡:对冲者和投机者对受限下行风险敞口的需求较高,而自然卖方的供应较少。这种失衡在危机期间尤为显著,因资本要求更加严格和杠杆容忍度降低。
III. 论文来源
The Volatility Risk Premium [点击浏览原文]
- G. Renninson, N. Pedersen
<摘要>
全球宏观经济不确定性上升及极端市场动荡频繁,促使投资者寻找传统资产类别以外的多元化投资机会。本文研究了旨在捕获时间内溢价的期权策略,以补偿市场波动率突然上升时可能出现的损失风险。研究表明,这些“波动率风险溢价”策略在1994年6月至2012年6月的14个期权市场中实现了可观的风险调整后收益,尤其在2008年危机后表现显著改善(见图1)。我们得出结论,波动率策略的风险回报权衡优于传统投资(如股票和债券),且与股票风险的相关性较低。因此,想要分散股票风险敞口的投资者应考虑将部分资产配置于波动率风险溢价策略。然而,成功实施需跨主要期权市场(如股票、利率、货币和大宗商品)实现多元化,并结合积极的风险管理和谨慎的规模控制。

IV. 回测表现
| 年化收益率 | 6.1% |
| 波动率 | 5.2% |
| Beta | 0.109 |
| 夏普比率 | 1.17 |
| 索提诺比率 | -0.299 |
| 最大回撤 | -8.5% |
| 胜率 | 45% |
V. 完整python代码
from AlgorithmImports import *
class VolatilityRiskPremiuminCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.min_expiry = 30
self.max_expiry = 45
self.symbols = []
self.contracts = {}
tickers = ['USO', 'GLD', 'UNG']
for ticker in tickers:
# equity data
data = self.AddEquity(ticker, Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
# change normalization to raw to allow adding etf contracts
data.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.symbols.append(data.Symbol)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.last_day:int = -1
self.trade_flag = False
def OnData(self, data):
# check once a day
if self.Time.day == self.last_day:
return
self.last_day = self.Time.day
# trade option contracts after selection and make sure, they are ready
if self.trade_flag and data.OptionChains.Count != 0:
# next rebalance will be after next selection
self.trade_flag = False
# sell atm straddle of subscribed contracts
for symbol, contract_obj in self.contracts.items():
# get call and put contract
call, put = contract_obj.contracts
# get underlying price
underlying_price = contract_obj.underlying_price
options_q = int((self.Portfolio.TotalPortfolioValue / len(self.symbols)) / (underlying_price * 100))
self.Sell(call, options_q)
self.Sell(put, options_q)
# delta hedge
self.MarketOrder(symbol, options_q*50)
# check contracs expiration
for symbol in self.symbols:
# close trade once option is
if symbol in self.contracts and self.contracts[symbol].expiry_date-timedelta(days=1) <= self.Time.date():
# liquidate expired contracts
for contract in self.contracts[symbol].contracts:
self.Liquidate(contract)
self.Liquidate(symbol)
# remove Contracts object for current symbol
del self.contracts[symbol]
# rebalance, when all option contracts expiried
if len(self.contracts) == 0:
self.Liquidate()
# subscribe to new option contracts
for symbol in self.symbols:
# get all contracts for current etf
contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for etf
underlying_price = self.Securities[symbol].Price
# get strikes from etf contracts
strikes = [i.ID.StrikePrice for i in contracts]
# can't filter contracts, if there isn't any strike price
if len(strikes) <= 0:
continue
# filter calls and puts contracts with one month expiry
calls, puts = self.FilterContracts(strikes, contracts, underlying_price)
# make sure, there is at least one call and put contract
if len(calls) and len(puts):
# sort by expiry
call = sorted(calls, key = lambda x: x.ID.Date)[0]
put = sorted(puts, key = lambda x: x.ID.Date)[0]
subscription = self.SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(call.Underlying)
# make sure put and call contract subscription is valid
if subscription:
# add call contract
self.AddContract(call)
# add put contract
self.AddContract(put)
# retrieve expiry date for contracts
expiry_date = call.ID.Date.date() if call.ID.Date.date() <= put.ID.Date.date() else put.ID.Date.date()
# store contracts with expiry date under etf symbol
self.contracts[symbol] = Contracts(expiry_date, underlying_price, [call, put])
# strategy sells subscribed contracts on next day
self.trade_flag = True
def FilterContracts(self, strikes, contracts, underlying_price):
''' filter call and put contracts from contracts parameter '''
''' return call and put contracts '''
# Straddle
call_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))
put_strike = call_strike
calls = [] # storing call contracts
puts = [] # storing put contracts
for contract in contracts:
# check if contract has one month expiry
if self.min_expiry < (contract.ID.Date - self.Time).days < self.max_expiry:
# check if contract is call
if contract.ID.OptionRight == OptionRight.Call and contract.ID.StrikePrice == call_strike:
calls.append(contract)
# check if contract is put
elif contract.ID.OptionRight == OptionRight.Put and contract.ID.StrikePrice == put_strike:
puts.append(contract)
# return filtered calls and puts with one month expiry
return calls, puts
def AddContract(self, contract):
''' subscribe option contract, set price mondel and normalization mode '''
option = self.AddOptionContract(contract, Resolution.Daily)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
class Contracts():
def __init__(self, expiry_date, underlying_price, contracts):
self.expiry_date = expiry_date
self.underlying_price = underlying_price
self.contracts = contracts
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))