Quant BuffetRelax, Not Over Thinking

Volatility Investing Across Asset Classes

Log in to collect

Academic paper

Realized Semibetas: Signs of Things to Come

AuthorsTim Bollerslev; Andrew J. Patton; Rogier Quaedvlieg

Institute
  • 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)