Volatility Investing Across Asset Classes
Log in to collectAcademic paper
Realized Semibetas: Signs of Things to Come
Tim Bollerslev; Andrew J. Patton; Rogier Quaedvlieg
- Duke University
- National Bureau of Economic Research
- ?Duke University - Department of Economics
- ?Duke University - Finance
- ?National Bureau of Economic Research (NBER)
- DEEuropean Central Bank
- ?European Central Bank (ECB)
Strategy in a nutshell
The strategy trades 15 indexes across equities, commodities, forex, and bonds. Every third Friday, the investor shorts monthly variance swaps with a constant 1% vega exposure, assuming that future realized variance will revert. The portfolio is equally weighted and rebalanced monthly.
Economic rationale
Variance swaps are often overpriced due to investor demand for protection, skewness aversion, and capital-guaranteed products, combined with margin effects. Selling them captures the variance risk premium while exploiting these behavioral and market-driven mispricings.
Backtest performance
Annualised return19.9%
Volatility18.9%
Beta0.525
Sharpe ratio1.05
Sortino ratio0.087
Maximum drawdown58%
Win rate53%
Full Python code
from collections import deque
import numpy as np
class VolatilityInvestingAcrossAssetClasses(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.symbols = ['SPY', 'IWM', 'EUE', 'CNKY', 'EEM', 'EWZ', 'HSI', 'USO', 'GLD', 'SLV', 'EURUSD', 'GBPUSD', 'JPYUSD', 'IEF']
# Daily price data.
self.data = {}
self.period = 21
self.SetWarmUp(self.period)
for symbol in self.symbols:
data = self.AddEquity(symbol, Resolution.Daily)
data.SetLeverage(5)
data_symbol = data.Symbol
option = self.AddOption(data_symbol, Resolution.Minute)
self.data[symbol] = deque(maxlen = self.period)
self.last_day = -1
self.invested_etf = []
def OnData(self, slice):
# Check once a day.
if self.Time.day == self.last_day:
return
self.last_day = self.Time.day
# Store underlying daily price data.
for symbol in self.symbols:
if symbol in slice and slice[symbol]:
price = slice[symbol].Value
self.data[symbol].append(price)
if self.IsWarmingUp: return
weight_ratio = sum([1 / Volatility(self.data[symbol]) for symbol in self.data if len(self.data[symbol]) == self.data[symbol].maxlen])
if weight_ratio == 0: return
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
if len(invested) <= len(self.invested_etf):
# Only ETF's or nothing are invested in.
symbols_to_remove = []
for symbol in self.invested_etf:
# Liquidate etf holdings and underlying symbol for options.
self.Liquidate(symbol)
symbols_to_remove.append(symbol)
for symbol in symbols_to_remove:
self.invested_etf.remove(symbol)
for i in slice.OptionChains:
chains = i.Value
calls = list(filter(lambda x: x.Right == OptionRight.Call, chains))
puts = list(filter(lambda x: x.Right == OptionRight.Put, chains))
if not calls or not puts: continue
symbol = chains.Underlying.Symbol.Value
if len(self.data[symbol]) != self.data[symbol].maxlen: continue
underlying_price = chains.Underlying.Price
expiries = [i.Expiry for i in puts]
# Determine expiration date nearly one month.
expiry = min(expiries, key=lambda x: abs((x.date()-self.Time.date()).days-30))
strikes = [i.Strike for i in puts]
# Determine at-the-money strike.
strike = min(strikes, key=lambda x: abs(x-underlying_price))
atm_call = [i for i in calls if i.Expiry == expiry and i.Strike == strike][0]
atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike][0]
if atm_call and atm_put:
etf_weight = (1 / Volatility(self.data[symbol])) / weight_ratio
self.SetHoldings(symbol, etf_weight)
self.invested_etf.append(symbol)
self.Securities[atm_call.Symbol].MarginModel = BuyingPowerModel(5)
self.Securities[atm_put.Symbol].MarginModel = BuyingPowerModel(5)
# Sell at-the-money straddle.
self.Sell(atm_call.Symbol, 1)
self.Sell(atm_put.Symbol, 1)
def Volatility(values):
values = np.array(values)
returns = (values[1:] - values[:-1]) / values[:-1]
return np.std(returns)