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.

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 Return7.34%
Volatility22.23%
Beta-0.014
Sharpe Ratio0.33
Sortino Ratio-0.284
Maximum DrawdownN/A
Win Rate50%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading