Quant Buffet放轻松,别过度思虑

跨资产类别的波动率投资

登录后收藏

学术论文

Realized Semibetas: Signs of Things to Come

作者Realized Semibetas: Signs of Things to Come [点击查看论文]

机构
  • 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)

策略概要

投资范围包括横跨4个资产类别的15个指数:股票(标准普尔500指数、罗素2000指数、欧元区斯托克50指数、日经225指数、EEM ETF、EWZ ETF、恒生指数)、商品(USO、GLD、SLV)、外汇(欧元兑美元、英镑兑美元、美元兑日元)和债券(10年期美国国债期货)。投资者在每个月的第三个星期五卖空月度方差互换投资组合,保持1%的常数vega敞口。该策略假设未来实现的方差风险溢价基于近期可观察的方差。投资组合等权重,并每月重新平衡。

II. 策略合理性

通过方差互换卖空波动率涉及重大风险,需要对潜在的回撤进行补偿。行为金融学强调了导致定价错误的三个关键偏差。首先,偏度厌恶导致投资者偏好具有正偏度的资产,促使他们购买保护。其次,寻求达到风险价值(VaR)目标或减少回撤的投资者发现,长期方差互换作为对冲工具很有吸引力。第三,投资者偏好资本担保票据而非期权,从而推高了对保护的需求。这种供不应求导致方差互换定价过高。此外,保证金要求进一步加剧了供需之间的不对称性,增加了隐含波动率,并将溢价推高至公平水平之上。

回测表现

波动率18.9%
夏普比率1.05
索提诺比率0.087
最大回撤58%
胜率53%

完整 Python 代码

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)