In the days before earnings announcements we find an average price increase of almost 1 percent for stocks that are likely to be overpriced already - stocks with low institutional ownership combined with high market-to-book ratios, turnover, volatility, or analyst forecast dispersion. However, in the days after earnings announcements these same stocks generate negative abnormal returns of more than 3 percent. Together, these results indicate a significant net correction following earnings announcements for stocks that are prone to be overpriced. These results are consistent with the optimism bias hypothesized in Miller (1977), and with recent evidence that cross-sectional return predictability is concentrated among stocks with low institutional ownership.
Keywords: market efficiency, short-sale restrictions, overpricing, earnings
- Focus on equities within the Russell 3000 index.
- Organize stocks into five groups based on institutional ownership levels and trading volume.
- Select stocks with the lowest ownership and highest volume.
- Identify stocks with upcoming earnings announcement dates.
- Take a long position in stocks 48 hours prior to their announcement.
- Take a short position in stocks 48 hours post-announcement.
- Maintain equal weighting for all stocks.
- Adjust the portfolio on a daily basis.
import backtrader as bt import pandas as pd import numpy as np class EarningsAnnouncementStrategy(bt.Strategy): def __init__(self): self.earnings_data = pd.read_csv('earnings_data.csv') # Import earnings data self.russell_data = pd.read_csv('russell_3000_data.csv') # Import Russell 3000 data def next(self): date = self.datas.datetime.date(0) if len(self) % 5 == 0: # Update stock selection every 5 days selected_stocks = self.select_stocks(date) self.rebalance_portfolio(selected_stocks) for data in self.datas: symbol = data._name earnings_date = self.earnings_data.loc[self.earnings_data['symbol'] == symbol, 'earnings_date'].iloc if not earnings_date: continue earnings_date = pd.to_datetime(earnings_date).date() days_to_earnings = (earnings_date - date).days if days_to_earnings == 2: # Go long 2 days before announcement self.order_target_percent(data, target=1.0 / len(selected_stocks)) elif days_to_earnings == -2: # Short 2 days after announcement self.order_target_percent(data, target=-1.0 / len(selected_stocks)) def select_stocks(self, date): current_data = self.russell_data.loc[self.russell_data['date'] == date] current_data['quintile'] = pd.qcut(current_data['institutional_ownership'] * current_data['volume'], 5, labels=False) selected_stocks = current_data.loc[current_data['quintile'] == 0, 'symbol'].tolist() return selected_stocks def rebalance_portfolio(self, selected_stocks): for data in self.datas: symbol = data._name if symbol not in selected_stocks: self.order_target_percent(data, target=0.0) if __name__ == '__main__': cerebro = bt.Cerebro() cerebro.addstrategy(EarningsAnnouncementStrategy) # Add data feeds for Russell 3000 stocks for symbol in russell_3000_symbols: data = bt.feeds.YahooFinanceData(dataname=symbol, fromdate=start_date, todate=end_date) cerebro.adddata(data, name=symbol) cerebro.broker.setcash(100000.0) cerebro.run() cerebro.plot()
Make sure to replace
russell_3000_data.csv with the appropriate CSV file paths containing earnings announcement dates and Russell 3000 index data, respectively. Also, replace
end_date with the relevant information.