波动率的波动效应在股票中的表现
登录后收藏学术论文
Baltussen
- ?Van Bekkum, Van Der Grient
这篇论文研究了预期股票回报的不确定性如何在股票横截面中定价。不确定性通过期权隐含波动率的波动性(vol-of-vol)来代理,较高的vol-of-vol表明投资者对预期股票回报的不确定性较大。我们发现,高vol-of-vol的股票在接下来的一个月内表现不如低vol-of-vol的股票,差距约为0.85%,即每年约为10%。这一负向vol-of-vol效应无法通过许多先前已记录的因子来解释,且持续超过18个月,在ADR样本中也得到了验证。统计测试未能确认vol-of-vol效应是由套利摩擦、乐观偏差、跳跃风险或随机波动风险所驱动的。此外,我们没有发现vol-of-vol是传统资产定价模型中的定价风险因子,也没有反映更高阶的风险。我们的结果似乎与代表性代理的理性不确定性定价不一致,并表明期权市场与股票市场之间存在强烈的信息联动。
https://quantpedia.com/www/Unknown_Unknowns_-_Vol-of-Vol_and_the_Cross_Section_of_Stock_Returns.pdf
策略概要
该策略专注于纽约证券交易所(NYSE)股票,排除封闭式基金、房地产投资信托(REITs)、便士股(股价低于5美元)以及市值低于2.25亿美元的股票。只使用市值最大、流动性最强的股票(最高市值分位)。波动率的波动率每月计算,使用过去20天的ATM看涨和看跌期权的平均隐含波动率,按隐含波动率的标准差进行缩放(按来源方法)。股票根据该比率被分为五个分位。该策略在最低分位上建立多头头寸,在最高分位上建立空头头寸,使用价值加权的投资组合,并每月进行再平衡。
策略合理性
源研究论文指出,经济理论为负波动率波动效应提供了多种解释,但没有一种能够完全解释这一现象。研究假设,股票期权市场包含的信息会稍后反映在股价中,并且信息会缓慢地渗透到各个市场,期权市场有助于股票市场的价格发现。
回测表现
完整 Python 代码
from AlgorithmImports import *
import numpy as np
from typing import List, Dict
#endregion
class VolatilityOfVolatilityEffectinStocks(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
# self.min_expiry = 30
# self.max_expiry = 60
self.tickers_to_ignore: List[str] = ['XOM', 'AAPL', 'AMZN']
self.period: int = 20 # need n of daily implied volatility values
self.leverage: int = 5
self.min_share_price: int = 5
self.quantile: int = 5
self.data: Dict[Symbol, SymbolData] = {} # storing daily IV
self.contracts: Dict[Symbol, Contract] = {} # storing option contracts
self.tickers_symbols: Dict[str, Symbol] = {} # storing symbols under their tickers
symbol: Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
self.day: int = -1
self.fundamental_count: int = 100
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Minute
self.settings.daily_precise_end_time = False
self.AddUniverse(self.FundamentalSelectionFunction)
self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
self.Schedule.On(self.DateRules.MonthStart(symbol), self.TimeRules.BeforeMarketClose(symbol), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# rebalance weekly
if not self.selection_flag:
return Universe.Unchanged
# select top n stocks by dollar volume with price higher than 5
selected: List[Fundamental] = sorted([
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 and x.Price > self.min_share_price and x.Symbol.Value not in self.tickers_to_ignore
], key=lambda x: x.DollarVolume, reverse=True)[:self.fundamental_count]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
selected_symbols: List[Symbol] = [] # storing symbols of selected stocks
selected_tickers: List[str] = [] # storing tickers of selected stocks
# add new stocks to dictionaries
for stock in selected:
symbol: Symbol = stock.Symbol
ticker: str = symbol.Value
market_cap: float = stock.MarketCap
# remove duplicate stocks from fine
if ticker in selected_tickers:
# check if symbol of duplicated ticker was stored in self.data
if symbol in self.data and symbol in self.contracts:
# remove stock's contracts
for contract in self.contracts[symbol].contracts:
self.RemoveSecurity(contract)
del self.data[symbol]
del self.contracts[symbol]
continue
# add stock symbol to list of selected stocks
selected_symbols.append(symbol)
# add stock ticker to list of selected tickers
selected_tickers.append(ticker)
# don't override data, if they are consecutive
if ticker in self.tickers_symbols and symbol in self.data and symbol in self.contracts:
# update market cap
self.data[symbol].update_market_cap(market_cap)
continue
# store stock market cap and create RollingWindwow for IV values
self.data[symbol] = SymbolData(self.period, market_cap)
# store symbol under stock ticker
self.tickers_symbols[ticker] = symbol
# create object from Contracts class for stock symbol
self.contracts[symbol] = Contract(self.Time.date(), [])
# make sure, data are consecutive
remove_tickers_symbols: List[Tuple[str, Symbol]] = [] # storing tuple (ticker, symbol)
for ticker, symbol in self.tickers_symbols.items():
# add stocks, which weren't selected to remove list
if symbol not in selected_symbols:
remove_tickers_symbols.append((ticker, symbol))
# remove not selected stocks from dictionaries
for ticker, symbol in remove_tickers_symbols:
if symbol in self.contracts:
# remove stock's contracts
for contract in self.contracts[symbol].contracts:
self.RemoveSecurity(contract)
del self.contracts[symbol]
if symbol in self.data:
# delete stock from dictionaries
del self.data[symbol]
del self.tickers_symbols[ticker]
# return symbols of selected stocks
return selected_symbols
def OnData(self, data: Slice) -> None:
# each day store implied volatility for selected stocks
if self.Time.hour == 9:
if data.OptionChains.Count != 0:
for kvp in data.OptionChains:
chain: OptionChain = kvp.Value
symbol: Symbol = chain.Underlying.Symbol
# get option ticker from option symbol
ticker: str = symbol.Value
if ticker not in self.tickers_symbols:
continue
# based on option ticker get stock symbol
stock_symbol: Symbol = self.tickers_symbols[ticker]
# make sure, IV is updated once in a day
if stock_symbol not in self.data or self.data[stock_symbol].updated_date == self.Time.date():
continue
contracts: List[OptionContract] = [x for x in chain]
# make sure, there are enough contracts for stock
if len(contracts) < 2:
continue
call_iv: Union[None, float] = None
put_iv: Union[None, float] = None
# get atm call and atm put contract
for c in contracts:
if c.Right == OptionRight.Call:
# found atm call
call_iv = c.ImpliedVolatility
elif c.Right == OptionRight.Put:
# found atm put
put_iv = c.ImpliedVolatility
# check if there are both contracts
if call_iv and put_iv:
# calculate IV
iv: float = (put_iv + call_iv) / 2
# update RollingWindow for stock's IV values
self.data[stock_symbol].update_implied_vol(iv, self.Time.date())
# execute once a day
if self.day == self.Time.day:
return
self.day = self.Time.day
# check expiry of contracts
for symbol in self.data:
# remove contract, when it has 1 day to expiry
if symbol in self.contracts and ((self.contracts[symbol].expiry_date - timedelta(days=1)) <= self.Time.date()):
# remove expired contracts
for contract in self.contracts[symbol].contracts:
self.RemoveSecurity(contract)
# remove Contracts object for current symbol
del self.contracts[symbol]
# subscribe to contracts, if stock symbol doesn't have any
if symbol not in self.contracts:
# get new contracts after expiration
self.SubscribeOptionContracts(symbol)
# rebalance monthly
if not self.selection_flag:
return
self.selection_flag = False
vov: Dict[Symbol, float] = { x : self.data[x].volatility_of_volatility() for x in self.data if self.data[x].is_ready() }
# make sure there are enough stocks for quantile selection
if len(vov) < self.quantile:
self.Liquidate()
return
# sort stocks by VOV and perform quantile selection
quantile: int = int(len(vov) / self.quantile)
sorted_by_vov: List[Symbol] = [x[0] for x in sorted(vov.items(), key=lambda item: item[1])]
# long stocks with lowest VOV
long: List[Symbol] = sorted_by_vov[:quantile]
# short stocks with highest VOV
short: List[Symbol] = sorted_by_vov[-quantile:]
weight: Dict[Symbol, float] = {}
# trade execution
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda x: self.data[x].market_cap, portfolio)))
for symbol in portfolio:
weight[symbol] = (((-1)**i) * self.data[symbol].market_cap / mc_sum)
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
def SubscribeOptionContracts(self, symbol: Symbol) -> None:
''' get atm and atm strike for specific symbol then it filters atm call and atm put '''
''' if there are enough atm calls and atm puts this function subscribes one of their contracts based on expiry and store expiry date '''
# get all contracts for current commodity future
contracts: List[Symbol] = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for commodity future
underlying_price: float = self.Securities[symbol].Price
# get strikes from commodity future contracts
strikes: List[float] = [i.ID.StrikePrice for i in contracts]
# check if there is at least one strike
if len(strikes) <= 0:
return
# at the money
atm_strike: Union[None, float] = None
atm_strike = min(strikes, key=lambda x: abs(x-underlying_price))
# filtred contracts based on option rights and strikes
atm_calls: List[Symbol] = self.FilterContracts(contracts, OptionRight.Call, atm_strike)
atm_puts: List[Symbol] = self.FilterContracts(contracts, OptionRight.Put, atm_strike)
# make sure there are enough contracts
if len(atm_calls) > 0 and len(atm_puts) > 0:
# sort by expiry
atm_call: Symbol = sorted(atm_calls, key = lambda item: item.ID.Date, reverse=True)[0]
atm_put: Symbol = sorted(atm_puts, key = lambda x: x.ID.Date, reverse=True)[0]
# add contracts
for contract in [atm_call, atm_put]:
self.AddContract(contract)
# get expiry date of contracts
expiry_date: datetime.date = atm_call.ID.Date.date()
# create new Contracts object for stock
self.contracts[symbol] = Contract(expiry_date, [atm_call, atm_put])
def FilterContracts(self, contracts: List[Symbol], option_right: float, strike: float) -> List[Symbol]:
''' filter contracts based on option_right and select only contracts with expiry in next month are selected '''
# filter contracts based on option right and select only contracts with next month expiry
filtered_contracts: List[Symbol] = [i for i in contracts if i.ID.OptionRight == option_right and
i.ID.StrikePrice == strike and
(self.Time.month + 1) == i.ID.Date.month]
# self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
# return filtered contracts
return filtered_contracts
def AddContract(self, contract: Symbol) -> None:
''' subcribe to contract, set price model and normalization mode '''
option = self.AddOptionContract(contract, Resolution.Minute)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)
def Selection(self) -> None:
self.selection_flag = True
class Contract():
def __init__(self, expiry_date: datetime.date, contracts: List[Symbol]) -> None:
self.expiry_date: datetime.date = expiry_date
self.contracts: List[Symbol] = contracts
class SymbolData():
def __init__(self, period: int, market_cap: float):
self.implied_vol: RollingWindow = RollingWindow[float](period)
self.market_cap: float = market_cap
self.updated_date: Union[None, datetime.date] = None
def update_implied_vol(self, implied_vol: float, updated_date: datetime.date) -> None:
self.implied_vol.Add(implied_vol)
self.updated_date = updated_date
def update_market_cap(self, market_cap: float) -> None:
self.market_cap = market_cap
def volatility_of_volatility(self) -> float:
iv_values: np.ndarray = np.array([x for x in self.implied_vol])
mean_iv: float = np.mean(iv_values)
vov: float = np.sqrt(np.mean((iv_values - mean_iv)**2)) / mean_iv
return vov
def is_ready(self) -> bool:
return self.implied_vol.IsReady and self.market_cap
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))