
The strategy buys conglomerates on NYSE with earnings surprises, holding them for 58 days. It focuses on firms with a market cap above $5 billion and diversified operations, rebalancing daily.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: Post-Earnings Announcement Drift
I. STRATEGY IN A NUTSHELL
The strategy trades NYSE-listed conglomerates with market caps ≥ $5B and operations across multiple industries (≥2 SIC codes). Stocks are bought two days after an earnings surprise and held for 58 trading days. The portfolio is rebalanced daily to capture post-earnings momentum from market reactions to surprises.
II. ECONOMIC RATIONALE
Post-earnings announcement drift (PEAD) is stronger in conglomerates due to complex earnings reports, fewer analyst coverages, and slower price discovery. Low short interest, modest institutional ownership, and limited trading activity amplify delayed reactions. Statistical analysis confirms that earnings surprises interact with PEAD, making conglomerates ideal for momentum strategies.
III. SOURCE PAPER
Firm Complexity and Post-Earnings-Announcement Drift [Click to Open PDF]
Alexander Barinov, Shawn Saeyeul Park, Çelim Yıldızhan, University of California Riverside, Yonsei University, Koç University; University of Nevada Las Vegas
<Abstract>
We show that the post earnings announcement drift (PEAD) is stronger for conglomerates than single-segment firms. Conglomerates, on average, are larger than single segment firms, so it is unlikely that limits-to-arbitrage drive the difference in PEAD. Rather, we hypothesize that market participants find it more costly and difficult to understand firm-specific earnings information regarding conglomerates as they have more complicated business models than single-segment firms. This in turn slows information processing about them. In support of our hypothesis, we find that, compared to single-segment firms with similar firm characteristics, conglomerates have relatively low institutional ownership and short interest, are covered by fewer analysts, these analysts have less industry expertise and make larger forecast errors. Finally, we find that an increase in organizational complexity leads to larger PEAD and document that more complicated conglomerates have even greater PEAD. Our results are robust to a long list of alternative explanations of PEAD as well as alternative measures of firm complexity.


IV. BACKTEST PERFORMANCE
| Annualised Return | 16.4% |
| Volatility | N/A |
| Beta | 0.219 |
| Sharpe Ratio | N/A |
| Sortino Ratio | 0.173 |
| Maximum Drawdown | N/A |
| Win Rate | 55% |
V. FULL PYTHON CODE
import numpy as np
from AlgorithmImports import *
from trade_manager import TradeManager
from collections import deque
from pandas.tseries.offsets import BDay
from dateutil.relativedelta import relativedelta
class ConglomeratesPostEarningsAnnouncementDrift(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.period:int = 13
self.leverage:int = 5
self.seasonal_eps_count:int = 3
self.eps:Dict[Symbol, List[datetime.date, float]] = {}
self.earnings_data:Dict[datetime.date, Dict[str, float]] = {}
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.long:List[Symbol] = []
# equally weighted brackets for traded symbols
self.trade_manager:TradeManager = TradeManager(self, 20, 20, 58)
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_data[date] = {}
for stock_data in obj['stocks']:
ticker:str = stock_data['ticker']
if stock_data['eps'] != '':
self.earnings_data[date][ticker] = float(stock_data['eps'])
self.UniverseSettings.Resolution = Resolution.Daily
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.AddUniverse(self.FundamentalSelectionFunction)
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]:
# stocks with yesterday's earnings
yesterday:datetime.date = (self.Time - BDay(1)).date()
if yesterday not in self.earnings_data:
return Universe.Unchanged
tickers_with_yesterday_earnings:List[str] = list(self.earnings_data[yesterday].keys())
# stocks with yesterday's earnings
selected:List[Fundamental] = [x for x in fundamental if x.AssetClassification.MorningstarIndustryGroupCode == MorningstarIndustryGroupCode.Conglomerates and \
x.Symbol.Value in tickers_with_yesterday_earnings]
for stock in selected:
symbol:Symbol = stock.Symbol
ticker:str = symbol.Value
# store eps data
if symbol not in self.eps:
self.eps[symbol] = deque(maxlen=self.period)
data:List[datetime.date, float] = [yesterday, self.earnings_data[yesterday][ticker]]
self.eps[symbol].append(data)
if len(self.eps[symbol]) == self.eps[symbol].maxlen:
recent_eps_data:List[datetime.date, float] = self.eps[symbol][-1]
year_range:range = range(self.Time.year - 3, self.Time.year)
last_month_date:datetime.date = recent_eps_data[0] - relativedelta(months=1)
next_month_date:datetime.date = recent_eps_data[0] + relativedelta(months=1)
month_range:List[int] = [last_month_date.month, recent_eps_data[0].month, next_month_date.month]
# earnings with todays month number 4 years back
seasonal_eps_data:List[List[datetime.date, float]] = [x for x in self.eps[symbol] if \
x[0].month in month_range and x[0].year in year_range]
if len(seasonal_eps_data) != self.seasonal_eps_count: continue
# make sure we have a consecutive seasonal data
# same months with one year difference
year_diff:np.array = np.diff([x[0].year for x in seasonal_eps_data])
if all(x == 1 for x in year_diff):
seasonal_eps:List[float] = [x[1] for x in seasonal_eps_data]
diff_values:np.array = np.diff(seasonal_eps)
drift:float = np.average(diff_values)
# SUE calculation
last_earnings:float = seasonal_eps[-1]
expected_earnings:float = last_earnings + drift
actual_earnings:float = recent_eps_data[1]
# earning beat
if actual_earnings > expected_earnings:
self.long.append(symbol)
return self.long
def OnData(self, data):
self.trade_manager.TryLiquidate()
# Open new trades.
for symbol in self.long:
self.trade_manager.Add(symbol, True)
self.long.clear()
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))