Notebook

Quantpedia Series: Predicting Earnings Following Buyback Announcements

This research is published in partnership with Quantpedia, an online resource for discovering new trading ideas.

You can view the full Quantpedia series in the library along with other research and strategies.


</a>

Before Proceeding: Click here to import necessary functions


Whitepaper authors: Shahram Amini, Vijay Singal

Whitepaper source: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2589966

Abstract

From Quantpedia:

> The paper states that it is generally accepted that managers have more information about the firm than investors. Given this information asymmetry, managers can make informed decisions about corporate actions such as equity offerings or repurchases. The announcement of stock repurchase or secondary equity offering is voluntary and can be easily moved by a few weeks or months. Therefore timing of SEO or repurchase announcement before earnings announcement could be perceived as important information about future performance of stock during earnings announcement period.

While equity issues are not yet available through Quantopian, I find evidence of earnings predictability similar to the authors with positive returns of 1.11% over a 25-day window (-10, +15) for earnings following a buyback announcement. The results hold true for different time windows (0, +15) and sample selection criteria. However, unlike the authors, I find the highest positive returns for earnings that are (5, 15) days after a buyback announcement. Lastly, because the data used in this notebook is up till present day, it may be useful to view this as an "out-of-sample" validation of the authors' original work.

Introduction

Most financial analysts will agree that managers have informational advantage about their company and this advantage helps them to schedule corporate actions in order to maximize value for shareholders. The authors, Shahram Amini and Vijay Singal, propose that one way managers execute on their informational edge is by timing corporate actions like share buybacks and issues close to the date of an earnings announcement.

>Assuming no predictability, the average market reaction to earnings announcements should not be significantly different from zero.

The authors show that these strategically timed corporate actions result in earnings that still surprise the market. My research finds similar results with a positive raw returns of 1.115% in a (-10, +15) day window for buyback announcements. This earnings predictability is consistent through robustness tests for regulated firms, small cap securities, different return time windows, and from 2010 ~ 2016.

>In contrast to their study, we believe that earnings predictability, if any, must occur in the earnings announcement immediately following equity issues or buyback announcements where the superiority of managerial information is likely to be more evident.

So while share issues weren't analyzed in this study, the authors' research as well as my own suggest positive indicators for earnings predictability following buyback announcements. Using a sample size of 5,900 buybacks from 2007 till 2016, I find an average 1.00% positive return over a (-10, +15) day window with average returns increasing to ~2.00% from 2010 ~ 2016 for earnings reports that have occured 16 ~ 30 days after a buyback announcement. And like the authors, I find this earnings predictability surprising for a number of reasons: First, the positive returns around earnings after a buyback doesn't depend on market reactions to earnings (e.g. earnings surprise, PEAD). Second, if this predictability is significant, it means that the market is not efficiently pricing out the buyback announcement and that effect seems compounded on the earnings announcement.

Table of Contents

You can navigate the rest of this notebook as follows:

  1. Methodology and Sample
  2. Empirical Results and Event Study
  3. Robustness Tests
  4. Strategy Implementation
  5. Conclusion
In [2]:
# Premium Versions
from quantopian.interactive.data.eventvestor import buyback_auth
from quantopian.interactive.data.eventvestor import earnings_calendar as ev_earnings

# Sample Versions
# from quantopian.interactive.data.eventvestor import buyback_auth_free as buyback_auth
# from quantopian.interactive.data.eventvestor import earnings_calendar_free as ev_surprise

fundamentals = init_fundamentals()

# Aggregating the data into yearly slices as well as one full aggregate DataFrame
yearly_buyback_data, all_buyback_data, \
yearly_earnings_data, all_earnings_data = get_yearly_and_all_buyback_earnings_data()

</a>

Section: Methodology and Sample

The methodology behind the study is based on the idea that (1) if a buyback is announced today, the stock's price is expected to increase and (2) if the stock's price increases on the following earnings announcement, it would provide support for market inefficiency surrounding the corporate action.

The data used in this Research Notebook is sourced from EventVestor's Buyback Authorizations and Earnings Calendar Dataset. The sample version is available from 2007 up till 2014 while the premium version is available up till present day.

Sample Statistics

  • Summary statistics
  • Distributions

Summary stats of buyback announcements by year

