Overall Statistics
Total Trades
132
Average Win
1.35%
Average Loss
-0.63%
Compounding Annual Return
282.013%
Drawdown
7.400%
Expectancy
1.229
Net Profit
54.825%
Sharpe Ratio
8.374
Probabilistic Sharpe Ratio
99.852%
Loss Rate
29%
Win Rate
71%
Profit-Loss Ratio
2.14
Alpha
2.117
Beta
0.052
Annual Standard Deviation
0.255
Annual Variance
0.065
Information Ratio
5.958
Tracking Error
0.287
Treynor Ratio
40.872
Total Fees
$0.00
from QuantConnect import *
from QuantConnect.Data import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from System.Collections.Generic import List


class EmaCrossUniverseSelectionAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020,9,30)  #Set Start Date
        self.SetCash(100000)         #Set Strategy Cash
        self.resolution = Resolution.Daily
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddEquity("SPY", Resolution.Daily)
        self.SetBrokerageModel(BrokerageName.Alpaca)
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(0.2))
        self.momentumpick_portfolio_percentage = 0.10
        self.stocks = []
        self.universeCount = 10
        self.averages = {}
        self.SetUniverseSelection(FineFundamentalUniverseSelectionModel(self.CoarseSelectionFunction, self.FineSelectionFunction))
        self.Schedule.On(self.DateRules.EveryDay(),self.TimeRules.AfterMarketOpen("SPY",30),self.trade_stocks)
        self.Schedule.On(self.DateRules.EveryDay(),self.TimeRules.AfterMarketOpen("SPY",360),self.trade_stocks)

    
    def CoarseSelectionFunction(self, coarse):
        self.exit_stocks = []
        hasfundamental = [x for x in coarse if x.HasFundamentalData ]
        filtered = [ x for x in hasfundamental if x.Price > 1 and x.Volume > 5000000]
        sortedByDollarVolume = sorted(filtered, key=lambda x: x.Volume,reverse=True)[:300]

        for security in sortedByDollarVolume:
            if security.Symbol not in self.averages:
                self.averages[security.Symbol] = SymbolData(security.Symbol, self)
                self.history = self.History(security.Symbol, 30)
                if not self.history.empty:
                    self.averages[security.Symbol].WarmUpIndicators(self.history)
            self.averages[security.Symbol].update(security.EndTime, security.Price, security.Volume)

        values = list(filter(lambda x: x.is_uptrend, self.averages.values()))
        values = sorted(values, key=lambda x: x.scale)
        return [ x.symbol for x in values]
        

    def FineSelectionFunction(self,fine):
        sortedByROE = sorted(fine, key = lambda x: x.OperationRatios.ROE.OneYear)
        if len(sortedByROE)>1:
            self.stocks = [x for x in sortedByROE[-self.universeCount:]]
            return [x.Symbol for x in sortedByROE[-self.universeCount:]]
        return []
            
            
    def trade_stocks(self):

        if self.Portfolio.Invested:
            invested = [x.Key for x in self.Portfolio if x.Value.Invested]
            for security in invested:
                security_in_portfolio = self.Portfolio[security]
                profitPercentage = security_in_portfolio.UnrealizedProfitPercent
                if profitPercentage > 0.10:
                    self.Liquidate(security, "---Take Profit: " + str(profitPercentage*100))
                    break
                if profitPercentage < -0.05:
                    self.Liquidate(security, "---Take Loss: " + str(profitPercentage*100))
                    break
                
        
        for security in self.stocks:
            if 'SPY' in str(security.Symbol): continue
            if self.Portfolio.Cash > 5000:
                self.SetHoldings(security.Symbol, self.momentumpick_portfolio_percentage)

        if len(self.stocks) > 0:
            self.stocks = []
                
                    
