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
- 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
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, reverse=True) quintile_size = len(sorted_by_surprise) // 5 long_stocks = [x for x in sorted_by_surprise[:quintile_size] if x > 0] short_stocks = [x for x in sorted_by_surprise[-quintile_size:] if x < 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 (
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.