Quant Buffet放轻松,别过度思虑

横截面六个月减去一个月股票ATM跨式日历交易策略

登录后收藏

学术论文

Understanding and Trading the Term Structure of Volatility

作者Jump Risk and Option Returns [点击查看论文]

机构
  • Kansas State University
  • University of Massachusetts Amherst
  • ?Kansas State University - Department of Finance
  • ?University of Massachusetts Amherst - Isenberg School of Management
  • ?Isenberg School of Management, University of Massachusetts

策略概要

投资范围包括美国股票期权,重点关注每月到期后形成的跨式期权。仅考虑ATM期权(delta在0.35到0.65之间)。跨式期权根据隐含波动率期限结构的斜率分为十分位数。该策略涉及卖出十分位数10的一个月ATM跨式期权(具有最倒置的斜率),并买入十分位数10的六个月ATM跨式期权(具有最陡峭的正斜率)。投资组合每月重新平衡,并等权重,由于其高偏度,仅将20%的投资组合分配给该策略。该方法使用日历价差,涉及交易具有相同执行价格但不同到期日的期权。

II. 策略合理性

该策略基于风险价格和时间范围之间的关系,较长的风险衡量期通常会导致较小的风险价格。这一发现在所有资产类别和风险类型中都是一致的,表明投资者对不同的时间范围有偏好。隐含波动率的期限结构与短期到期期权的过度反应有关,而已实现波动率则影响短期和长期隐含波动率。波动率期限结构的斜率与短期到期期权的波动率风险溢价直接相关。倒置的期限结构降低了已实现波动率的影响,而陡峭的期限结构则增加了已实现波动率的影响,从而对一个月期权和长期到期期权的回报产生不同的影响。

回测表现

波动率11.8%
夏普比率3.12
索提诺比率-0.818
胜率40%

完整 Python 代码

from AlgorithmImports import *
from typing import List, Dict
from pandas.core.frame import DataFrame
from pandas.core.series import Series
#endregion
class CrossSectionalSixMinusOneMonthEquityATMStraddleCalendarTradingStrategy(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2012, 1, 1)
self.SetCash(1000000)

self.long_term_min_expiry: int = 150
self.long_term_max_expiry: int = 230

self.short_term_min_expiry: int = 20
self.short_term_max_expiry: int = 45

self.long_term_period: int = 6 * 21 # need n of stock daily prices
self.short_term_period: int = 21 # need n of stock daily prices
self.percentage_traded: float = 0.2
self.min_contracts: int = 4
self.leverage: int = 10
self.min_share_price: int = 5
self.quantile: int = 5

self.data: Dict[Symbol, RollingWindow] = {}
self.symbols_by_ticker: Dict[str, Symbol] = {}
self.subscribed_contracts: Dict[Symbol, Contracts] = {}

symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.day: int = -1
self.fundamental_count: int = 50
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag: bool = False
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
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, 0), 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]:
# update daily prices of stocks in self.data dictionary
for stock in fundamental:
    symbol: Symbol = stock.Symbol
    
    if symbol in self.data:
        self.data[symbol].Add(stock.AdjustedPrice)

# rebalance monthly
if not self.selection_flag:
    return Universe.Unchanged

# select top n stocks by dollar volume
selected: List[Fundamental] = [
    x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price > self.min_share_price
]
if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
for stock in selected:
    symbol: Symbol = stock.Symbol
    ticker: str = symbol.Value 
    self.symbols_by_ticker[ticker] = symbol
    
    if symbol in self.data:
        continue
    
    self.data[symbol] = RollingWindow[float](self.long_term_period)
    history: DataFrame = self.History(symbol, self.long_term_period, Resolution.Daily)
    if history.empty:
        continue
    closes: Series = history.loc[symbol].close
    for time, close in closes.items():
        self.data[symbol].Add(close)
# return newly selected symbols
return list(map(lambda x: x.Symbol, selected))
def OnData(self, data: Slice) -> None:
# execute once a day
if self.day == self.Time.day:
    return
self.day = self.Time.day

