期权凸性预测连续股票回报策略
登录后收藏学术论文
A Smiling Bear in the Equity Options Market and the Cross-Section of Stock Returns
股票期权市场中的微笑熊与股票回报的横截面 [点击查看论文]
- Southwestern University of Finance and Economics
- ?Southwestern University of Finance and Economics (SWUFE)
- KRKorea University
- ?Korea University Business School (KUBS)
- KRGachon University
策略概要
该策略专注于在纽约证券交易所、美国证券交易所和纳斯达克上市的具有流动性期权且可从OptionMetrics获得隐含波动率(IV)数据的股票。投资者使用以下公式计算IV凸度:IV凸度 = IVput(Δ = -0.2) + IVput(Δ = -0.8) − 2 x IVcall(Δ = 0.5)。在每个月末,股票根据其IV凸度被分为五个五等分。投资者做多IV凸度最低的五分之一(Q1),做空IV凸度最高的五分之一(Q5)。投资组合每月重新平衡并持有一个月,股票按价值加权。
II. 策略合理性
由于交易成本较低、杠杆较高且没有卖空限制等优势,知情交易者在股票期权市场比在股票市场更能有效地利用其信息优势。因此,期权价格通常比股票价格更早反映信息。预测股票尾部风险过高的投资者会导致回报的峰态隐含分布,这表明未来股票表现较差。这产生了一种负相关关系,即较高的超额峰度(通过隐含波动率(IV)凸度衡量)预测较低的未来股票回报。
回测表现
波动率9.99%
夏普比率1.05
索提诺比率-0.079
胜率52%
完整 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"))