
“该投资策略涵盖纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)上市且提供公司赞助的股息再投资计划(DRIPs)的股票。投资者在每个交易日收盘时买入次日支付股息的股票,并持有该股票一天。所有股票采用等权重分配。”
资产类别: 股票 | 地区: 美国 | 周期: 每日 | 市场: 股票 | 关键词: 股息支付日
I. 策略概要
该策略针对纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)上市且提供公司赞助股息再投资计划(DRIPs)的股票。投资者在股息支付日前一天的收盘时买入股票,并持有一天,所有持仓均等权重分配。
II. 策略合理性
不断增长的股息支付日效应与日益减少的市场异常现象形成对比,这归因于对此现象的有限认知和不断增加的羊群效应。这种羊群效应源于股息支付日股息再投资的上升趋势,随着时间的推移,尽管其他异常现象因广为人知而失去意义,但这种效应却被放大。
III. 来源论文
DRIPs与股息支付日效应 [点击查看论文]
- Henk Berkman 和 Paul D. Koch。奥克兰大学商学院。爱荷华州立大学金融系;爱荷华州立大学金融系。
<摘要>
在股息支付日,我们发现显著的正平均异常回报,这些回报在随后的几天内完全逆转。这种股息支付日效应自1970年代以来一直在增强,并且与临时价格压力假说一致。支付日效应集中在有股息再投资计划(DRIPs)的股票中,并且对于股息收益率较高、DRIP参与度较高以及套利限制较大的股票而言,效应更大。随着时间的推移,利用这种行为的交易策略的利润与股息收益率和价差呈正相关,与总体流动性呈负相关。


IV. 回测表现
| 年化回报 | 98.8% |
| 波动率 | 48.37% |
| β值 | 0.542 |
| 夏普比率 | 2.04 |
| 索提诺比率 | 0.942 |
| 最大回撤 | N/A |
| 胜率 | 54% |
V. 完整的 Python 代码
from AlgorithmImports import *
from datetime import datetime
from pandas.tseries.offsets import BDay
from typing import Dict, List
import json
#endregion
class TradingDividendPaydate(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2012, 9, 18)
self.SetCash(100000)
symbol:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
# Store drip tickers.
# Source: http://www.dripdatabase.com/DRIP_Directory_AtoZ.aspx
csv_string_file:str = self.Download('data.quantpedia.com/backtesting_data/economic/drip_tickers.csv')
lines:str = csv_string_file.split('\r\n')
self.drip_tickers:List[str] = [x for x in lines[1:]]
# dividend data
self.dividend_data:Dict = {} # dict of dicts indexed by paydate date
# 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] = DividendInfo(ticker, ex_div_date, payday, record_date, dividend_value, ann_dividend_value, announcement_date)
self.active_universe:List[Symbol] = [] # selected stock universe
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.Schedule.On(self.DateRules.MonthEnd(symbol), self.TimeRules.AfterMarketOpen(symbol), self.Selection)
self.Schedule.On(self.DateRules.EveryDay(symbol), self.TimeRules.BeforeMarketClose(symbol, 16), self.Rebalance)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
selection:List[Fundamental] = [x for x in fundamental if x.Symbol.Value in self.drip_tickers and x.MarketCap != 0 and x.SecurityReference.ExchangeId in self.exchange_codes]
# sorting by market cap
sorted_by_market_cap = sorted(selection, key = lambda x: x.MarketCap, reverse = True)
half = int(len(sorted_by_market_cap) / 2)
# pick lower half
self.active_universe = [x.Symbol for x in sorted_by_market_cap[-half:]]
# pick upper half
# self.active_universe = [x.Symbol for x in sorted_by_market_cap[:half]]
return self.active_universe
def Rebalance(self) -> None:
# close opened positions
stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
q_invested:int = self.Portfolio[symbol].Quantity
self.MarketOnCloseOrder(symbol, -q_invested)
day_to_check = (self.Time.date() + BDay(1)).date()
# there are stocks with payday next business day
if day_to_check in self.dividend_data:
payday_tickers = list(self.dividend_data[day_to_check].keys())
long = []
for symbol in self.active_universe:
if symbol.Value in payday_tickers:
long.append(symbol)
if len(long) != 0:
portfolio_value = self.Portfolio.MarginRemaining / len(long)
for symbol in long:
price = self.Securities[symbol].Price
if price != 0:
q = portfolio_value / price
self.MarketOnCloseOrder(symbol, q)
def Selection(self) -> None:
if self.Time.month % 3 == 0:
self.selection_flag = True
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
class DividendInfo():
def __init__(
self,
ticker:str,
ex_div_date:datetime.date,
payday:datetime.date,
record_date:Union[datetime.date, None],
dividend_value:float,
ann_dividend_value:float,
announcement_date:datetime.date
):
self.ticker:str = ticker
self.ex_div_date:datetime.date = ex_div_date
self.payday:datetime.date = payday
self.record_date:Union[datetime.date, None] = record_date
self.dividend_value:float = dividend_value
self.ann_dividend_value:float = ann_dividend_value
self.announcement_date:Union[datetime.date, None] = announcement_date