Overall Statistics
Total Trades
3094
Average Win
0.64%
Average Loss
-0.62%
Compounding Annual Return
5.280%
Drawdown
33.700%
Expectancy
0.062
Net Profit
63.778%
Sharpe Ratio
0.332
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.04
Alpha
0.016
Beta
0.469
Annual Standard Deviation
0.229
Annual Variance
0.053
Information Ratio
-0.228
Tracking Error
0.232
Treynor Ratio
0.162
Total Fees
$7885.85
class SeasonalitySignalAlgorithm(QCAlgorithm):
    '''
    A strategy that takes long and short positions based on historical same-calendar month returns
    Paper: https://www.nber.org/papers/w20815.pdf
    '''
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)   # Set Start Date
        self.SetEndDate(2019, 8, 1)     # Set End Date
        self.SetCash(100000)            # Set Strategy Cash

        self.num_coarse = 100           # Number of equities for coarse selection
        self.num_long = 5               # Number of equities to long
        self.num_short = 5              # Number of equities to short
        self.longSymbols = []           # Contain the equities we'd like to long
        self.shortSymbols = []          # Contain the equities we'd like to short

        self.UniverseSettings.Resolution = Resolution.Daily     # Resolution of universe selection
        self.AddUniverse(self.SameMonthReturnSelection)         # Universe selection based on historical same-calendar month returns

        self.nextRebalance = self.Time  # Next rebalance time


    def SameMonthReturnSelection(self, coarse):
        '''
        Universe selection based on historical same-calendar month returns
        '''
        # Before next rebalance time, just remain the current universe
        if self.Time < self.nextRebalance:
            return Universe.Unchanged

        # Sort the equities with prices > 5 in DollarVolume decendingly
        selected = sorted([x for x in coarse if x.Price > 5],
                          key=lambda x: x.DollarVolume, reverse=True)

        # Get equities after coarse selection
        symbols = [x.Symbol for x in selected[:self.num_coarse]]

        # Get historical close data for coarse-selected symbols of the same calendar month
        start = self.Time.replace(day = 1, year = self.Time.year-1)
        end = Expiry.EndOfMonth(start) - timedelta(1)
        history = self.History(symbols, start, end, Resolution.Daily).close.unstack(level=0)

        # Get the same calendar month returns for the symbols
        MonthlyReturn = {ticker: prices.iloc[-1]/prices.iloc[0] for ticker, prices in history.iteritems()}

        # Sorted the values of monthly return
        sortedReturn = sorted(MonthlyReturn.items(), key=lambda x:x[1], reverse=True)

        # Get the symbols to long / short
        self.longSymbols = [x[0] for x in sortedReturn[:self.num_long]]
        self.shortSymbols = [x[0] for x in sortedReturn[-self.num_short:]]

        # Note that self.longSymbols/self.shortSymbols contains strings instead of symbols
        return [x for x in symbols if str(x) in self.longSymbols + self.shortSymbols]


    def OnData(self, data):
        '''
        Rebalance every month based on same-calendar month returns effect
        '''
        # Before next rebalance, do nothing
        if self.Time < self.nextRebalance:
            return

        count = len(self.longSymbols + self.shortSymbols)
        # Open long positions
        for symbol in self.longSymbols:
            self.SetHoldings(symbol, 1/count)

        # Open short positions
        for symbol in self.shortSymbols:
            self.SetHoldings(symbol, -1/count)

        # Rebalance at the end of every month
        self.nextRebalance = Expiry.EndOfMonth(self.Time) - timedelta(1)


    def OnSecuritiesChanged(self, changes):
        '''
        Liquidate the stocks that are not in the universe
        '''
        for security in changes.RemovedSecurities:
            if security.Invested:
                self.Liquidate(security.Symbol, 'Removed from Universe')