Overall Statistics
Total Trades
544
Average Win
2.68%
Average Loss
-1.06%
Compounding Annual Return
41.272%
Drawdown
36.100%
Expectancy
1.573
Net Profit
9415.429%
Sharpe Ratio
1.691
Probabilistic Sharpe Ratio
96.069%
Loss Rate
27%
Win Rate
73%
Profit-Loss Ratio
2.53
Alpha
0
Beta
0
Annual Standard Deviation
0.213
Annual Variance
0.045
Information Ratio
1.691
Tracking Error
0.213
Treynor Ratio
0
Total Fees
$11030.32
Estimated Strategy Capacity
$50000.00
'''
12,321.84 %
PSR 98.875%

Intersection of ROC comparison using OUT_DAY approach by Vladimir v1.3 
(with dynamic selector for fundamental factors and momentum)

inspired by Peter Guenther, Tentor Testivis, Dan Whitnable, Thomas Chang, Miko M, Leandro Maia

Leandro Maia setup modified by Vladimir
https://www.quantconnect.com/forum/discussion/9632/amazing-returns-superior-stock-selection-strategy-superior-in-amp-out-strategy/p2/comment-29437
Changes: STK_MOM is used not only for momenum, but for average dollar volume
'''
from QuantConnect.Data.UniverseSelection import *
import numpy as np
import pandas as pd
import operator
import collections
# --------------------------------------------------------------------------------------------------------
BONDS = ['TLT']; SAFE_BONDS = ['SHY']; VOLA = 126; BASE_RET = 85; STK_MOM = 126; N_COARSE = 100; N_FACTOR = 20; N_MOM = 5; LEV = 1.00; VOLA_FCTR = 0.6;
# --------------------------------------------------------------------------------------------------------