In [3]:
for i, year in enumerate(yearly_buyback_data):
    df = yearly_buyback_data[year].dropna()
    data = {year: {'N': df.asof_date.count(), 'Median Buybacks as % of SO': df['Percent of SO'].median(),
                   'Median Total Value (Mill)': df['Total Value (Mill)'].median(),
                   'Median Market Cap (Mill)': df['Market Cap (Mill)'].median()}}
    if i == 0:
        stats_df = pd.DataFrame(data)
        total_df = df
    else:
        stats_df[year] = pd.DataFrame(data)
        total_df = total_df.append(df)

total_stats = {'total':{'N': total_df.asof_date.count(),
                        'Median Buybacks as % of SO': total_df['Percent of SO'].median(),
                        'Median Total Value (Mill)': total_df['Total Value (Mill)'].median(),
                        'Median Market Cap (Mill)': total_df['Market Cap (Mill)'].median()}}

stats_df = stats_df.T
stats_df.index = stats_df.index.order()
stats_df.hist()
stats_df = stats_df.T
stats_df['total'] = pd.DataFrame(total_stats)
stats_df.T
Out[3]:
Median Buybacks as % of SO Median Market Cap (Mill) Median Total Value (Mill) N
2007 0.058437 1810.368400 100 465
2008 0.065055 3552.212623 250 464
2009 0.066236 915.327090 75 523
2010 0.061099 995.095274 68 212
2011 0.068235 1974.232460 150 537
2012 0.062207 2059.901700 100 709
2013 0.060919 2260.256510 150 616
2014 0.057979 3456.829800 200 650
2015 0.053581 2536.604400 125 825
2016 0.056468 2471.783318 125 911
total 0.060308 2182.521710 120 5912

Overall, there were 5,900 buybacks studied between 2007 ~ 2016. Firms announced a median buyback of 6.03% during that time as opposed to the 5.34% found in the paper.

Next I look at the distribution of buybacks around earnings announcements.

Distribution of buybacks around earnings announcements

The authors use a window of +/- 30 trading days so to replicate a similar date window, we use a range of 30+/- trading days on each side

In [4]:
bins = range(-30, 35, 5)

days_away = get_days_away(all_buyback_data, all_earnings_data)
days_away = days_away[abs(days_away) <= 30]
ax = days_away.hist(bins=bins, color='r', alpha=.6, label="All Buybacks")

all_buyback_data_filtered = all_buyback_data[all_buyback_data["Percent of SO"] > .05]
days_away = get_days_away(all_buyback_data_filtered, all_earnings_data)
days_away = days_away[abs(days_away) <= 30]
days_away.hist(bins=bins, color='g', alpha=.6, label='Buybacks > 5%')
plt.title("Distribution of Buybacks around Earnings Announcements")
plt.ylabel("Number of Buybacks Announced")
plt.xlabel("Number of Days")
plt.legend()
Out[4]:
<matplotlib.legend.Legend at 0x7efd92822550>

A majority of announcements occur after the earnings announcement for all buybacks announcements. As shown in the Returns section, this often happens after the stock's price performs abnormaly poorly after an earnings report.

For the rest of this study, only buybacks > 5% are looked at. This is (1) to only include important corporate actions and (2) use all buybacks as a criteria in the Robustness test section.

This next section is a closer look at buybacks > 5%.

Distribution of buybacks around earnings announcements (Stats)

In [5]:
"""
Filter out for only buybacks that are > 5%
"""
bins = range(-30, 31, 5)
all_buyback_data_filtered = all_buyback_data[all_buyback_data["Percent of SO"] > .05]
days_away = get_days_away(all_buyback_data_filtered, all_earnings_data)
days_away = days_away[abs(days_away) <= 30]
ax = days_away.hist(bins=bins, color='g', alpha=.6, label='Buybacks > 5%')
plt.title("Distribution of Buybacks around Earnings Announcements")
plt.ylabel("Number of Buybacks Announced")
plt.xlabel("Number of Days")
plt.legend()

"""
Label each buyback as percentage sample count of whole
"""
rects = ax.patches
labels = ["label%d" % i for i in xrange(len(rects))]
cut_bins = np.linspace(days_away.min(), days_away.max(), len(bins))
groups = days_away.groupby(pd.cut(days_away, bins, right=False))
x = groups.count()/days_away.count()
for i, rect in enumerate(rects):
    height = rect.get_height()
    label = "%0.2f%%" % (100*x.iloc[i])
    ax.text(rect.get_x() + rect.get_width()/2, height + 5, label, ha='center', va='bottom')
    