# subscribe to new contracts after selection
if len(self.subscribed_contracts) == 0 and self.selection_flag:
    for _, symbol in self.symbols_by_ticker.items():
        if symbol in self.data and self.data[symbol].IsReady:
            # get all contracts for current stock symbol
            contracts: List[Symbol] = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
            # get current price for etf
            underlying_price: float = self.data[symbol][0]
            
            # get strikes from commodity future contracts
            strikes: List[float] = [i.ID.StrikePrice for i in contracts]
            
            # can't filter contracts, if there isn't any strike price
            if len(strikes) <= 0 or underlying_price == 0:
                continue
            
            # filter calls and puts contracts with six months expiry
            long_term_calls, long_term_puts = self.FilterContracts(
                strikes,
                contracts,
                underlying_price,
                self.long_term_min_expiry,
                self.long_term_max_expiry
            )
            
            # filter calls and puts contracts with one month expiry
            short_term_calls, short_term_puts = self.FilterContracts(
                strikes,
                contracts,
                underlying_price,
                self.short_term_min_expiry,
                self.short_term_max_expiry
            )
            
            # make sure, there is at least one call and put contract
            if len(long_term_calls) > 0 and len(long_term_puts) > 0 and len(short_term_calls) > 0 and len(short_term_puts) > 0:
                # sort by expiry
                long_term_call: Symbol = sorted(long_term_calls, key = lambda x: x.ID.Date, reverse=True)[0]
                long_term_put: Symbol = sorted(long_term_puts, key = lambda x: x.ID.Date, reverse=True)[0]
                
                short_term_call: Symbol = sorted(short_term_calls, key = lambda x: x.ID.Date, reverse=True)[0]
                short_term_put: Symbol = sorted(short_term_puts, key = lambda x: x.ID.Date, reverse=True)[0]
                
                subscriptions = self.SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(short_term_call.Underlying)
                if subscriptions:
                    # add contracts
                    for contract in [long_term_call, long_term_put, short_term_call, short_term_put]:
                        
                        self.AddContract(contract)
                    
                    # retrieve expiry date for contracts
                    long_term_expiry_date: datetime.date = long_term_call.ID.Date.date()
                    
                    short_term_expiry_date: datetime.date = short_term_call.ID.Date.date()
                    
                    # store contracts with expiry date under stock's symbol
                    self.subscribed_contracts[symbol] = Contracts(
                        long_term_expiry_date,
                        short_term_expiry_date, 
                        [long_term_call, long_term_put],
                        [short_term_call, short_term_put],
                        underlying_price
                    )
    
# calculate term structure and trade options
elif len(self.subscribed_contracts) != 0 and data.OptionChains.Count != 0 and self.selection_flag:
    self.selection_flag = False # this makes sure, there will be no other trades until next selection
    
    term_structure_long_term: Dict[Symbol, float] = {} # storing term structures keyed by stock's symbol
    term_structure_short_term: Dict[Symbol, float] = {}
    
    for kvp in data.OptionChains:
        chain: OptionChain = kvp.Value
        ticker: str = chain.Underlying.Symbol.Value
        if ticker in self.symbols_by_ticker:
            # get stock's symbol
            symbol: Symbol = self.symbols_by_ticker[ticker]
            if symbol in self.subscribed_contracts:
                # get contracts
                contracts: List[Symbol] = [x for x in chain]
                
                # check if there are enough contracts for option and daily prices are ready
                if len(contracts) < self.min_contracts or not self.data[symbol].IsReady:
                    continue
                
                # expiry dates are needed for finding out which contract is long term and which one is short term when retrieving IV
                long_term_expiry_date: datetime.date = self.subscribed_contracts[symbol].long_term_expiry_date
                short_term_expiry_date: datetime.date = self.subscribed_contracts[symbol].short_term_expiry_date
                
                # get call and put implied volatility
                long_term_call_iv, long_term_put_iv, short_term_call_iv, short_term_put_iv = self.GetImpliedVolatilities(
                    contracts,
                    long_term_expiry_date,
                    short_term_expiry_date
                )
                
                if long_term_call_iv and long_term_put_iv and short_term_call_iv and short_term_put_iv:
                    # make mean from call implied volatility and put implied volatility
                    long_term_iv: float = (long_term_call_iv + long_term_put_iv) / 2 
                    short_term_iv: float = (short_term_call_iv + short_term_put_iv) / 2
                    
                    # get historical volatility for long term
                    long_term_hv: float = self.GetHistoricalVolatility(self.data[symbol], self.long_term_period)
                    short_term_hv: float = self.GetHistoricalVolatility(self.data[symbol], self.short_term_period)
                    
                    # store stock's term structure
                    term_structure_long_term[symbol] = (long_term_iv - long_term_hv) / long_term_hv
                    term_structure_short_term[symbol] = (short_term_iv - short_term_hv) / short_term_hv
        
    # can't perform selection
    if len(term_structure_long_term) < self.quantile or len(term_structure_short_term) < self.quantile:
        return
    
    # perform quintile selection
    quantile: int = int(len(term_structure_long_term) / self.quantile)
    sorted_by_ts_long_term: List[Symbol] = [x[0] for x in sorted(term_structure_long_term.items(), key=lambda item: item[1])]
    sorted_by_ts_short_term: List[Symbol] = [x[0] for x in sorted(term_structure_short_term.items(), key=lambda item: item[1])]
    
    # the strategy sells quintile 5 of 1 month ATM straddle and buys quintile 5 of 6 month ATM straddles.
    long: List[Symbol] = sorted_by_ts_long_term[:quantile]
    short: List[Symbol] = sorted_by_ts_short_term[:quantile]
    
    # trade execution
    self.Liquidate()
    
    # trade long
    self.TradeOptions(data, long, True, True) # parameters: symbols, long_flag, long_term_flag
    # trade short
    self.TradeOptions(data, short, False, False) # parameters: symbols, long_flag, long_term_flag
    
def Selection(self) -> None:
self.selection_flag = True # perform new selection
self.Liquidate() # rebalance monthly, so liquidate all holding contracts

