商品期权隐含波动率策略
登录后收藏学术论文
Commodity Option Implied Volatilities and the Expected Futures Returns
Commodity Option Implied Volatilities and the Expected Futures Returns [点击查看论文]
- LULuxembourg School of Business
- LUUniversity of Luxembourg
- ?Luxembourg School of Finance
- ?Universite du Luxembourg
策略概要
投资范围包括25种商品,根据30天隐含波动率(通过过去12个月的平均波动率去除趋势)分为四组。“低”组包含波动率最低的25%的商品,而“高”组包含波动率最高的25%的商品。该策略为多空策略,买入“低”组中的商品,卖出“高”组中的商品。投资组合等权重,每月重新平衡。
II. 策略合理性
VOL策略的回报主要由现货回报的可预测性驱动。波动率保险昂贵的商品价格往往下跌,而保险便宜的商品价格往往上涨。这种可预测性与套利限制有关。当对冲成本上升时,资本受限的对冲者会减少库存,从而造成卖压。相反,当波动率较低且市场条件稳定时,对冲成本较低,允许对冲者对冲更多产量。这种机制有助于商品市场的价格动态,从而形成VOL策略中观察到的模式。
回测表现
波动率18.48%
夏普比率0.69
索提诺比率0.342
胜率50%
完整 Python 代码
from AlgorithmImports import *
#endregion
# https://quantpedia.com/strategies/commodity-option-implied-volatility-strategy/
#
# The investment universe consists of 25 commodities.
# Commodities are sorted into four groups based on the 30-days implied volatility de-trended by the previous 12 months mean of implied volatility (see page 8 for exact formula).
# The “Low” (“High”) group contains the top 25% of all commodities with the lowest (highest) volatilities.
# The portfolio is long-short and buys commodities from the group “Low” and sells commodities from the group “High”.
# The portfolio is equally-weighted and is rebalanced on a monthly basis.
#
# QC Implementation:
import numpy as np
class CommodityOptionImpliedVolatilityStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.min_expiry = 25
self.max_expiry = 35
self.period = 12 # need n of implied volatilities
self.iv = {} # storing implied volatilies in RollingWindow
self.contracts = {} # storing option contracts
self.tickers_symbols = {} # storing commodities symbols under their tickers
self.tickers = ['GLD', 'USO', 'UNG', 'SLV', 'DBA', 'DBB', 'PPLT', 'PALL']
self.next_expiry = None
for ticker in self.tickers:
# subscribe to commodity
security = self.AddEquity(ticker, Resolution.Minute)
# change normalization to raw to allow adding contracts
security.SetDataNormalizationMode(DataNormalizationMode.Raw)
# set fee model and leverage
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
# get commodity symbol
symbol = security.Symbol
# store symbol under ticker
self.tickers_symbols[ticker] = symbol
# create RollingWindow for implied volatilities
self.iv[symbol] = RollingWindow[float](self.period)
self.day = -1
def OnData(self, data):
# rebalance daily
if self.day == self.Time.day:
return
self.day = self.Time.day
if self.next_expiry and self.Time.date() >= self.next_expiry.date():
self.Liquidate()
for symbol in self.tickers_symbols:
if symbol in self.contracts:
# remove expired contracts
for contract in self.contracts[symbol]:
self.RemoveSecurity(contract)
# remove contracts from dictionary
del self.contracts[symbol]
if not self.Portfolio.Invested:
for symbol in self.tickers_symbols:
if symbol not in self.contracts:
# get all contracts for current commodity
contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for commodity
underlying_price = self.Securities[symbol].Price
# get strikes from commodity contracts
strikes = [i.ID.StrikePrice for i in contracts]
if len(strikes) > 0:
# get at the money strike
atm_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))
atm_calls:list = [i for i in contracts if i.ID.OptionRight == OptionRight.Call and
i.ID.StrikePrice == atm_strike and
self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
atm_puts:list = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and
i.ID.StrikePrice == atm_strike and
self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
if len(atm_calls) and len(atm_puts):
# sort by expiry
atm_call = sorted(atm_calls, key = lambda x: x.ID.Date)[0]
atm_put = sorted(atm_puts, key = lambda x: x.ID.Date)[0]
self.next_expiry = min(atm_call.ID.Date, atm_put.ID.Date)
# add contracts
option = self.AddOptionContract(atm_call, Resolution.Minute)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)
option = self.AddOptionContract(atm_put, Resolution.Minute)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)
# store atm contracts by symbol
self.contracts[symbol] = [atm_call, atm_put]
iv_detrend = {} # storing detrend implied volatility for options
if data.OptionChains.Count != 0:
for kvp in data.OptionChains:
chain = kvp.Value
contracts = [x for x in chain]
# check if there are enough contracts for option
if len(contracts) < 2:
continue
atm_call_iv = None
atm_put_iv = None
# get ticker
ticker = chain.Underlying.Symbol.Value
# go through option contracts
for c in contracts:
if c.Right == OptionRight.Call:
# found atm call
atm_call_iv = c.ImpliedVolatility
else:
# found put option
atm_put_iv = c.ImpliedVolatility
if atm_call_iv and atm_put_iv:
# make mean from atm call implied volatility and atm put implied volatility
iv = (atm_call_iv + atm_put_iv) / 2
# get symbol based on ticker from option contract
commodity_symbol = self.tickers_symbols[ticker]
# check if there are enough data of mean implied volatilities
if self.iv[commodity_symbol].IsReady:
# calculate mean of previous mean implied volatilities
vol_mean = np.mean([x for x in self.iv[commodity_symbol]])
# calculate detrend implied volatility and store it by symbol
iv_detrend[commodity_symbol] = iv - vol_mean
# add current mean of implied volatility
self.iv[commodity_symbol].Add(iv)
# can't perform quintile selection
if len(iv_detrend) < 4:
self.Liquidate()
return
quintile = int(len(iv_detrend) / 4)
sorted_by_iv_detrend = [x[0] for x in sorted(iv_detrend.items(), key=lambda item: item[1])]
# go long smallest quintile
long = sorted_by_iv_detrend[:quintile]
# go short largest quintile
short = sorted_by_iv_detrend[-quintile:]
# trade execution
long_length = len(long)
short_length = len(short)
for symbol in long:
self.SetHoldings(symbol, 1 / long_length)
for symbol in short:
self.SetHoldings(symbol, -1 / short_length)
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))