"""
Print a few summary statistics about the buyback auth dataset
"""
data_freq = pd.DataFrame(groups.mean())
data_freq = data_freq.rename(columns={0: "Mean"})
data_freq['Count'] = groups.count()
data_freq['Week'] = (data_freq['Mean']/7).apply(lambda x: int(x))
data_freq['Percentage'] = data_freq['Count']/data_freq['Count'].sum()*100
data_freq.index.name = 'Interval'
data_freq['Cumulative %'] = data_freq.Percentage.cumsum()
data_freq = data_freq.drop('Mean', axis=1)
data_freq.name = "Buybacks"
print "Buybacks Distribution"
data_freq
Buybacks Distribution
Out[5]:
Count Week Percentage Cumulative %
Interval
[-30, -25) 111 -3 4.547317 4.547317
[-25, -20) 93 -3 3.809914 8.357231
[-20, -15) 72 -2 2.949611 11.306841
[-15, -10) 62 -1 2.539943 13.846784
[-10, -5) 79 -1 3.236379 17.083163
[-5, 0) 60 0 2.458009 19.541172
[0, 5) 297 0 12.167145 31.708316
[5, 10) 292 1 11.962311 43.670627
[10, 15) 437 1 17.902499 61.573126
[15, 20) 275 2 11.265875 72.839000
[20, 25) 391 3 16.018025 88.857026
[25, 30) 272 3 11.142974 100.000000

So it seems that the bulk of buybacks tend to occur after an earnings announcement. However, about 22% of announcements are still made in the 30 day window prior to an earnings announcement.

</a>

Section: Results

On both a long (-10, +15) and short (-1, 1) time window, I find positive raw returns similar to what the authors have documented (1.15% and .0046%) looking at firms with earnings that have been announced 16 ~ 30 days after a buyback.

Abnormal returns are much lower for this specific time window at ~0%. One explanation for this difference is that this study looks at a much more recent date range than the original study and the time window for alpha following a corporate action may have shortened since the study because when looking at earnings announcements (5, 15) days after an earnings versus (16, 30) days, the abnormal returns improves significantly.

This initial study shows the returns looking at buybacks greater than 5% but as you'll see in the Robustness section, the returns increase when that criteria is expanded to include all buybacks and different time windows. Specifically, the (5, 15) day criteria for earnings had the highest returns.

The figures below show the cumulative and abnormal returns (assuming risk-free = 0) for both buyback and earnings announcements. Although the focus of this research is earnings, buybacks are shown to highlight the evidence for positive abnormal returns around this corporate aciton.

Returns for Repurchasing Firms

In [6]:
# Find different earnings announcement dependent on length away from buybacks
days_to_study = {
    '16 to 31 days after': [16, 31],
    '16 to 31 days before': [-16, -31]
}
earnings_study_results = {}
buybacks_study_results = {}
data_for_earnings = {}
data_for_buybacks = {}

for name, days in days_to_study.iteritems():
    earnings, buybacks = find_earnings_announcements(all_buyback_data_filtered,
                                                     all_earnings_data,
                                                     days=days)
    data_for_earnings[name] = earnings
    data_for_buybacks[name] = buybacks
    
for name, earnings in data_for_earnings.iteritems():
    print "------------------------------------------------------------------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()
    
