Original paper
Abstract
This paper examines stock price formation subsequent to management forecasts of quarterly earnings. In the post-announcement period, we find a significant upward price drift for both good news forecasts and bad news forecasts. The asymmetry in the initial market response and the subsequent upward drift in stock prices are consistent with a reversal of an initial overreaction to managers’ bad news forecasts and a continuation of an initial underreaction to managers’ good news forecasts. This interpretation is supported by a negative (positive) relationship between the initial market response and the post-guidance drift in the bad news (good news) group. The drift pattern is robust to issues arising from measurement. Trading strategies exploiting the post-announcement drift suggest the existence of economically significant trading profits, net of estimated trading costs.
Keywords:Â Management forecasts, earnings guidance, stock price drift, overreaction, underreaction
Trading rules
- Investment universe: NYSE, AMEX, and NASDAQ companies
- Criteria: Quarterly earnings guidance, stock price > $1
- Forecast surprise: Management EPS forecast - Pre-management-forecast analyst consensus mean EPS forecast
- Sorting: Quintile portfolios based on forecast surprise
- Positioning: Long on positive surprise firms, short on negative surprise firms
- Entry: 3rd day after management forecast
- Holding period: 3 months
- Portfolio weighting: Equally weighted
Python code
Backtrader
import backtrader as bt
class PostGuidanceDrift(bt.Strategy):
params = (
('hold_period', 63),
('ranking_period', 1)
)
def __init__(self):
self.quarterly_earnings = dict()
self.forecast_surprise = dict()
def next(self):
if len(self) % self.params.ranking_period:
return
for d in self.datas:
if d._name not in self.quarterly_earnings:
continue
surprise = self.quarterly_earnings[d._name]['management_eps_forecast'] - self.quarterly_earnings[d._name]['pre_mgmt_analyst_consensus_mean']
self.forecast_surprise[d._name] = surprise
sorted_by_surprise = sorted(self.forecast_surprise.items(), key=lambda x: x[1], reverse=True)
quintile_size = len(sorted_by_surprise) // 5
long_stocks = [x[0] for x in sorted_by_surprise[:quintile_size] if x[1] > 0]
short_stocks = [x[0] for x in sorted_by_surprise[-quintile_size:] if x[1] < 0]
for d in self.datas:
if d._name in long_stocks:
self.order_target_percent(data=d, target=1 / len(long_stocks))
elif d._name in short_stocks:
self.order_target_percent(data=d, target=-1 / len(short_stocks))
else:
self.order_target_percent(data=d, target=0)
def notify_order(self, order):
pass
def notify_trade(self, trade):
pass
cerebro = bt.Cerebro()
# Add your data feeds here
cerebro.addstrategy(PostGuidanceDrift)
cerebro.run()
Please note that this code only provides the core structure of the strategy. You will need to provide the quarterly earnings data in the self.quarterly_earnings
dictionary with the required fields (management_eps_forecast
and pre_mgmt_analyst_consensus_mean
). You’ll also need to load your data feeds for NYSE, AMEX, and NASDAQ companies and ensure they have the required quarterly earnings guidance and stock prices greater than $1.