
“该策略根据利率变化交易美国股票,目标是股息收益率十分位,在高股息和低股息股票之间切换,并每月重新平衡以获得最佳表现。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 股息、利率
I. 策略概要
该策略侧重于CRSP数据库中的美国股票,根据联邦基金利率的一年期变化来识别利率上升或下降的时期。股票按股息收益率分为十等份。在利率下降时期,该策略做多高股息股票(前十等份)并做空低股息股票(后十等份)。在利率上升时期,该策略反向操作,做多低股息股票并做空高股息股票。投资组合采用等权重,每月重新平衡,以适应利率变化,利用不同经济条件下的股息收益率动态。
II. 策略合理性
该研究探讨了投资者行为偏差如何影响投资组合选择,特别是在低利率时期。理性投资者对收入和资本利得应该无动于衷,但许多人遵循一种经验法则,即靠投资收入生活,同时保留本金。这导致了“追求收入”现象,即当存款和债券的利息收入不足以满足消费需求时,投资者转向高收益资产。通过股票持有量和共同基金流向的数据,研究发现在降息期间高收益资产有显著流入,而在加息期间有流出,这影响了基于股息政策的公司估值。一个理论模型演示了这种行为如何影响货币政策、消费、投资组合选择和资产价格。低利率导致高估的股息股票,突出了金融部门在传导货币政策效应方面的作用及其对经济动态的更广泛影响。
III. 来源论文
Monetary Policy and Reaching for Income [点击查看论文]
- 肯特·丹尼尔(Kent Daniel)、洛伦佐·加拉皮(Lorenzo Garlappi)、肖凯荣(Kairong Xiao)
<摘要>
我们利用个人投资组合持有数据和共同基金流向数据发现,低利率导致对高股息股票和高收益债券等创收资产的需求显著增加。我们认为这种“追求收入”现象是由遵循“靠收入生活”经验法则的投资者驱动的。我们的实证分析表明,这种对当前收入的偏好影响着家庭投资组合选择和创收资产的价格。此外,我们还探讨了追求收入对资本配置和货币政策有效性的影响。


IV. 回测表现
| 年化回报 | 7.34% |
| 波动率 | 22.23% |
| β值 | -0.014 |
| 夏普比率 | 0.33 |
| 索提诺比率 | -0.284 |
| 最大回撤 | N/A |
| 胜率 | 50% |
V. 完整的 Python 代码
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"))