class Fundamental_Factors_Momentum_ROC_Comparison_OUT_DAY(QCAlgorithm):

    def Initialize(self):

        # Dates and cash below changed for PROD
        self.SetStartDate(2008, 1, 1)
        #self.SetEndDate(2009, 12, 13)
       
        self.SetEndDate(2021, 3, 5)
        #self.SetEndDate(2021, 1, 13)
        #self.SetStartDate(2013, 3, 1)
        #self.SetEndDate(2013, 6, 13)
        self.InitCash = 100000
        
        
        self.SetCash(self.InitCash)  
        self.MKT = self.AddEquity("SPY", Resolution.Hour).Symbol
        self.mkt = []
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        res = Resolution.Hour        

        self.BONDS = [self.AddEquity(ticker, res).Symbol for ticker in BONDS]
        self.SAFE_BONDS = [self.AddEquity(ticker, res).Symbol for ticker in SAFE_BONDS]
        self.INI_WAIT_DAYS = 15  
        self.wait_days = self.INI_WAIT_DAYS        

        self.GLD = self.AddEquity('GLD', res).Symbol
        self.SLV = self.AddEquity('SLV', res).Symbol
        self.XLU = self.AddEquity('XLU', res).Symbol
        self.XLI = self.AddEquity('XLI', res).Symbol
        self.UUP = self.AddEquity('UUP', res).Symbol
        self.DBB = self.AddEquity('DBB', res).Symbol
        
        self.pairs = [self.GLD, self.SLV, self.XLU, self.XLI, self.UUP, self.DBB]

        self.bull = 1
        self.bull_prior = 0
        self.count = 0 
        self.outday = (-self.INI_WAIT_DAYS+1)
        self.SetWarmUp(timedelta(350))

        self.UniverseSettings.Resolution = res
        self.AddUniverse(self.CoarseFilter, self.FineFilter)
        self.data = {}
        self.RebalanceFreq = 60
        self.UpdateFineFilter = 0
        self.symbols = None
        self.RebalanceCount = 0
        self.wt = {}
        
        # For Avg Dollar Volume history
        self.averages = { }
        
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 60),  
            self.daily_check) # change to 60 minutes back
        
        symbols = [self.MKT] + self.pairs
        for symbol in symbols:
            self.consolidator = TradeBarConsolidator(timedelta(days=1))
            self.consolidator.DataConsolidated += self.consolidation_handler
            self.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
        
        self.history = self.History(symbols, VOLA, Resolution.Daily)
        if self.history.empty or 'close' not in self.history.columns:
            return
        self.history = self.history['close'].unstack(level=0).dropna()
        
        self.correlationModel = UncorrelatedUniverseSelectionModel(windowLength = 6, historyLength = 9)
        
        
    def consolidation_handler(self, sender, consolidated):
        self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close
        self.history = self.history.iloc[-VOLA:]
        
    def derive_vola_waitdays(self):
        sigma = VOLA_FCTR * np.log1p(self.history[[self.MKT]].pct_change()).std() * np.sqrt(252)
        wait_days = int(sigma * BASE_RET)
        period = int((1.0 - sigma) * BASE_RET)
        return wait_days, period       
        
    def CoarseFilter(self, coarse):
        
        if not (((self.count-self.RebalanceCount) == self.RebalanceFreq) or (self.count == self.outday + self.wait_days - 1)):
            self.UpdateFineFilter = 0
            return Universe.Unchanged
        
        self.UpdateFineFilter = 1
        
        self.correlationModel.SelectCoarse(self, coarse)

        selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)]#[:10]
        
        filterByDollarMomentum = True
        
        if filterByDollarMomentum:
            #https://www.quantconnect.com/terminal/index.php?key=processCache&request=embedded_backtest_9b39116ede741fb39e3eee940b4da720.html
            addedSymbols = [symbol.Symbol for symbol in selected]
            for cf in selected:
                symbol = cf.Symbol
                if cf.Symbol not in self.averages:
                    # First parameter - 21 doesn't seem to make any difference as of Feb 27, 2021
                    self.averages[cf.Symbol] = SymbolDataVolume(cf.Symbol, 21, 5)
                # Updates the SymbolData object with current EOD price - we don't need history for 5 days, result is the same
                avg = self.averages[cf.Symbol]
                avg.update(cf.EndTime, cf.AdjustedPrice, cf.DollarVolume)
            values = list(filter(lambda sd: sd.smaw.Current.Value > 0, self.averages.values()))
            
            values_str = [str(x.symbol) for x in values]
            values.sort(key=lambda x: x.smaw.Current.Value, reverse=True)
    
            # we need to return only the symbol objects
            return [ x.symbol for x in values[:N_COARSE] ]
        else:
            filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True)
        return [x.Symbol for x in filtered[:N_COARSE]]
        
        
        
    def FineFilter(self, fundamental):
        if self.UpdateFineFilter == 0:
            return Universe.Unchanged
        
        use_custom_fine = False
        
        filtered_fundamental = [x for x in fundamental if (x.ValuationRatios.EVToEBITDA > 0)
                                        and (x.EarningReports.BasicAverageShares.ThreeMonths > 0) 
                                        and float(x.EarningReports.BasicAverageShares.ThreeMonths) * x.Price > 2e9
                                        and x.SecurityReference.IsPrimaryShare
                                        and x.SecurityReference.SecurityType == "ST00000001"
                                        and x.SecurityReference.IsDepositaryReceipt == 0
                                        and x.CompanyReference.IsLimitedPartnership == 0]
        # Doesn't make a difference
        # x.FinancialStatements.CashFlowStatement.CommonStockPayments.TwelveMonths >= 0 or <= 0
        # NetCommonStockIssuance.TwelveMonths <= 0
        # NetCommonStockIssuance.ThreeMonths <= 0
        filtered_fundamental = [x for x in filtered_fundamental if x.AssetClassification.MorningstarIndustryGroupCode != MorningstarIndustryGroupCode.DrugManufacturers]
        #filtered_fundamental = [x for x in filtered_fundamental if x.AssetClassification.MorningstarIndustryCode != MorningstarIndustryCode.IntegratedFreightAndLogistics]
        
        if use_custom_fine:
            # https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Reference-Tables
            # sorting: reverse=False means "longing highest",  reverse=True means "longing lowest"
            s1 = sorted(filtered_fundamental, key=lambda x: x.ValuationRatios.EVToEBITDA, reverse=False)                                  # <- added
            s2 = sorted(filtered_fundamental, key=lambda x: x.ValuationRatios.PricetoEBITDA, reverse=False)                               # <- added
            s3 = sorted(filtered_fundamental, key=lambda x: (x.ValuationRatios.PERatio if x.ValuationRatios.PERatio > 0.5 else 1000), reverse=True)                                     # <- added
    
            dict = {}
            for i, elem in enumerate(s1):                                                                                                 # <- added
                i1 = i                                                                                                                    # <- added
                i2 = s2.index(elem)                                                                                                       # <- added
                i3 = s3.index(elem)                                                                                                       # <- added
                score = sum([i1 * 0.6, i2 * 0.1, i3 * 0.3])                                                                            # <- added
                dict[elem] = score                                                                                                        # <- added
            
            top = sorted(dict.items(), key = lambda x: x[1], reverse=True)[:N_FACTOR]                                                     # <- changed
            self.symbols = [x[0].Symbol for x in top]
        else:
            top = sorted(filtered_fundamental, key = lambda x: x.ValuationRatios.EVToEBITDA, reverse=True)[:N_FACTOR]
            self.symbols = [x.Symbol for x in top]
        self.UpdateFineFilter = 0
        self.RebalanceCount = self.count
        return self.symbols

    
    def OnSecuritiesChanged(self, changes):
   
        addedSymbols = []
        for security in changes.AddedSecurities:
            addedSymbols.append(security.Symbol)
            if security.Symbol not in self.data:
                self.data[security.Symbol] = SymbolData(security.Symbol, STK_MOM, self)
   
        if len(addedSymbols) > 0:
            history = self.History(addedSymbols, 1 + STK_MOM, Resolution.Daily).loc[addedSymbols]
            for symbol in addedSymbols:
                try:
                    self.data[symbol].Warmup(history.loc[symbol])
                except:
                    self.Debug(str(symbol))
                    continue
 
        
    def daily_check(self):
        self.wait_days, period = self.derive_vola_waitdays()

        r = self.history.pct_change(period).iloc[-1]
        
        bear = ((r[self.SLV] < r[self.GLD]) and (r[self.XLI] < r[self.XLU]) and (r[self.DBB] < r[self.UUP]))

        if bear:
            self.bull = False
            self.outday = self.count
            
        if (self.count >= self.outday + self.wait_days):
            self.bull = True
            
        self.wt_stk = LEV if self.bull else 0  
        self.wt_bnd = 0 if self.bull else LEV    
        
        if bear:
            self.trade_out()
       
        if (self.bull and not self.bull_prior) or (self.bull and (self.count==self.RebalanceCount)):
            self.trade_in()
                
        self.bull_prior = self.bull
        self.count += 1
        
        
    def trade_out(self):
        try:
            sec = self.BONDS[0]
            correlationWithMKT_arr = self.correlationModel.cache[sec].correlation['A']
            correlationWithMKT = sum(correlationWithMKT_arr)/len(correlationWithMKT_arr)
        except:
            correlationWithMKT = 0
        
        bonds = self.BONDS
        if correlationWithMKT < -0.9:
            bonds = self.SAFE_BONDS
            
            #sec = self.BONDS[0]
            #correlationWithMKT = self.correlationModel.cache[sec].correlation['A'][0]
            #self.Debug('{} {} Will switch to safe bonds, correlation with SPY: {}'.format(self.Time.strftime("%m/%d/%Y %A %H:%M:%S"), str(sec), correlationWithMKT))
            
        for sec in bonds:
            self.wt[sec] = self.wt_bnd/len(bonds)
        
        for sec in self.Portfolio.Keys:
            if sec not in bonds:
                self.wt[sec] = 0
            
        for sec, weight in self.wt.items():
            if weight == 0 and self.Portfolio[sec].IsLong:
                self.Liquidate(sec)
                
        for sec, weight in self.wt.items(): 
            if weight != 0:
                self.SetHoldings(sec, weight)  
                
    def trade_out_old(self):
        for sec in self.BONDS:
            self.wt[sec] = self.wt_bnd/len(self.BONDS)
        
        for sec in self.Portfolio.Keys:
            if sec not in self.BONDS:
                self.wt[sec] = 0
            
        for sec, weight in self.wt.items():
            if weight == 0 and self.Portfolio[sec].IsLong:
                self.Liquidate(sec)
                
        for sec, weight in self.wt.items(): 
            if weight != 0:
                self.SetHoldings(sec, weight)  


    def trade_in(self):
            
        if self.symbols is None: return
        
        output = self.calc_return(self.symbols)
        stocks = output.iloc[:N_MOM].index
        
        for sec in self.Portfolio.Keys:
            if sec not in stocks:                
                self.wt[sec] = 0
        for sec in stocks:
            self.wt[sec] = self.wt_stk/N_MOM
            
        for sec, weight in self.wt.items():             
            self.SetHoldings(sec, weight) 
 
        
    def calc_return(self, stocks):

        ret = {}
        for symbol in stocks:
            try:
                ret[symbol] = self.data[symbol].Roc.Current.Value
            except:
                self.Debug(str(symbol))
                continue
            
        df_ret = pd.DataFrame.from_dict(ret, orient='index')
        df_ret.columns = ['return']
        sort_return = df_ret.sort_values(by = ['return'], ascending = False)
        
        return sort_return
    
        
    def OnEndOfDay(self): 
        
        mkt_price = self.Securities[self.MKT].Close
        self.mkt.append(mkt_price)
        mkt_perf = self.InitCash * self.mkt[-1] / self.mkt[0] 
        self.Plot('Strategy Equity', self.MKT, mkt_perf)     
        
        account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue
        
        self.Plot('Holdings', 'leverage', round(account_leverage, 2))
        self.Plot('Holdings', 'Target Leverage', LEV)
    
        
