Overall Statistics
Total Orders
9994
Average Win
0.43%
Average Loss
-0.39%
Compounding Annual Return
25.082%
Drawdown
51.700%
Expectancy
0.090
Start Equity
100000
End Equity
462023.50
Net Profit
362.024%
Sharpe Ratio
0.719
Sortino Ratio
0.767
Probabilistic Sharpe Ratio
20.774%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.09
Alpha
0.12
Beta
0.694
Annual Standard Deviation
0.249
Annual Variance
0.062
Information Ratio
0.414
Tracking Error
0.228
Treynor Ratio
0.258
Total Fees
$32160.26
Estimated Strategy Capacity
$9000.00
Lowest Capacity Asset
SBI R735QTJ8XC9X
Portfolio Turnover
99.98%
#region imports
from AlgorithmImports import *
#endregion
# https://quantpedia.com/strategies/trading-on-the-dividend-paydate/
#
# The investment universe consists of stocks from NYSE, AMEX and NASDAQ that offer company-sponsored DRIPs. 
# Each day at close investors buy stocks which have dividend payday on the next working day and hold these stocks
# for one day. Stocks are weighted equally.
#
# QC implementation:

# Can scrape the below websites for info
# Source of drip tickers: http://www.dripdatabase.com/DRIP_Directory_AtoZ.aspx
# Source of dividend data: https://www.nasdaq.com/market-activity/dividends

from datetime import datetime
from pandas.tseries.offsets import BDay

