
The strategy trades CRSP stocks, using top 50 dividend payment yields from the past year to go long on the value-weighted market portfolio, rebalancing daily for returns.
ASSET CLASS: ETFs, futures, stocks | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: Dividend Day
I. STRATEGY IN A NUTSHELL
The strategy targets CRSP common stocks (codes 10 and 11) on NYSE, NASDAQ, and AMEX. Daily, it calculates a dividend payment yield—the sum of dividends paid today and yesterday divided by the 252-day average dividend. Stocks in the top 50 daily yields over the past year are included in a long, value-weighted market portfolio, rebalanced daily to exploit high dividend payments as a predictive signal.
II. ECONOMIC RATIONALE
Dividends are largely reinvested into the market, often by mutual funds targeting high-quality assets. This reinvestment creates predictable upward price pressure around dividend paydates. The strategy captures this effect, generating returns consistently over the mid-term. Its robustness across 58 international markets suggests that the dividend payment signal is globally valid and not confined to U.S. equities.
III. SOURCE PAPER
Predictable Price Pressure [Click to Open PDF]
Hartzmark, Samuel M., Boston College – Carroll School of Management; Solomon, David H., Boston College – Carroll School of Management
<Abstract>
We present evidence that stock returns, both at the market level and the individual stock level, can be predicted by the timing of uninformed inflows and outflows of cash that are known in advance. Aggregate dividend payments to investors predict higher value-weighted market returns on the day of payment and the day afterwards, by 13 b.p. for the top five days per year, and 5 b.p. for the top fifty days. This effect holds in the US and internationally. Effects are weaker in months when mutual funds pay out dividends to investors (and so are less likely to reinvest). Industries with greater past exposure to dividend price pressure significantly underperform those with less exposure, consistent with an eventual partial reversal. Predictable selling pressure leads to significantly lower returns after earnings announcements for firms with higher stock compensation. Back of the envelope calculations suggest price multipliers of each dollar invested in the aggregate market ranging from 1.5 to 2.3. These results suggest that predictable price pressure is a widespread result of money flows, rather than an anomaly.


IV. BACKTEST PERFORMANCE
| Annualised Return | 2.43% |
| Volatility | 1.3% |
| Beta | 0.225 |
| Sharpe Ratio | 1.86 |
| Sortino Ratio | -0.115 |
| Maximum Drawdown | N/A |
| Win Rate | 55% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from typing import Dict, List
import data_tools
import json
class PricePressureDuringTopDividendDays(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2012, 1, 1)
self.SetCash(100000)
self.leverage:int = 5
self.symbol = self.AddEquity('VTI', Resolution.Daily).Symbol
self.data:Dict[Symbol, SymbolData] = {}
# dividend data
self.dividend_data:Dict[Dict[datetime.date, DividendInfo]] = {} # dict of dicts indexed by paydate date
self.dividend_tickers:List[str] = []
self.dividend_period:int = 272 # one year of trading days
self.dividend_payments_yield_period:int = 252
self.dividend_paids_yields:DividendPaitYield = data_tools.DividendPaidYield(self.dividend_period, self.dividend_payments_yield_period)
# Data source: https://www.nasdaq.com/market-activity/dividends
dividend_data:str = self.Download('data.quantpedia.com/backtesting_data/economic/dividend_dates.json')
dividend_data_json:Dict[str] = json.loads(dividend_data)
for obj in dividend_data_json:
ex_div_date:datetime.date = datetime.strptime(obj['date'], "%Y-%m-%d").date()
for stock_data in obj['stocks']:
ticker:str = stock_data['ticker']
payday:datetime.date = datetime.strptime(stock_data['PayDate'], '%m/%d/%Y').date()
if payday not in self.dividend_data:
self.dividend_data[payday] = {}
record_date:Union[datetime.date, None] = datetime.strptime(stock_data['RecordDate'], '%m/%d/%Y').date() if 'RecordDate' in stock_data else None
dividend_value:float = stock_data['Div']
ann_dividend_value:float = stock_data['AnnDiv']
announcement_date:Union[datetime.date, None] = datetime.strptime(stock_data['AnnounceDate'], '%m/%d/%Y').date() if 'AnnounceDate' in stock_data else None
# store ticker dividend info to current ex-div date
self.dividend_data[payday][ticker] = data_tools.DividendInfo(ticker, ex_div_date, payday, record_date, dividend_value, ann_dividend_value, announcement_date)
# store dividend info ticker universe
self.dividend_tickers.append(ticker)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.selection_flag = False
self.active_universe = [] # selected stock universe
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes:SecurityChanges):
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# update daily prices
for stock in fundamental:
symbol:Symbol = stock.Symbol
if symbol in self.data:
self.data[symbol].update_close(stock.AdjustedPrice)
# monthly selection
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
selection:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Symbol.Value in self.dividend_tickers and x.Price > 5 \
and x.MarketCap != 0 and ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))]
# select stocks, which have dividends data
selected_symbols = []
# warm up stock prices
for stock in selection:
symbol:Symbol = stock.Symbol
if symbol not in self.data:
self.data[symbol] = data_tools.SymbolData()
self.data[symbol].update_close(stock.AdjustedPrice)
self.data[symbol].update_market_cap(stock.MarketCap)
selected_symbols.append(symbol)
self.active_universe = selected_symbols
# return only stocks, which have ready close and market capitalization
return selected_symbols
def OnData(self, data):
yesterday:datetime.date = self.Time.date() - timedelta(days=1)
# rebalance daily
self.Liquidate()
today_dividends_paid:int = 0
# can't get today's data at 00:00
if yesterday in self.dividend_data:
payday_tickers:List[str] = list(self.dividend_data[yesterday].keys())
dividends_paid:List[float] = [] # storing values of paid dividends for each stock, which has payday
# iterate through currently selected universe
for symbol in self.active_universe:
ticker:str = symbol.Value
# stock has payday today
if ticker in payday_tickers and symbol in self.data:
# get stock's dividend value
dividend_value:float = self.dividend_data[yesterday][ticker].dividend_value
# make sure, there are data for stock's dividend paid value calculation
if not self.data[symbol].is_ready() or not dividend_value:
continue
close:float = self.data[symbol].close
market_cap:float = self.data[symbol].market_cap
# calculate stock's dividend paid value
stock_dividend_paid:float = (dividend_value / close) * market_cap
# store stock's dividend paid value
dividends_paid.append(stock_dividend_paid)
today_dividends_paid:float = sum(dividends_paid)
self.dividend_paids_yields.update_dividends_paid(today_dividends_paid)
# check if one year of daily dividends paid data are ready
if self.dividend_paids_yields.dividends_paid_ready():
# update dividend payments yield
self.dividend_paids_yields.update_dividend_payments_yield()
# check if dividend payments yield data are ready
if self.dividend_paids_yields.dividend_payments_yield_ready():
# check if today dividend payments yield is in top 50 yields for 252 trading days period
trade_flag:bool = self.dividend_paids_yields.check_for_trade()
if trade_flag:
# go long on SPY, because today dividend payment yield is in top 50 yields for 252 trading days period
self.SetHoldings(self.symbol, 1)
def Selection(self):
self.selection_flag = True