class SymbolData(object):

    def __init__(self, symbol, roc, algorithm):
        self.Symbol = symbol
        self.Roc = RateOfChange(roc)
        self.algorithm = algorithm
        
        self.consolidator = algorithm.ResolveConsolidator(symbol, Resolution.Daily)
        algorithm.RegisterIndicator(symbol, self.Roc, self.consolidator)
        
    def Warmup(self, history):
        for index, row in history.iterrows():
            self.Roc.Update(index, row['close'])

class SelectionData():
    
    def __init__(self, history):
        self.avgDollarVolume = SimpleMovingAverage(STK_MOM)
        
        for index, row in history.iterrows():
            self.avgDollarVolume.Update(index, row['close'] * row['volume'])
        # for bar in history.itertuples():
        #     timeIndex = 1
        #     self.avgDollarVolume.Update(bar.Index[timeIndex], bar.close * bar.volume)
    
    def is_ready(self):
        return self.avgDollarVolume.IsReady
    
    def update(self, time, price, volume):
        self.avgDollarVolume.Update(time, price * volume)

class SymbolDataVolume(object):
    def __init__(self, symbol, period, periodw):
        self.symbol = symbol
        #self.tolerance = 1.01
        self.tolerance = 0.95
        self.fast = ExponentialMovingAverage(10)
        self.slow = ExponentialMovingAverage(21)
        self.is_uptrend = False
        self.scale = 0
        self.volume = 0
        self.volume_ratio = 0
        self.volume_ratiow = 0
        self.sma = SimpleMovingAverage(period)
        self.smaw = SimpleMovingAverage(periodw)

    def update(self, time, value, volume):
        self.volume = volume

        if self.smaw.Update(time, volume):
            # get ratio of this volume bar vs previous 10 before it.
            if self.smaw.Current.Value != 0:
                self.volume_ratiow = volume / self.smaw.Current.Value
        if self.sma.Update(time, volume):
            # get ratio of this volume bar vs previous 10 before it.
            if self.sma.Current.Value != 0:
                self.volume_ratio = self.smaw.Current.Value / self.sma.Current.Value
            
        if self.fast.Update(time, value) and self.slow.Update(time, value):
            fast = self.fast.Current.Value
            slow = self.slow.Current.Value
            #self.is_uptrend = fast > slow * self.tolerance
            self.is_uptrend = (fast > (slow * self.tolerance)) and (value > (fast * self.tolerance))

        if self.is_uptrend:
            self.scale = (fast - slow) / ((fast + slow) / 2.0)
            
            
            
