Overall Statistics
Total Trades
573
Average Win
2.58%
Average Loss
-1.50%
Compounding Annual Return
37.101%
Drawdown
36.300%
Expectancy
0.669
Net Profit
6139.727%
Sharpe Ratio
0.982
Probabilistic Sharpe Ratio
18.616%
Loss Rate
39%
Win Rate
61%
Profit-Loss Ratio
1.72
Alpha
0
Beta
0
Annual Standard Deviation
0.389
Annual Variance
0.151
Information Ratio
0.982
Tracking Error
0.389
Treynor Ratio
0
Total Fees
$11602.40
'''
1.3
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
'''
from QuantConnect.Data.UniverseSelection import *
import numpy as np
import pandas as pd
# --------------------------------------------------------------------------------------------------------
BONDS = ['TLT']; VOLA = 126; BASE_RET = 85; STK_MOM = 126; N_COARSE = 1000; N_FACTOR = 100; N_MOM = 5; LEV = 1.00; 
# --------------------------------------------------------------------------------------------------------

class Fundamental_Factors_Momentum_ROC_Comparison_OUT_DAY(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2008, 1, 1)  
        self.SetEndDate(2021, 2, 1)  
        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.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 = {}
        
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 30), 
            self.daily_check)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 60),
            self.trade)  
        
        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()
        
        
    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 = 0.6 * 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

        selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 3)]
        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
        
        universe_valid = [x for x in fundamental
            if float(x.EarningReports.BasicAverageShares.ThreeMonths) * x.Price > 1e9
            and x.SecurityReference.IsPrimaryShare
            and x.SecurityReference.SecurityType == "ST00000001"
            and x.SecurityReference.IsDepositaryReceipt == 0
            and x.CompanyReference.IsLimitedPartnership == 0
            and x.OperationRatios.ROIC
            and x.OperationRatios.CapExGrowth
            and x.OperationRatios.FCFGrowth
            and x.ValuationRatios.BookValueYield
            and x.ValuationRatios.EVToEBITDA
            and x.ValuationRatios.PricetoEBITDA
            and x.ValuationRatios.PERatio
            and x.ValuationRatios.FCFYield
            ]
        
        returns, volatility, sharpe_ratio = self.get_momentum(universe_valid)
        
        sortedByfactor0 = sorted(universe_valid, key=lambda x: returns[x.Symbol],                        reverse=False) # high return or sharpe or low volatility
        sortedByfactor1 = sorted(universe_valid, key=lambda x: x.OperationRatios.ROIC.OneYear,           reverse=False) # high ROIC
        sortedByfactor2 = sorted(universe_valid, key=lambda x: x.OperationRatios.CapExGrowth.ThreeYears, reverse=False) # high growth
        sortedByfactor3 = sorted(universe_valid, key=lambda x: x.OperationRatios.FCFGrowth.ThreeYears,   reverse=False) # high growth
        sortedByfactor4 = sorted(universe_valid, key=lambda x: x.ValuationRatios.BookValueYield,         reverse=False) # high Book Value Yield
        sortedByfactor5 = sorted(universe_valid, key=lambda x: x.ValuationRatios.EVToEBITDA,             reverse=True)  # low enterprise value to EBITDA
        sortedByfactor6 = sorted(universe_valid, key=lambda x: x.ValuationRatios.PricetoEBITDA,          reverse=True)  # low share price to EBITDA
        sortedByfactor7 = sorted(universe_valid, key=lambda x: x.ValuationRatios.PERatio,                reverse=True)  # low share price to its per-share earnings
        sortedByfactor8 = sorted(universe_valid, key=lambda x: x.ValuationRatios.FCFYield,               reverse=False)

        stock_dict = {}
        for i, elem in enumerate(sortedByfactor0):
            rank0 = i
            rank1 = sortedByfactor1.index(elem)
            rank2 = sortedByfactor2.index(elem)
            rank3 = sortedByfactor3.index(elem)
            rank4 = sortedByfactor4.index(elem)
            rank5 = sortedByfactor5.index(elem)
            rank6 = sortedByfactor6.index(elem)
            rank7 = sortedByfactor7.index(elem)
            rank8 = sortedByfactor8.index(elem)
            score = sum([rank0*1.0, rank1*0.0, rank2*0.0, rank3*0.0, rank4*0.0, rank5*0.0, rank6*0.0, rank7*0.0, rank8*1.0])
            stock_dict[elem] = score
        
        self.sorted_stock_dict = sorted(stock_dict.items(), key=lambda x:x[1], reverse=True)
        sorted_symbol = [x[0] for x in self.sorted_stock_dict]
        
        top  = [x for x in sorted_symbol[:N_FACTOR]]
        self.symbols = [i.Symbol for i in top]
        self.UpdateFineFilter = 0
        self.RebalanceCount = self.count
        return self.symbols
        
        
    def get_momentum(self, universe):
        symbols = [i.Symbol for i in universe]
        hist_df = self.History(symbols, 63, Resolution.Daily)
        returns = {}
        volatility = {}
        sharpe = {}
        for s in symbols:
            ret = np.log( hist_df.loc[str(s)]['close'] / hist_df.loc[str(s)]['close'].shift(1) )
            returns[s] = ret.mean() * 252
            volatility[s] = ret.std() * np.sqrt(252) 
            sharpe[s] = (returns[s] - 0.03) / volatility[s]
        return returns, volatility, sharpe
    
    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 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 daily_check(self):
        self.wait_days, period = self.derive_vola_waitdays()

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

        if self.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    


    def trade(self):
        if self.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):
        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 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'])