Original paper
Abstract
We demonstrate that sorting stocks on sales seasonality predicts future abnormal returns. A long-short strategy of buying low-sales-season stocks and shorting high-sales-season stocks generates an annual alpha of 8.4%. Further, this strategy has become stronger over time, generating an annual alpha of approximately 15% over the last decade. This seasonal effect predicts future stock returns in cross-sectional regressions, and is independent of previously documented seasonal anomalies. Moreover, the alphas from this trading strategy cannot be explained by differences in stock market liquidity, systematic risk, asymmetric information, or financing decisions. Further tests indicate that this phenomenon may be driven partially by seasonal fluctuations in the level of investor attention.
Keywords:Â Sales Seasonality, Stock Returns, Investor Attention
Trading rules
- Target stocks: Non-financial U.S. stocks on NYSE, AMEX, or NASDAQ using CRSP data (avoiding share codes 10 and 11).
- Every quarter, derive variable SEA[q,t] as: SALES[q,t] / ANNUALSALES[t].
- Determine AVGSEA[q,t]: Average of SEA[q,t] over years t-2 and t-3 (mitigate outlier impact).
- Monthly stock allocation: Rank stocks into ten groups based on the sales seasonality indicator (AVGSEA[q,t] from two years prior).
- Trading approach: Take a long position in the stocks from the lowest decile and a short position in those from the highest decile.
- Portfolio operations: Value-weighted, held for one month, rebalanced monthly.
Python code
Backtrader
Here’s a basic Backtrader Python code snippet for implementing the given trading rules:
import backtrader as bt
import backtrader.feeds as btfeeds
import pandas as pd
class SalesSeasonality(bt.Strategy):
def __init__(self):
self.sea = {}
self.avgsea = {}
def prenext(self):
self.next()
def next(self):
for data in self.datas:
symbol = data._name
if symbol not in self.sea:
self.sea[symbol] = []
if symbol not in self.avgsea:
self.avgsea[symbol] = []
sales = data.sales[0]
annual_sales = data.annualsales[0]
sea = sales / annual_sales
self.sea[symbol].append(sea)
if len(self.sea[symbol]) >= 8:
avgsea = sum(self.sea[symbol][-8:-4]) / 4
self.avgsea[symbol].append(avgsea)
if len(self.avgsea) > 0:
sorted_symbols = sorted(self.avgsea, key=lambda x: self.avgsea[x][-1], reverse=True)
long_symbols = sorted_symbols[:len(sorted_symbols)//10]
short_symbols = sorted_symbols[-len(sorted_symbols)//10:]
for data in self.datas:
symbol = data._name
if symbol in long_symbols:
self.order_target_percent(data, target=1 / len(long_symbols))
elif symbol in short_symbols:
self.order_target_percent(data, target=-1 / len(short_symbols))
else:
self.order_target_percent(data, target=0.0)
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.addstrategy(SalesSeasonality)
# Load your data feed here
# data = btfeeds.PandasData(dataname=your_dataframe)
# cerebro.adddata(data)
cerebro.broker.set_cash(100000)
cerebro.run()
print(f"Final Portfolio Value: {cerebro.broker.getvalue()}")
Replace the ‘your_dataframe’ with your custom data containing columns for sales and annualsales. This code snippet is a starting point and may need some adjustments depending on the data format and other factors.