In the paper "Driven to Distraction: Extraneous Events and Underreaction to Earnings News", the authors compare Post Earnings Announcement Drift for stocks that announce earnings during peak earnings season and stocks that announce earnings when there are fewer other competing announcements.

I backtested this strategy in Quantopian: Every day, I look at whether the companies announcing earnings that day are either in the top or bottom quintile of earnings announcements. If it's in the top quintile of announcememnts, I go long PEAD by buying the top quintile of earnings beaters and shorting the bottom quintile of earnings missers. On the other hand, if it is in the bottom quintile of announcements, I do the opposite PEAD trade: I short the top quintile of beaters and go long the bottom quintile of missers.

In order to estimeate the quintile cutoffs for beaters and missers, as well as the quintile cutoffs for stocks on high and low announcement days, I created this notebook.

In [1]:

```
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor
from quantopian.research import run_pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import Q500US
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
BusinessDaysUntilNextEarnings,
BusinessDaysSincePreviousEarnings
)
from quantopian.pipeline.data.zacks import EarningsSurprises
```

In [2]:

```
pipe = Pipeline()
pipe.add(BusinessDaysSincePreviousEarnings(), "business_days_since")
pipe.set_screen(Q500US() & EarningsSurprises.eps_mean_est.latest.notnan())
pipe_output = run_pipeline(pipe, start_date='2015-10-01',end_date='2016-09-30')
pipe_output[:20]
```

Out[2]:

To get the number of companies announcing earnings on each day of the quarter:

In [3]:

```
earners=pipe_output[pipe_output['business_days_since']==0]
earners[:20]
```

Out[3]:

`unstack`

method to make it a traditional two dimensional DataFrame (or alternatively you can use `groupby(level=0)`

), and count how many stocks announce on each day.

In [4]:

```
earnings_count=earners.unstack().count(axis=1)
earnings_count.sort_index()
```

Out[4]:

And the distribution of number of announcements:

In [5]:

```
earnings_count.hist(bins=20)
plt.ylabel("Frequency")
plt.xlabel("Number of Earnings Announcements")
```

Out[5]:

Below we compute the quintiles of this distribution.

Because there are many companes that announce on the highest 20% of days and fewer companies that announce on low announcement days, it is important to note that these aren't the cutoffs that divide the universe into quintiles of equal number of *stocks* (we do that calculation later in the notebook).

In [6]:

```
D5=earnings_count.quantile(.8)
D1=earnings_count.quantile(.2)
print 'Number of earnings announcements on the top 20%% announcement days: %d' % D5
print 'Number of earnings announcements on the bottom 20%% announcement days: %d' % D1
```

In [7]:

```
no_ann_dates=list(set(pipe_output.unstack().index).symmetric_difference(earners.unstack().index))
no_earnings=pd.Series(np.zeros(len(no_ann_dates)),index=no_ann_dates)
earnings_count=pd.concat([earnings_count,no_earnings])
earnings_count.sort_index().plot()
plt.ylabel("Number of Earnings Announcements")
```

Out[7]:

`idx`

below) and see how many announcements that corresponds to. We do the same for the top quintile.

In [8]:

```
earnings_count_sort=earnings_count.sort_values()
earnings_count_sort.index=range(len(earnings_count))
earnings_count_cumsum=earnings_count_sort.cumsum()
earnings_count_cumsum.index=range(len(earnings_count))
# For top quintile
p=.8
cutoff=p*earnings_count_sort.sum()
idx=(earnings_count_cumsum-cutoff).abs().idxmin()
N5=earnings_count_sort[idx]
# For bottom quintile
p=.2
cutoff=p*earnings_count_sort.sum()
idx=(earnings_count_cumsum-cutoff).abs().idxmin()
N1=earnings_count_sort[idx]
print 'Number of earnings announcements for the top 20%% companies: %d' % N5
print 'Number of earnings announcements for the bottom 20%% companies: %d' % N1
```

**Earnings Beat and Miss Quintiles**

To get the earnings beat and miss quintile cutoffs, we'll download Zacks data for the last four quarters (again, this may take a while, and you may need to shutdown some open notebooks). We define the earnings surprise, or forecast error $FE$ as $$FE=\frac{E-F}{P}$$ where $E$ is the actual earnings, $F$ is the consensus forecast, and $P$ is the stock price at the time of the earnings announcement.

In [9]:

```
pipe = Pipeline()
earnings_beat_normalized=((EarningsSurprises.eps_act.latest-EarningsSurprises.eps_mean_est.latest)/
USEquityPricing.close.latest)
pipe.add(earnings_beat_normalized, "earnings_beat_normalized")
earn_today=(BusinessDaysSincePreviousEarnings().eq(0))
pipe.set_screen(Q500US() & EarningsSurprises.eps_mean_est.latest.notnan() & earn_today)
FE_output = run_pipeline(pipe, start_date='2015-10-01',end_date='2016-09-30')
FE_output[:20]
```

Out[9]:

We can simply compute the desired quintiles of $FE$:

In [10]:

```
E5=FE_output['earnings_beat_normalized'].quantile(.8)
E1=FE_output['earnings_beat_normalized'].quantile(.2)
print 'The top 20%% earnings beat is: %.5f' % E5
print 'The bottom 20%% earnings beat is: %.5f' % E1
```

In [ ]:

```
```