“该投资策略涵盖纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)上市且提供公司赞助的股息再投资计划(DRIPs)的股票。投资者在每个交易日收盘时买入次日支付股息的股票,并持有该股票一天。所有股票采用等权重分配。”

I. 策略概要

该策略针对纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)上市且提供公司赞助股息再投资计划(DRIPs)的股票。投资者在股息支付日前一天的收盘时买入股票,并持有一天,所有持仓均等权重分配。

II. 策略合理性

不断增长的股息支付日效应与日益减少的市场异常现象形成对比,这归因于对此现象的有限认知和不断增加的羊群效应。这种羊群效应源于股息支付日股息再投资的上升趋势,随着时间的推移,尽管其他异常现象因广为人知而失去意义,但这种效应却被放大。

III. 来源论文

DRIPs与股息支付日效应 [点击查看论文]

<摘要>

在股息支付日,我们发现显著的正平均异常回报,这些回报在随后的几天内完全逆转。这种股息支付日效应自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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读