Backtest Period
1999-2013
Markets Traded
Equities
Maximum Drawdown
Period of Rebalancing
Daily
Return (Annual)
20.9%
Sharpe Ratio
Standard Deviation (Annual)
Original paper
SSRN-id2634875.pdf1176.6KB
Trading rules
- Investment universe: US stocks with market value > $100 million, share price > $2, and Friday earnings announcements
- Create portfolios based on earnings surprise magnitude
- Use empirical SUE distribution from year t-1 to calculate medians of positive and negative SUE values
- Construct portfolios each month in year t:
- Go long on firms with SUE value above positive SUE median
- Go short on firms with SUE value below negative SUE median
- Include firms with positive (negative) SUEs above (below) median in long (short) portfolio at month-end, or next month if announcement is on last trading day
- Maintain stock in portfolio for 12 months, from month t+1 to t+12 (inclusive)
- Equally weight stocks and rebalance portfolio monthly
Python code
Backtrader
import backtrader as bt
import datetime
class EarningsSurpriseStrategy(bt.Strategy):
def __init__(self):
self.positive_sue_median = None
self.negative_sue_median = None
def next(self):
date = self.data.datetime.date()
if date.month != self.prev_month:
# Calculate medians of positive and negative SUE values
positive_sue_list = [d.sue[0] for d in self.datas if d.sue[0] > 0]
negative_sue_list = [d.sue[0] for d in self.datas if d.sue[0] < 0]
self.positive_sue_median = self.calc_median(positive_sue_list)
self.negative_sue_median = self.calc_median(negative_sue_list)
# Rebalance portfolio
long_portfolio = [d for d in self.datas if d.sue[0] > self.positive_sue_median]
short_portfolio = [d for d in self.datas if d.sue[0] < self.negative_sue_median]
for data in self.datas:
position = self.getposition(data).size
if data in long_portfolio:
if position <= 0:
self.buy(data, exectype=bt.Order.Market, size=1)
elif data in short_portfolio:
if position >= 0:
self.sell(data, exectype=bt.Order.Market, size=1)
elif position != 0:
self.close(data, exectype=bt.Order.Market)
self.prev_month = date.month
@staticmethod
def calc_median(values_list):
sorted_list = sorted(values_list)
length = len(sorted_list)
if length % 2 == 0:
median = (sorted_list[length // 2 - 1] + sorted_list[length // 2]) / 2
else:
median = sorted_list[length // 2]
return median
cerebro = bt.Cerebro()
# Add stock data
for stock_data in your_stock_data_list:
if stock_data.market_value > 100e6 and stock_data.share_price > 2 and stock_data.earnings_announcement_on_friday():
cerebro.adddata(stock_data)
# Add strategy
cerebro.addstrategy(EarningsSurpriseStrategy)
# Run backtest
results = cerebro.run()
Please note that this code assumes that you have stock data with the required attributes (market_value, share_price, earnings_announcement_on_friday()) and the SUE value available in your custom DataFeed. You’ll need to adjust the code to work with your specific data source.