# class used to improve readability of the coarse selection function
class SymbolData:
    def __init__(self, symbol, algo):
        self.algo = algo
        self.symbol = symbol
        self.is_uptrend = False
        self.scale = 0
        self.resolution = Resolution.Daily
        self.priceSMA = SimpleMovingAverage(5)
        self.volSMA = SimpleMovingAverage(5)
        self.priceWin = RollingWindow[float](5)


        #direcitonal indicators
        self._macdHistogram = RollingWindow[float](12)
        # Creates a Rolling Window indicator to keep the 2 TradeBar
        self._bbupwindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        self._bbmidwindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        self._bblowindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        
        self._keltnerupwindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        self._keltnermidwindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        self._keltnerlowindow = RollingWindow[Decimal](12)    # For other security types, use QuoteBar
        self._mom = RollingWindow[float](12)
        
        self.stock = self.algo.AddEquity(self.symbol,self.resolution)

        self._macd = self.algo.MACD(self.symbol, 12, 26, 9, MovingAverageType.Exponential, self.resolution)
        self._bb = self.algo.BB(self.symbol, 20, 2,  MovingAverageType.Simple, self.resolution)
        self._keltner = self.algo.KCH(self.symbol, 20, Decimal(1.5), MovingAverageType.Simple, self.resolution)  
        self._mom1 = self.algo.MOM(self.symbol, 20, self.resolution)
    
    
    def bullSqueeze(self):
        a = False
        b = False
        c = 3
        d = 3
        
        for num in range(0,c):
            if self._bbupwindow.Samples > c and self._keltnerupwindow.Samples > c and self._bblowindow.Samples > c and self._keltnerlowindow.Samples > c:
                if self._bbupwindow[num] < self._keltnerupwindow[num] and self._bblowindow[num] > self._keltnerlowindow[num]:
                    a = True
                else:
                    return False
            else:
                return False
        if a == True:
            for num in range(1,d):
                if self._mom.Samples > d and self._macdHistogram.Samples > d:
                    if self._macdHistogram[num] < self._macdHistogram[0] and self._mom[num] < self._mom[0]:
                        b = True 
                    else: return False
                else: return False
        return b

    def update(self, time, price, volume):
                    
        if self._macd.IsReady and self._keltner.IsReady and self._bb.IsReady and self.priceSMA.IsReady and self.volSMA.IsReady:
            self.priceWin.Add(price)
            self.priceSMA.Update(time, price)
            self.volSMA.Update(time, volume)
            self._macdHistogram.Add(self._macd.Histogram.Current.Value)
            self._mom.Add(self._mom1.Current.Value)
            self._bbupwindow.Add(self._bb.UpperBand.Current.Value)
            self._bbmidwindow.Add(self._bb.MiddleBand.Current.Value)
            self._bblowindow.Add(self._bb.LowerBand.Current.Value)
            self._keltnerupwindow.Add(self._keltner.UpperBand.Current.Value)
            self._keltnermidwindow.Add(self._keltner.MiddleBand.Current.Value)
            self._keltnerlowindow.Add(self._keltner.LowerBand.Current.Value)
            self.MAprice = self.priceSMA.Current.Value
            self.MAvol = self.volSMA.Current.Value
            self.is_uptrend =  self.bullSqueeze()
            
        if self.is_uptrend:
            self.scale = ((volume - self.MAvol) / ((self.MAvol + volume) / 2.0))
            
                                
    def WarmUpIndicators(self, history):
        for index, row in history.loc[str(self.symbol)].iterrows():
            self.priceWin.Add(row["close"])
            self._macd.Update(index, row["close"])
            self._bb.Update(index, row["close"])
            self.priceSMA.Update(index, row["close"])
            self.volSMA.Update(index, row["volume"])
            self._mom.Add(self._mom1.Current.Value)
            if self._macd.IsReady and self._bb.IsReady and self.priceSMA.IsReady and self.volSMA.IsReady:
                self._macdHistogram.Add(self._macd.Histogram.Current.Value)
                self._bbupwindow.Add(self._bb.UpperBand.Current.Value)
                self._bbmidwindow.Add(self._bb.MiddleBand.Current.Value)
                self._bblowindow.Add(self._bb.LowerBand.Current.Value)
                

        for bar in history.itertuples():
            tradeBar = TradeBar(bar.Index[1], bar.Index[0], bar.open, bar.high, bar.low, bar.close, bar.volume, timedelta(1))
            self._keltner.Update(tradeBar)
            if self._keltner.IsReady:
                self._keltnerupwindow.Add(self._keltner.UpperBand.Current.Value)
                self._keltnermidwindow.Add(self._keltner.MiddleBand.Current.Value)
                self._keltnerlowindow.Add(self._keltner.LowerBand.Current.Value)