利用跨式期权交易财报公告
登录后收藏学术论文
预测不确定性:财报公告前后的跨式期权 [点击查看论文]
https://quantpedia.com/www/Anticipating_Uncertainty-Straddles_Around_Earnings_Announcements.pdf
策略概要
该策略的目标是价格高于5美元的纽约证券交易所、美国证券交易所和纳斯达克的可期权股票。识别期权到期日前十天内的财报公告。对于符合条件的股票,投资者在财报公告前三天购买delta中性跨式期权组合。由于高波动性,仅分配10%的投资组合资产。跨式期权组合采用等权重,并持有至到期日,旨在利用财报公告前后的价格波动,同时限制整体投资组合风险的敞口。这种方法利用时机和期权定价动态来获取潜在收益。
II. 策略合理性
学术研究表明,delta中性的平值跨式期权在财报公告期间产生显著的正回报。这种情况的发生是因为期权市场投资者在公告前始终对公司基本面不确定性的增加反应不足。这种反应不足源于一种称为保守主义的行为偏差,即投资者基于新数据调整其信念的速度很慢。这种对信息的延迟反应为跨式期权策略创造了机会,可以利用财报事件周围期权市场中可预测的波动性上升和错误定价。
回测表现
波动率64.67%
夏普比率0.57
索提诺比率-1.119
胜率36%
完整 Python 代码
from AlgorithmImports import *
from pandas.tseries.offsets import BDay
from typing import Dict, List, Tuple
#endregion
class UsingStraddlesToTradeOnEarningsAnnouncements(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2018, 1, 1)
self.SetCash(100_000)
self.earnings: Dict[datetime.date, str] = {}
self.selected_for_trade: List[Tuple[Symbol, float]] = []
# dj30 universe
self.tickers: List[str] = [
'UNH','GS','HD','AMGN','MSFT','CAT','MCD','V','BA','HON','CRM','MMM','DIS','JNJ','JPM','TRV','AXP','IBM','WMT','PG','NKE','AAPL','CVX','MRK','DOW','VZ','INTC','KO','WBA','CSCO'
]
min_strike: int = -5
max_strike: int = 5
min_expiry: int = 4
max_expiry: int = 30
self.leverage: int = 10
self.day_threshold: int = 5
self.percentage_traded: float = 0.1 # only 10% of portfolio assets are used to perform this strategy
for ticker in self.tickers:
# add equity
data: Security = self.AddEquity(ticker, Resolution.Minute)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(self.leverage)
# add options
option: Option = self.AddOption(ticker, Resolution.Minute)
option.SetFilter(min_strike, max_strike, min_expiry, max_expiry)
# source: 'https://www.nasdaq.com/market-activity/earnings
earnings_data: str = self.Download('data.quantpedia.com/backtesting_data/economic/earnings_dates_eps.json')
earnings_data_json: List[dict] = json.loads(earnings_data)
for obj in earnings_data_json:
date: datetime.date = datetime.strptime(obj['date'], "%Y-%m-%d").date()
self.earnings[date] = []
for stock_data in obj['stocks']:
ticker: str = stock_data['ticker']
self.earnings[date].append(ticker)
# sort earnings by date
self.earnings: Dict[datetime, str] = {date: tickers for date, tickers in sorted(self.earnings.items(), key=lambda x: x[0])}
self.earnings_date: Union[None, datetime.date] = None
self.last_day: int = -1
self.opened_equity_count: int = 0
def OnData(self, slice: Slice) -> None:
# check once a day
if self.Time.day == self.last_day:
return
self.last_day = self.Time.day
invested: List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
# only equities are opened, options expired
if self.opened_equity_count != 0 and len(invested) == self.opened_equity_count:
self.opened_equity_count = 0
# liquidate hedge
self.Liquidate()
if not self.Portfolio.Invested and len(self.selected_for_trade) == 0:
current_date: datetime.date = self.Time.date()
for date in self.earnings:
# get closest earnings annoucement day, which is older than 3 days from current
# if date > current_date and abs((date - current_date).days) > 3 and self.earnings_date != date:
if date > (current_date + BDay(3)).date() and self.earnings_date != date:
self.earnings_date = date
break
# return from function, if earnings_date wasn't selected
if self.earnings_date is None:
return
for i in slice.OptionChains:
chains: OptionChains = i.Value
# get ticker for current chain
ticker:str = chains.get_Symbol().get_Value()[1:]
# check if current ticker is stored in self.earnings dictionary under self.earnings_date
if ticker not in self.earnings[self.earnings_date]:
continue
calls: List[OptionChains] = list(filter(lambda x: x.Right == OptionRight.Call, chains))
puts: List[OptionChains] = list(filter(lambda x: x.Right == OptionRight.Put, chains))
if not calls or not puts:
continue
underlying_price: float = chains.Underlying.Price
expiries: List[datetime.date] = [i.Expiry for i in puts]
expiry_date: datetime.date = None
for expiry in expiries:
date: datetime.date = expiry.date()
# pick expiry_date, if difference between expiration date and earnings annoucement date is less than 10 days
if abs((date - self.earnings_date).days) < 10:
expiry_date = date
break
# don't continue in current iteration, if expiry_date wasn't picked
if expiry_date is None:
continue
strikes: List[float] = [i.Strike for i in puts]
# determine at-the-money strike
strike: float = min(strikes, key=lambda x: abs(x-underlying_price))
atm_call: List[OptionChains] = [i for i in calls if i.Expiry.date() == expiry_date and i.Strike == strike]
atm_put: List[OptionChains] = [i for i in puts if i.Expiry.date() == expiry_date and i.Strike == strike]
if atm_call and atm_put:
self.selected_for_trade.append((atm_call[0], atm_put[0], underlying_price))
if (self.Time + BDay(3)).date() > self.earnings_date and len(self.selected_for_trade) != 0:
self.selected_for_trade.clear()
# need to trade 3 days before earnings annoucement
if (self.Time + BDay(3)).date() == self.earnings_date and len(self.selected_for_trade) != 0:
length: int = len(self.selected_for_trade)
for atm_call, atm_put, underlying_price in self.selected_for_trade:
if (atm_call.Symbol in slice and slice[atm_call.Symbol] and atm_put.Symbol in slice and slice[atm_put.Symbol] and atm_put.UnderlyingSymbol in slice and slice[atm_put.UnderlyingSymbol]):
options_q: float = int(((self.Portfolio.TotalPortfolioValue*self.percentage_traded) / length) / (underlying_price * 100))
# buy at-the-money straddle
self.Buy(atm_call.Symbol, options_q)
self.Buy(atm_put.Symbol, options_q)
self.SetHoldings(atm_put.UnderlyingSymbol, -self.percentage_traded / length)
# increase number of opened equities
self.opened_equity_count += 1
# clear list after trade
self.selected_for_trade.clear()
# 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"))