Quant BuffetRelax, Not Over Thinking

Net Payout Yield Strategy

Log in to collect

Academic paper

Strategy in a nutshell

The investment universe comprises NYSE, AMEX, and NASDAQ stocks. Annually in June, ten portfolios are created based on net payout yield rankings. Net payout yield is dividends plus repurchases minus share issuances divided by year-end market capitalization. Two measures of payout yield are used: one from cash flow statements, the other from Treasury stock changes. We employ the cash flow-based repurchase measure for net payout yield. The top net payout yield portfolio is purchased and held for a year, then rebalanced.

Economic rationale

Traditionally, dividends were seen as reliable indicators of a stock's health, but recent research challenges this notion, indicating the need for alternative predictors. Previously, high dividend yields signaled undervaluation relative to cash flows returned to shareholders. Adjusting this logic to current conditions, net payout yield incorporates various cash distribution methods. Studies show a stronger correlation between total payout yield and returns compared to dividend yield and returns. Fama-MacBeth regressions confirm this, revealing insignificant associations between dividend yields and returns, while payout measures exhibit significant associations. Payout yields maintain consistent predictive power across time periods and demonstrate significant out-of-sample predictability, unlike dividend yields. Trading strategies based on payout yields generate profitable portfolios with negative market betas and size factor loadings, indicating unexplained returns beyond conventional risk metrics.

Backtest performance

Annualised return22.13%
Beta0.885
Sharpe ratio0.355
Sortino ratio0.371
Maximum drawdown52.6%
Win rate82%

Full Python code

from AlgoLib import *
import numpy as np

class NetPayoutYieldEffect(XXX):

def Initialize(self):
self.SetStartDate(2000, 1, 1)  
self.SetCash(100_000) 

self.UniverseSettings.Leverage = 5
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0

# Fundamental Filter Parameters
self.exchange_codes = ['NYS', 'NAS', 'ASE']
self.fundamental_count = 500
self.quantile = 10

self.long_symbols = []

self.rebalancing_month = 6
self.selection_flag = True

self.exchange = self.AddEquity('SPY', Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthEnd(self.exchange), 
                self.TimeRules.AfterMarketOpen(self.exchange), 
                self.Selection)

def FundamentalFunction(self, fundamental):
if not self.selection_flag:
    return Universe.Unchanged

filtered = [f for f in fundamental if f.HasFundamentalData
            and f.SecurityReference.ExchangeId in self.exchange_codes
            and not np.isnan(f.MarketCap)
            and f.MarketCap !=0
            and not np.isnan(f.ValuationRatios.TotalYield)
            and f.ValuationRatios.TotalYield != 0
            and not np.isnan(f.FinancialStatements.CashFlowStatement.CommonStockIssuance.TwelveMonths)
            and f.FinancialStatements.CashFlowStatement.CommonStockIssuance.TwelveMonths != 0]

top_by_dollar_volume = sorted(filtered, 
                                key=lambda x: x.DollarVolume, 
                                reverse=True)[:self.fundamental_count]

payout_yield = lambda x: ((x.ValuationRatios.TotalYield * (x.MarketCap)) - \
            (x.FinancialStatements.CashFlowStatement.CommonStockIssuance.TwelveMonths / (x.MarketCap)))

sorted_by_payout = sorted(top_by_dollar_volume, key = payout_yield, reverse=True)

if len(sorted_by_payout) >= self.quantile:
    quantile = int(len(sorted_by_payout) / self.quantile)
    self.long_symbols = [x.Symbol for x in sorted_by_payout[:quantile]]

return self.long_symbols

def OnData(self, slice):
if not self.selection_flag:
    return
self.selection_flag = False

# Trade Execution
portfolio = [PortfolioTarget(symbol, 1 / len(self.long_symbols)) 
                                    for symbol in self.long_symbols 
                                    if slice.ContainsKey(symbol) and slice[symbol] is not None]

self.SetHoldings(portfolio, True)
self.long_symbols.clear()

def Selection(self):
if self.Time.month == self.rebalancing_month:
    self.selection_flag = True

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())

# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))