“该策略涉及具有流动性期权的股票,使用隐含波动率凸度将股票分为五等分。投资者做多凸度最低的五分之一,做空凸度最高的五分之一,每月重新平衡。”

I. 策略概要

该策略专注于在纽约证券交易所、美国证券交易所和纳斯达克上市的具有流动性期权且可从OptionMetrics获得隐含波动率(IV)数据的股票。投资者使用以下公式计算IV凸度:IV凸度 = IVput(Δ = -0.2) + IVput(Δ = -0.8) − 2 x IVcall(Δ = 0.5)。在每个月末,股票根据其IV凸度被分为五个五等分。投资者做多IV凸度最低的五分之一(Q1),做空IV凸度最高的五分之一(Q5)。投资组合每月重新平衡并持有一个月,股票按价值加权。

II. 策略合理性

由于交易成本较低、杠杆较高且没有卖空限制等优势,知情交易者在股票期权市场比在股票市场更能有效地利用其信息优势。因此,期权价格通常比股票价格更早反映信息。预测股票尾部风险过高的投资者会导致回报的峰态隐含分布,这表明未来股票表现较差。这产生了一种负相关关系,即较高的超额峰度(通过隐含波动率(IV)凸度衡量)预测较低的未来股票回报。

III. 来源论文

股票期权市场中的微笑熊与股票回报的横截面 [点击查看论文]

<摘要>

我们提出了期权隐含波动率曲线凸度的一种度量方法,即IV凸度,作为对感知到的标的股票回报方差的超额尾部风险贡献的前瞻性度量。使用2000-2013年美国上市个股的股票期权数据,我们发现最低和最高IV凸度五等分投资组合之间的平均回报差异每月超过1%,这在风险调整后的基础上具有经济和统计学意义。我们的实证结果表明,知情期权交易在股票市场实现尾部风险规避方面的价格发现贡献。

IV. 回测表现

年化回报14.44%
波动率9.99%
β值-0.025
夏普比率1.05
索提诺比率-0.079
最大回撤N/A
胜率52%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
from typing import Dict, List, Tuple
class OptionsConvexityPredictsConsecutiveStockReturns(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        
        self.min_expiry: int = 25
        self.max_expiry: int = 35
        self.quantile: int = 5
        self.leverage: int = 5
        self.min_share_price: int = 5
        self.contracts_count: int = 3
        self.thresholds: List[int] = [0.95, 1.05]
        self.next_expiry: Union[None, datetime.date] = None
        
        self.fundamental_count: int = 100
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.selection_flag: bool = False
        self.stock_universe: List[Symbol] = []
        self.option_universe: Dict[Symbol, List[Symbol]] = {}
        self.contracts_expiry: Dict[Symbol, datetime.date] = {} # storing contracts expiry date under symbols
        self.settings.daily_precise_end_time = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
        self.current_day: int = -1
        symbol: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.Schedule.On(self.DateRules.MonthStart(symbol), self.TimeRules.AfterMarketOpen(symbol), self.Selection)
    
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
        # remove old option contracts
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.option_universe:
                for option in self.option_universe[symbol]:
                    self.RemoveSecurity(option)
                del self.option_universe[symbol]
                self.Liquidate(symbol)
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # new symbol selection once a quarter
        if not self.selection_flag:
            return Universe.Unchanged
        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]
                ]     
        
        self.stock_universe = [x.Symbol for x in selected]
        return self.stock_universe
        
    def Selection(self) -> None:
        if self.Time.month % 3 == 0:
            self.selection_flag = True
            self.Liquidate()
    
    def OnData(self, data: Slice) -> None:
        # rebalance daily
        if self.current_day == self.Time.day:
            return
        self.current_day = self.Time.day
        
        if self.next_expiry and self.Time.date() >= self.next_expiry.date():
            for symbol in self.option_universe:
                for option in self.option_universe[symbol]:
                    self.RemoveSecurity(option)
            self.Liquidate()
        # for symbol in self.option_universe:
        #     # subscribe to new contracts, because current ones has expiried
        #     if symbol not in self.contracts_expiry or self.contracts_expiry[symbol] <= self.Time.date():
            
        if not self.Portfolio.Invested:
            for symbol in self.stock_universe:
                contracts: List[Symbol] = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
                underlying_price: float = self.Securities[symbol].Price
                
                if self.Securities[symbol].IsDelisted:
                    continue
                strikes: List[float] = [i.ID.StrikePrice for i in contracts]
                if len(strikes) > 0:
                    atm_strike: float = min(strikes, key=lambda x: abs(x-underlying_price))
                    itm_strike: float = min(strikes, key=lambda x: abs(x-(underlying_price*min(self.thresholds))))
                    otm_strike: float = min(strikes, key=lambda x: abs(x-(underlying_price*max(self.thresholds))))
                    
                    atm_calls: List[Symbol] = [i for i in contracts if i.ID.OptionRight == OptionRight.Call and 
                                                         i.ID.StrikePrice == atm_strike and 
                                                         self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
    
                    itm_puts: List[Symbol] = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and 
                                                         i.ID.StrikePrice == itm_strike and 
                                                         self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
    
                    otm_puts: List[Symbol] = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and 
                                                         i.ID.StrikePrice == otm_strike and 
                                                         self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
                    
                    if len(atm_calls) > 0 and len(itm_puts) > 0 and len(otm_puts) > 0:
                        # sort by expiry
                        atm_call: List[Symbol] = sorted(atm_calls, key = lambda x: x.ID.Date)[0]
                        itm_put: List[Symbol] = sorted(itm_puts, key = lambda x: x.ID.Date)[0]
                        otm_put: List[Symbol] = sorted(otm_puts, key = lambda x: x.ID.Date)[0]
                        
                        # store expiry date
                        # self.contracts_expiry[symbol] = itm_put.ID.Date.date()
                        self.next_expiry = atm_call.ID.Date
                        # add contracts
                        option: Option = self.AddOptionContract(atm_call, Resolution.Minute)
                        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
                        
                        option: Option = self.AddOptionContract(itm_put, Resolution.Minute)
                        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
                        
                        option: Option = self.AddOptionContract(otm_put, Resolution.Minute)
                        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
                        
                        options: List[Symbol] = [atm_call, itm_put, otm_put]
                        self.option_universe[symbol] = options           
            
            iv_convexity: Dict[Symbol, float] = {} 
            if data.OptionChains.Count != 0:
                for kvp in data.OptionChains:
                    chain: OptionChain = kvp.Value
                    contracts: List[Symbol] = [x for x in chain]
                    if len(contracts) == self.contracts_count:
                        atm_call_iv: Union[None, float] = None
                        itm_put_iv: Union[None, float] = None
                        otm_put_iv: Union[None, float] = None
                        symbol: Symbol = chain.Underlying.Symbol
                        for c in contracts:
                            if c.Right == OptionRight.Call:
                                # found atm call
                                atm_call_iv = c.ImpliedVolatility
                            else:
                                # found put option
                                underlying_price:float = self.Securities[c.UnderlyingSymbol].Price
                                if c.Strike < underlying_price:
                                    # found itm put
                                    itm_put_iv = c.ImpliedVolatility
                                else:
                                    # found otm put
                                    otm_put_iv = c.ImpliedVolatility
                        
                        if atm_call_iv and itm_put_iv and otm_put_iv:
                            iv_convexity[symbol] = itm_put_iv + otm_put_iv - (2*atm_call_iv)
            long: List[Symbol] = []
            short: List[Symbol] = []
        
            # convexity sorting
            if len(iv_convexity) >= self.quantile:
                sorted_by_convexity: List[Tuple[Symbol, float]] = sorted(iv_convexity.items(), key = lambda x: x[1], reverse = True)
                quantile: int = int(len(sorted_by_convexity) / self.quantile)
                long = [x[0] for x in sorted_by_convexity[-quantile:]]
                short = [x[0] for x in sorted_by_convexity[:quantile]]
            
            # trade execution
            targets: List[PortfolioTarget] = []
            for i, portfolio in enumerate([long, short]):
                for symbol in portfolio:
                    if symbol in data and data[symbol]:
                        targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
            self.SetHoldings(targets, True)
# 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"))

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读