
The strategy trades U.S. stocks based on interest rate changes, targeting dividend yield deciles, switching between high- and low-dividend stocks, and rebalancing monthly for optimal performance.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Dividend, Interest Rates
I. STRATEGY IN A NUTSHELL
The strategy trades U.S. stocks based on dividend yield and interest rate trends, going long high-yield stocks in falling-rate periods and reversing in rising-rate periods, rebalanced monthly.
II. ECONOMIC RATIONALE
Investor preference for income during low-rate periods drives demand for high-dividend stocks, influencing valuations and creating predictable return patterns tied to interest rate cycles.
III. SOURCE PAPER
Monetary Policy and Reaching for Income [Click to Open PDF]
Kent Daniel, Lorenzo Garlappi, Kairong Xiao.
<Abstract>
Using data on individual portfolio holdings and on mutual fund flows, we find that low interest rates lead to a significantly higher demand for income-generating assets such as high-dividend stocks and high-yield bonds. We argue that this “reaching for income” phenomenon is driven by investors who follow the rule-of-thumb of “living off income.” Our empirical analysis shows that this preference for current income affects both household portfolio choices and the prices of income-generating assets. In addition, we explore the implications of reaching for income for capital allocation and the effectiveness of monetary policy.


IV. BACKTEST PERFORMANCE
| Annualised Return | 7.34% |
| Volatility | 22.23% |
| Beta | -0.014 |
| Sharpe Ratio | 0.33 |
| Sortino Ratio | -0.284 |
| Maximum Drawdown | N/A |
| Win Rate | 50% |
V. FULL PYTHON CODE
from AlgorithmImports import *
import numpy as np
#endregion
class DividendStocksAndRisingOrFallingInterestRates(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.symbol:Symbol = self.AddData(FEDFUNDS, 'FEDFUNDS', Resolution.Daily).Symbol
self.period:int = 12
self.quantile:int = 10
self.leverage:int = 5
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.fed_fund_rate:RollingWindow = RollingWindow[float](self.period)
self.fundamental_count:int = 3000
self.fundamental_sorting_key = lambda x: x.MarketCap
self.recent_month:int = -1
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
# FED rate data is still comming in
last_update_date:datetime.date = FEDFUNDS.get_last_update_date()
if last_update_date <= self.Time.date():
return Universe.Unchanged
if self.fed_fund_rate.IsReady:
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
x.SecurityReference.ExchangeId in self.exchange_codes and x.Price >= self.min_share_price and \
not np.isnan(x.EarningReports.DividendPerShare.ThreeMonths) and x.EarningReports.DividendPerShare.ThreeMonths != 0
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
dividend_yield:Dict[Symbol, float] = {x.Symbol : x.EarningReports.DividendPerShare.ThreeMonths for x in selected}
if len(dividend_yield) >= self.quantile:
# Stocks are sorted descending by dividend yield
sorted_by_dividend_yield:List[Symbol] = sorted(dividend_yield, key=dividend_yield.get, reverse=True)
quantile:int = len(sorted_by_dividend_yield) // self.quantile
high_dividend_stocks:List[Symbol] = sorted_by_dividend_yield[:quantile] # top decile
low_dividend_stocks:List[Symbol] = sorted_by_dividend_yield[-quantile:] # bottom decile
# identify rising or declining interest rate periods, based on the one-year change in the fed funds rate
interest_rate_rising:bool = self.fed_fund_rate[0] > self.fed_fund_rate[self.period-1]
if interest_rate_rising: # rising period
self.long = low_dividend_stocks
self.short = high_dividend_stocks
else: # declining period
self.long = high_dividend_stocks
self.short = low_dividend_stocks
return self.long + self.short
def OnData(self, data: Slice) -> None:
# Add fed-rates to object in self.data
if self.symbol in data and data[self.symbol]:
price:float = data[self.symbol].Value
self.fed_fund_rate.Add(price)
self.selection_flag = True
return
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution
count_long:int = len(self.long)
count_short:int = len(self.short)
self.Liquidate()
if not (self.Time.year == 2008 and self.Time.month == 6):
for symbol in self.long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / count_long)
for symbol in self.short:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, - 1 / count_short)
self.short.clear()
self.long.clear()
# Source: https://fred.stlouisfed.org/series/T10Y3M
class FEDFUNDS(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource('data.quantpedia.com/backtesting_data/economic/FEDFUNDS.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return FEDFUNDS._last_update_date
def Reader(self, config, line, date, isLiveMode):
data = FEDFUNDS()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
if split[1] != '.':
data.Value = float(split[1])
if data.Time.date() > FEDFUNDS._last_update_date:
FEDFUNDS._last_update_date = data.Time.date()
return data
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
VI. Backtest Performance