class UncorrelatedUniverseSelectionModel:
    '''This universe selection model picks stocks that currently have their correlation to a benchmark deviated from the mean.'''

    def __init__(self,
                 benchmark = Symbol.Create("SPY", SecurityType.Equity, Market.USA),
                 tlt = Symbol.Create("TLT", SecurityType.Equity, Market.USA),
                 windowLength = 5,
                 historyLength = 25,
                 threshold = 0.5):
        '''Initializes a new default instance of the OnTheMoveUniverseSelectionModel
        Args:
            benchmark: Symbol of the benchmark
            tlt: TLT
            windowLength: Rolling window length period for correlation calculation
            historyLength: History length period
            threshold: Threadhold for the minimum mean correlation between security and benchmark'''
        

        self.benchmark = benchmark 
        self.tlt = tlt
        self.windowLength = windowLength
        self.historyLength = historyLength
        self.threshold = threshold

        self.cache = dict()
        self.symbol = list()

    def SelectCoarse(self, algorithm, coarse):
        '''Select stocks with highest Z-Score with fundamental data and positive previous-day price and volume'''

        # Verify whether the benchmark is present in the Coarse Fundamental
        benchmark = next((x for x in coarse if x.Symbol == self.benchmark), None)
        if benchmark is None:
            return self.symbol

        # Get the symbols with the highest dollar volume
        coarse = sorted([x for x in coarse if x.Symbol == self.tlt],
                        key = lambda x: x.DollarVolume, reverse=True)
        
        newSymbols = list()
        for cf in coarse + [benchmark]:
            symbol = cf.Symbol
            data = self.cache.setdefault(symbol, self.SymbolData(self, symbol))
            data.Update(cf.EndTime, cf.AdjustedPrice)
            if not data.IsReady:
                newSymbols.append(symbol)

        # Warm up the dictionary objects of selected symbols and benchmark that do not have enough data
        if len(newSymbols) > 1:
            history = algorithm.History(newSymbols, self.historyLength, Resolution.Daily)
            if not history.empty:
                history = history.close.unstack(level=0)
                for symbol in newSymbols:
                    self.cache[symbol].Warmup(history)

        # Create a new dictionary with the zScore
        zScore = dict()
        benchmark = self.cache[self.benchmark].GetReturns()
        for cf in coarse:
            symbol = cf.Symbol
            value = self.cache[symbol].CalculateZScore(benchmark)
            if value > 0: zScore[symbol] = value

        # Sort the zScore dictionary by value
        if len(zScore) > 0:
            sorted_zScore = sorted(zScore.items(), key=lambda kvp: kvp[1], reverse=True)
            zScore = dict(sorted_zScor)
        
        # Return the symbols
        self.symbols = list(zScore.keys())
        return self.symbols


    class SymbolData:
        '''Contains data specific to a symbol required by this model'''
        def __init__(self, model, symbol):
            self.symbol = symbol
            self.windowLength = model.windowLength
            self.historyLength = model.historyLength
            self.threshold = model.threshold
            self.history = RollingWindow[IndicatorDataPoint](self.historyLength)
            self.correlation = None

        def Warmup(self, history):
            '''Save the historical data that will be used to compute the correlation'''
            symbol = str(self.symbol)
            if symbol not in history:
                return

            # Save the last point before reset
            last = self.history[0]
            self.history.Reset()

            # Uptade window with historical data
            for time, value in history[symbol].iteritems():
                self.Update(time, value)

            # Re-add the last point if necessary
            if last.EndTime > time:
                self.Update(last.EndTime, last.Value)

        def Update(self, time, value):
            '''Update the historical data'''
            self.history.Add(IndicatorDataPoint(self.symbol, time, value))

        def CalculateZScore(self, benchmark):
            '''Computes the ZScore'''
            # Not enough data to compute zScore
            if not self.IsReady:
                return 0

            returns = pd.DataFrame.from_dict({"A": self.GetReturns(), "B": benchmark})

            if self.correlation is None:
                # Calculate stdev(correlation) using rolling window for all history
                correlation = returns.rolling(self.windowLength, min_periods = self.windowLength).corr()
                self.correlation = correlation["B"].dropna().unstack()
            else:
                last_correlation = returns.tail(self.windowLength).corr()["B"]
                self.correlation = self.correlation.append(last_correlation).tail(self.historyLength)

            # Calculate the mean of correlation and discard low mean correlation
            mean = self.correlation.mean()
            if mean.empty or mean[0] < self.threshold:
                return 0

            # Calculate the standard deviation of correlation
            std = self.correlation.std()

            # Current correlation
            current = self.correlation.tail(1).unstack()

            # Calculate absolute value of Z-Score for stocks in the Coarse Universe.
            return abs(current[0] - mean[0]) / std[0]

        def GetReturns(self):
            '''Get the returns from the rolling window dictionary'''
            historyDict = {x.EndTime: x.Value for x in self.history}
            return pd.Series(historyDict).sort_index().pct_change().dropna()

        @property
        def IsReady(self):
            return self.history.IsReady