for name, buybacks in data_for_buybacks.iteritems():
    print "------------------------------------------------------------------"
    print "Day 0: Buybacks with buyback announcements %s earnings" % name 
    results = custom_event_study(buybacks, end_date='2016-06-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    buybacks_study_results[name] = results
    plt.show()
------------------------------------------------------------------
Day 0: Earnings with buyback announcements 16 to 31 days after earnings
Formatting Data
Getting Plots
/usr/local/lib/python2.7/dist-packages/pandas/core/generic.py:2110: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self[name] = value
/usr/local/lib/python2.7/dist-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.
  warnings.warn(self.msg_depr % (key, alt_key))
Running Event Study
------------------------------------------------------------------
Day 0: Earnings with buyback announcements 16 to 31 days before earnings
Formatting Data
Getting Plots
Running Event Study
------------------------------------------------------------------
Day 0: Buybacks with buyback announcements 16 to 31 days after earnings
Formatting Data
Getting Plots
Running Event Study
------------------------------------------------------------------
Day 0: Buybacks with buyback announcements 16 to 31 days before earnings
Formatting Data
Getting Plots
Running Event Study

The Day 0: Earnings with buyback announcements 16 to 31 days before earnings event study plots show a significant upwards movement in stock price from before the earnings announcement up till 14 days after.

It's interesting to note that when buybacks happened to 16 to 31 days after the earnings announcement, there seems to be a reversal in the stock price which suggests that buybacks are likely announced after a poor market reaction to earnings.

The table below highlights the abnormal and raw returns for earnings announcements 16 ~ 31 days before/after buyback announcements.

Returns around earnings announcement dates

In [7]:
return_intervals = [(0, 1), (-1, 1), (0, 5), (0, 15), (-5, 5), (-10, 15)]

print "Day 0: Earnings Announcement Date"
print "----------------------------------------------------------"
print "Returns of Repurchasing Firms assuming risk-free rate is 0"
print "----------------------------------------------------------"
get_returns_table(earnings_study_results, return_intervals)
Day 0: Earnings Announcement Date
----------------------------------------------------------
Returns of Repurchasing Firms assuming risk-free rate is 0
----------------------------------------------------------
Out[7]:
Abnormal (CAPM) Raw
(-10, 15) (-5, 5) (-1, 1) (0, 1) (0, 5) (0, 15) (-10, 15) (-5, 5) (-1, 1) (0, 1) (0, 5) (0, 15)
16 to 31 days after N=894 -0.0190 -0.0126 -0.0047 -0.0011 -0.0034 -0.0053 -0.0189 -0.0114 -0.0038 -0.0002 -0.0034 -0.0028
16 to 31 days before N=277 -0.0005 -0.0004 0.0032 0.0001 -0.0005 0.0006 0.0115 0.0029 0.0046 0.0008 0.0008 0.0099

So you can see that for buybacks that happened after an earnings announcement, the market's reaction tended to be negative leading up to the buyback and vice versa for buybacks that happened prior.

Section: Robustness Tests

Up till now, my research supports that there is evidence of earnings predictability following stock buyback announcements. This section is dedicated to running my methodology through a series of robustness tests by altering the sample slices of data passed in as inputs.

The first three tests are taken from the original paper, the last two have been included for this notebook:

>1. Regulated firms account for a significant fraction of our sample and were not excluded from the sample used to estimate the main results. Financial firms (SIC codes 6000–6999) may issue equity or repurchase their own stock to meet capital requirements and regulations.

  1. Prior empirical evidence of long-term abnormal performance has found that under- performance of issuing firms is concentrated among the smallest firms (Fama, 1998). In addition, mean returns may be driven by small firms because those firms may experience large negative or positive returns in response to a corporate action.
  2. The above analyses have considered only firms that announced a buyback or SEO pricing 16 to 30 days before or after an earnings announcement. In this robustness test, we consider firms where the announcement of buybacks or SEO pricings occurs 5 to 15 days before or after earnings.

4 . 2007 ~ 2009 were extremely volatile years for the financial markets. In this robustness test, I consider years 2010 ~ 2016 exclusively.<br> 5 . The main study was conducted on buybacks > 5%. This test expands that to include all levels of buybacks.<br> 6 . This final test looks at buybacks from a window range of 1 ~ 30 days before an earnings announcement. I include this in order to maximize the sample size of buyback announcements to increase the frequency of trades (as event-based strategies tend to be on the lower end of turnovers).

Each of these tests will be presented as individual plots documenting before/after earnings announcements similar to the Results section and a summary table is presented towards the end.

In [8]:
robustness_tests = {}
In [9]:
"""
Exclude Financial Firms with SIC codes 6000 - 6999
"""
test_type = "Excluding firms with SIC codes 6000 - 6999"

print "------------------------------------------------------------------"
print test_type

earnings_study_results = {}
data_for_rb_tests = data_for_earnings.copy()

for name, earnings in data_for_rb_tests.iteritems():
    earnings_sic = earnings.apply(lambda row: insert_sic_code(row), axis=1)
    earnings_sic = earnings_sic[(earnings_sic['sic'] < 6000) | (earnings_sic['sic'] > 6999)]

    print "------------------------------------------------------------------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings_sic, end_date='2016-06-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)

"""
Excluding small cap or illiquid securities
"""
test_type = "Excluding small cap and illiquid securities"
print "------------------------------------------------------------------"
print "------------------------------------------------------------------"
print test_type

earnings_study_results = {}
data_for_rb_tests = data_for_earnings.copy()

for name, earnings in data_for_rb_tests.iteritems():
    print "--------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', use_liquid_stocks=True,
                                 days_before=10, days_after=15, top_liquid=1000)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)

