Quant Buffet放轻松,别过度思虑

利用跨式期权交易财报公告

登录后收藏

学术论文

作者预测不确定性:财报公告前后的跨式期权 [点击查看论文]

策略概要

该策略的目标是价格高于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"))