# remove contracts from securities
# for _, contracts_obj in self.subscribed_contracts.items():
#     for contract in contracts_obj.long_term_contracts + contracts_obj.short_term_contracts:
#         self.RemoveSecurity(contract)
        
# clear dictionary for subscribed contracts, because there will be new selection
self.subscribed_contracts.clear()
# clear dictionary of tickers and their symbols, because new stocks will be selected
self.symbols_by_ticker.clear()

def FilterContracts(self, 
                strikes: List[float],
                contracts: List[Symbol], 
                underlying_price: float, 
                min_expiry: datetime.date, 
                max_expiry: datetime.date) -> List[Symbol]:
''' filter call and put contracts from contracts parameter '''
''' return call and put contracts '''

# Straddle
call_strike: float = min(strikes, key=lambda x: abs(x-underlying_price))
put_strike: float = call_strike

calls: List[Symbol] = [] # storing call contracts
puts: List[Sybol] = [] # storing put contracts

for contract in contracts:
    # check if contract has one month expiry
    if min_expiry < (contract.ID.Date - self.Time).days < max_expiry:
        # check if contract is call
        if contract.ID.OptionRight == OptionRight.Call and contract.ID.StrikePrice == call_strike:
            calls.append(contract)
        # check if contract is put
        elif contract.ID.OptionRight == OptionRight.Put and contract.ID.StrikePrice == put_strike:
            puts.append(contract)

# return filtered calls and puts with one month expiry
return calls, puts

def AddContract(self, contract: Symbol) -> None:
''' subscribe option contract, set price mondel and normalization mode '''
option: Option = self.AddOptionContract(contract, Resolution.Daily)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)

def GetImpliedVolatilities(self, 
                    contracts: List[Symbol], 
                    long_term_expiry: datetime.date, 
                    short_term_expiry: datetime.date) -> float:
''' retrieve implied volatility of contracts from contracts parameteres '''
''' returns long term and short term implied volatility for call and put contracts '''
long_term_call_iv: Union[None, float] = None
long_term_put_iv: Union[None, float] = None

short_term_call_iv: Union[None, float] = None
short_term_put_iv: Union[None, float] = None
        
# go through option contracts
for c in contracts:
    iv: float = c.ImpliedVolatility
    expiry_date: datetime.date = c.get_Expiry().date()
    
    if c.Right == OptionRight.Call:
        if expiry_date == long_term_expiry:
            long_term_call_iv = iv
        elif expiry_date == short_term_expiry:
            short_term_call_iv = iv
    else:
        if expiry_date == long_term_expiry:
            long_term_put_iv = iv
        elif expiry_date == short_term_expiry:
            short_term_put_iv = iv
    
return long_term_call_iv, long_term_put_iv, short_term_call_iv, short_term_put_iv

def GetHistoricalVolatility(self, 
                        rolling_window_prices: RollingWindow, 
                        period: int) -> float:
''' calculate historical volatility based on daily prices in rolling_window_prices parameter '''
prices: np.ndarray = np.array([x for x in rolling_window_prices][:period])
returns: np.ndarray = (prices[:-1] - prices[1:]) / prices[1:]
return np.std(returns)

def TradeOptions(self, 
            data: Slice, 
            symbols: List[Symbol], 
            long_flag: bool, 
            long_term_flag: bool) -> None:
''' on long signal buy call and put option contract '''
''' on short signal sell call and put option contract '''
length: int = len(symbols)

# trade etf's call and put contracts
for symbol in symbols:
    if long_term_flag:
        contracts: List[Symbol] = self.subscribed_contracts[symbol].long_term_contracts
    else:
        contracts: List[Symbol] = self.subscribed_contracts[symbol].short_term_contracts
        
    # check if contracts are tradebale and don't have 0 price
    for contract in contracts:
        if not self.Securities[contract].IsTradable or self.Securities[contract].Price == 0:
            return
        
    # get call and put contract
    call, put = contracts
    # get underlying price
    underlying_price: float = self.subscribed_contracts[symbol].underlying_price
    
    options_q: int = int(((self.Portfolio.TotalPortfolioValue * self.percentage_traded) / length) / (underlying_price * 100))
    
    self.Securities[put].MarginModel = BuyingPowerModel(2)
    self.Securities[call].MarginModel = BuyingPowerModel(2)
    
    if call in data and data[call] and put in data and data[put]:
        if long_flag:
            self.Buy(call, options_q)
            self.Buy(put, options_q)
        else:
            self.Sell(call, options_q)
            self.Sell(put, options_q)

class Contracts():
def __init__(self, 
        long_term_expiry_date: datetime.date, 
        short_term_expiry_date: datetime.date, 
        long_term_contracts: List[Symbol], 
        short_term_contracts: List[Symbol], 
        underlying_price: float) -> None:
self.long_term_expiry_date: datetime.date = long_term_expiry_date
self.long_term_contracts: List[Symbol] = long_term_contracts

self.short_term_expiry_date: datetime.date = short_term_expiry_date
self.short_term_contracts: List[Symbol] = short_term_contracts

self.underlying_price = underlying_price
# 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"))