"""
Look at buybacks 5 ~ 15 versus 16 ~ 30
"""

days_to_study_two = {
    '5 to 15 days after': [5, 15],
    '5 to 15 days before': [-5, -15]
}
data_for_earnings_two = {}
earnings_study_results = {}

for name, days in days_to_study_two.iteritems():
    earnings, buybacks = find_earnings_announcements(all_buyback_data_filtered,
                                                     all_earnings_data,
                                                     days=days)
    data_for_earnings_two[name] = earnings
    
test_type = "Looking at a 5 ~ 15 day time window"
print "------------------------------------------------------------------"
print "------------------------------------------------------------------"
print test_type

earnings_study_results[name] = results

for name, earnings in data_for_earnings_two.iteritems():
    print "------------------------------------------------------------------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)

"""
Excluding 2007 ~ 2009
"""
test_type = "Excluding 2007 ~ 2009"
print "------------------------------------------------------------------"
print "------------------------------------------------------------------"
print test_type

earnings_study_results = {}
data_for_rb_tests = data_for_earnings.copy()

for name, earnings in data_for_rb_tests.iteritems():
    print "--------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', start_date='2010-01-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)

"""
Including all buybacks, not just buybacks > 5%
"""
test_type = "All buyback announcements"
print "------------------------------------------------------------------"
print "------------------------------------------------------------------"
print test_type

data_for_earnings_three = {}

for name, days in days_to_study.iteritems():
    earnings, buybacks = find_earnings_announcements(all_buyback_data,
                                                     all_earnings_data,
                                                     days=days)
    data_for_earnings_three[name] = earnings

earnings_study_results = {}
data_for_rb_tests = data_for_earnings_three

for name, earnings in data_for_rb_tests.iteritems():
    print "--------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', start_date='2010-01-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)

"""
Look at buybacks 1 ~ 30
"""

days_to_study_four = {
    '1 to 30 days after': [1, 31],
    '1 to 30 days before': [-1, -31]
}
data_for_earnings_four = {}
earnings_study_results = {}

for name, days in days_to_study_four.iteritems():
    earnings, buybacks = find_earnings_announcements(all_buyback_data_filtered,
                                                     all_earnings_data,
                                                     days=days)
    data_for_earnings_four[name] = earnings
    
test_type = "Looking at buybacks 1 ~ 30 days before/after"
print "------------------------------------------------------------------"
print "------------------------------------------------------------------"
print test_type

earnings_study_results[name] = results

for name, earnings in data_for_earnings_four.iteritems():
    print "------------------------------------------------------------------"
    print "Day 0: Earnings with buyback announcements %s earnings" % name 
    results = custom_event_study(earnings, end_date='2016-06-01', use_liquid_stocks=False,
                                 days_before=10, days_after=15)
    earnings_study_results[name] = results
    plt.show()

robustness_tests[test_type] = get_returns_table(earnings_study_results, return_intervals)
------------------------------------------------------------------
Excluding firms with SIC codes 6000 - 6999
------------------------------------------------------------------
Day 0: Earnings with buyback announcements 16 to 31 days after earnings
Formatting Data
Getting Plots
Running Event Study
------------------------------------------------------------------
Day 0: Earnings with buyback announcements 16 to 31 days before earnings
Formatting Data
Getting Plots
Running Event Study
------------------------------------------------------------------
------------------------------------------------------------------
Excluding small cap and illiquid securities
--------
Day 0: Earnings with buyback announcements 16 to 31 days after earnings
Formatting Data
Getting Plots
Running Event Study