Original paper
Abstract
This paper explores whether firms manage their earnings after stock splits to meet the raised expectations from the market due to the positive signal sent by the splits. We first document that post-split drift mainly exists in the first three months and is positively associated with post-split standardized unexpected earnings (SUE). However, the higher post-split SUE of split firms is associated with higher discretionary accruals and abnormally lower R&D expenses. This result is consistent with our hypothesis that split firms overstate their post-split earnings by manipulating accruals and reducing R&D spending. Moreover, post-split abnormal returns increase with discretionary accruals and R&D reduction for about six months and tend to reverse over longer horizons, especially for firms with negative pre-split SUE. Overall, our results indicate that the post-split drift is a short-term phenomenon and partly attributable to the earnings management after the splits.
Keywords:Â earnings management, stock split, earnings surprise, post-split drift
Trading rules
- Investment universe: NYSE/AMEX/NASDAQ firms
- Daily check for earnings announcements
- Determine if SUE is in top or bottom quintile (last 3 months)
- Long position:
- Stock split within last 3 months
- SUE in top quintile
- Initiate on 3rd day after earnings announcement
- Short position:
- No stock split within last 3 months
- SUE in bottom quintile
- Initiate on 3rd day after earnings announcement
- Equal weight for each stock
- Hold stocks for 3 months
Python code
Backtrader
import backtrader as bt
import pandas as pd
class PostSplitPEAD(bt.Strategy):
params = (
('sue_lookback', 63), # lookback period for SUE quintiles (3 months)
('holding_period', 63), # holding period for stocks (3 months)
)
def __init__(self):
self.sue = {} # dictionary to store SUE data
self.split_dates = {} # dictionary to store stock split dates
def next(self):
today = self.data.datetime.date(0)
# Check for earnings announcements
for data in self.datas:
symbol = data._name
# Check if there's an earnings announcement today
if self.check_earnings_announcement(symbol, today):
# Calculate the SUE quintiles for the last 3 months
sue_quintiles = self.calculate_sue_quintiles(symbol)
# Long position
if self.sue[symbol][-1] >= sue_quintiles[4] and self.check_stock_split(symbol, today):
if self.getposition(data).size == 0:
self.buy(data=data, exectype=bt.Order.Market, valid=bt.Order.DAY)
self.order_target_percent(data, target=1.0 / len(self.datas))
self.sell(data=data, exectype=bt.Order.Close, valid=bt.Order.DAY + self.params.holding_period)
# Short position
elif self.sue[symbol][-1] <= sue_quintiles[0] and not self.check_stock_split(symbol, today):
if self.getposition(data).size == 0:
self.sell(data=data, exectype=bt.Order.Market, valid=bt.Order.DAY)
self.order_target_percent(data, target=-1.0 / len(self.datas))
self.buy(data=data, exectype=bt.Order.Close, valid=bt.Order.DAY + self.params.holding_period)
def check_earnings_announcement(self, symbol, date):
# Implement earnings announcement check logic here
pass
def calculate_sue_quintiles(self, symbol):
return pd.qcut(self.sue[symbol][-self.params.sue_lookback:], 5, retbins=True, duplicates='drop')[1]
def check_stock_split(self, symbol, date):
split_date = self.split_dates.get(symbol)
if split_date:
days_since_split = (date - split_date).days
if 0 < days_since_split <= self.params.holding_period:
return True
return False
# Implement the data loading and strategy execution code here