class TradingDividendPaydate(QCAlgorithm):

    def Initialize(self):
        # self.SetStartDate(2010, 1, 1)
        self.SetStartDate(2017, 10, 1)
        # self.SetStartDate(2021, 12, 1)
        # self.SetStartDate(2022, 9, 20)
        # self.SetEndDate(2022, 8, 31)  # Set End Date
        self.SetEndDate(2024, 7, 31)  # Set End Date
        self.SetCash(100000)    

        self.symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
        
        # Store drip tickers.
        # csv_string_file = self.Download('data.quantpedia.com/backtesting_data/economic/drip_tickers.csv')
        csv_string_file = self.Download('https://www.dropbox.com/s/id5bvqweevq3cja/drip_tickers_1.csv?dl=1')
        lines = csv_string_file.split('\r\n')
        self.drip_tickers = [x for x in lines[1:]]
        
        # dividend data
        self.dividend_data = {}  # dict of dicts indexed by paydate date
        self.dividend_tickers = []
        
        # csv_string_file = self.Download('data.quantpedia.com/backtesting_data/economic/dividend_dates.csv')
        # csv_string_file = self.Download('https://www.dropbox.com/s/lgxdzccpsq3yvl8/nasdaq_dividend_dates_4.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/tlq1wty1a7twuhz/nasdaq_dividend_dates_5.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/5puro7dshiu5t2i/nasdaq_dividend_dates_5_short_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/thqtz9br5zhxfvu/nasdaq_dividend_dates_6_short_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/ydby6dber7i4347/dividend_dates_1.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/59frn1ixdzdbzaa/nasdaq_dividend_dates_7.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/8kq73i9qhuc0lvb/dividend_dates_8_short.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/5uuelwq20o46ruc/nasdaq_dividend_dates_9_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/p0y1n9p4cl4opww/nasdaq_dividend_dates_11_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/sdyfvyqymndud3w/nasdaq_dividend_dates_12_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/7urf9v9pc9al4ri/nasdaq_dividend_dates_13_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/dj68d3225os97ee/nasdaq_dividend_dates_14_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/42rlzc3ho76ypz9/nasdaq_dividend_dates_15_copy.csv?dl=1')
        # csv_string_file = self.Download('https://www.dropbox.com/s/3u51mnzipod4e0x/nasdaq_dividend_dates_16_copy.csv?dl=1')
        csv_string_file = self.Download('https://www.dropbox.com/s/svftrjdzxhukyw2/nasdaq_dividend_dates_18_copy.csv?dl=1')

        
        lines = csv_string_file.split('\r\n')
        # lines = csv_string_file.split('\n')
        for line in lines[3:]:  # skip first three comment lines
            if line == '':
                continue
            
            line_split = line.split(';')
            # self.Debug(str(line_split))
            # self.Debug("Before datetime: " + str(line_split[0]))
            ex_div_date = datetime.strptime(line_split[0], "%Y-%m-%d").date()
            # ex_div_date = datetime.strptime(line_split[0], "%Y-%m-%d %H:%M").date()
            # self.Debug("After datetime: " + str(ex_div_date))
            # 1/0
            
            # N stocks -> n*6 properties
            for i in range(1, len(line_split), 6):
                # parse dividend info
                ticker = str(line_split[i])
                # self.Debug("Before datetime: " + str(line_split[i+1]))
                payday = datetime.strptime(line_split[i+1], "%m/%d/%Y").date() if line_split[i+1] != '' else None
                # payday = datetime.strptime(line_split[i+1], "%m/%d/%Y").date() if line_split[i+1] != '' or line_split[i+1] != 'N/A' else None
                # self.Debug(str(payday))
                # self.Debug("After datetime: " + str(payday))
                # 1/0
                
                if payday not in self.dividend_data:
                    self.dividend_data[payday] = {}    
                
                # self.Debug("Before datetime: " + str(line_split[i+2]))
                record_date = datetime.strptime(line_split[i+2], "%m/%d/%Y").date() if line_split[i+2] != '' else None
                # record_date = datetime.strptime(line_split[i+2], "%m/%d/%Y").date() if line_split[i+2] != '' or line_split[i+2] != 'N/A' else None
                # self.Debug(str(record_date))
                # self.Debug("After datetime: " + str(record_date))
                
                # self.Debug("Before datetime: " + str(line_split[i+3]))
                dividend_value = float(line_split[i+3]) if line_split[i+3] != '' else None
                # dividend_value = float(line_split[i+3]) if line_split[i+3] != '' or line_split[i+3] != 'N/A' else None
                # self.Debug(str(dividend_value))
                # self.Debug("After datetime: " + str(dividend_value))
                
                # self.Debug("Before datetime: " + str(line_split[i+4]))
                ann_dividend_value = float(line_split[i+4]) if line_split[i+4] != '' else None
                # ann_dividend_value = float(line_split[i+4]) if line_split[i+4] != '' or line_split[i+4] != 'N/A' else None
                # self.Debug(str(ann_dividend_value))
                # self.Debug("After datetime: " + str(ann_dividend_value))
                
                # self.Debug("Before datetime: " + str(line_split[i+5]))
                try:
                    announcement_date = datetime.strptime(line_split[i+5], "%m/%d/%Y").date() if line_split[i+5] != '' else None
                except:
                    "05/02/2022"
                # announcement_date = datetime.strptime(line_split[i+5], "%m/%d/%Y").date() if line_split[i+5] != '' or line_split[i+5] != 'N/A' else None
                # self.Debug(str(announcement_date))
                # self.Debug("After datetime: " + str(announcement_date))
                # 1/0
                
                # 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.Debug("A")
                
                # store dividend info ticker universe
                self.dividend_tickers.append(ticker)
                # self.Debug("Dividend Tickers: " + str(self.dividend_tickers))

        self.opened_positions = {}
        self.active_universe = []   # selected stock universe
        self.selection_flag = False
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 16), self.Rebalance)

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

    def CoarseSelectionFunction(self, coarse):
        if not self.selection_flag:
            return Universe.Unchanged
        
        self.selection_flag = False
        
        # For Scalable Version
        # sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
        # filtered = [ x.Symbol for x in sortedByDollarVolume if x.Price > 10 and x.DollarVolume > 10000000 ]
        # selected = [x.Symbol for x in sortedByDollarVolume if x.Symbol in self.drip_tickers #x.HasFundamentalData 
                                # and x.Market == 'usa' and x.Price > 5
                                # and x.DollarVolume > 10000000 # My addition to help with scale
                                # and x.Symbol in self.drip_tickers
                                # and x.Symbol in self.dividend_tickers]
        # selected = selected[:2000]
        
        # selected = [x.Symbol for x in sortedByDollarVolume if x.Price > 5
        #             and x.DollarVolume > 10000000
        #             and x.Symbol.Value in self.drip_tickers 
        #             and x.Symbol.Value in self.dividend_tickers]
        
        # Original Version
        selected = [x.Symbol for x in coarse if x.Symbol.Value in self.drip_tickers and x.Symbol.Value in self.dividend_tickers]
        
        return selected

    def FineSelectionFunction(self, fine):
        fine = [x for x in fine if x.MarketCap != 0 and     \
                ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))]
        
        # sorting by market cap
        sorted_by_market_cap = sorted(fine, 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):
        # close opened positions
        if len(self.opened_positions) != 0:
            for symbol, q in self.opened_positions.items():
                self.MarketOnCloseOrder(symbol, -q)
            self.opened_positions.clear()
        
        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.TotalPortfolioValue / len(long)
                for symbol in long:
                    price = self.Securities[symbol].Price
                    if price != 0:
                        q = portfolio_value / price
                        self.MarketOnCloseOrder(symbol, q)
                        self.opened_positions[symbol] = q

    def Selection(self):
        if self.Time.month % 3 == 0:
            self.selection_flag = True

# custom fee model
# class CustomFeeModel(FeeModel):
class CustomFeeModel:
    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,
            payday:datetime, 
            record_date:datetime,
            dividend_value:float,
            ann_dividend_value:float,
            announcement_date:datetime
        ):
        self.ticker:str = ticker
        self.ex_div_date:datetime = ex_div_date
        self.payday:datetime = payday
        self.record_date:datetime = record_date
        self.dividend_value:float = dividend_value
        self.ann_dividend_value:float = ann_dividend_value
        self.announcement_date:datetime = announcement_date