Overall Statistics
Total Trades
259
Average Win
2.15%
Average Loss
-0.50%
Compounding Annual Return
99.248%
Drawdown
13.700%
Expectancy
1.124
Net Profit
99.624%
Sharpe Ratio
2.785
Probabilistic Sharpe Ratio
92.091%
Loss Rate
60%
Win Rate
40%
Profit-Loss Ratio
4.29
Alpha
0.616
Beta
0.243
Annual Standard Deviation
0.236
Annual Variance
0.056
Information Ratio
1.584
Tracking Error
0.309
Treynor Ratio
2.697
Total Fees
$333.29
Estimated Strategy Capacity
$1900000.00
Lowest Capacity Asset
CHTR UPXX4G43SIN9
##################################
# 
# SymbolData Class
#
##################################
class SymbolData():
    
    ## Constructor
    ## -----------
    def __init__(self, theSymbol, algo):

        ## Algo / Symbol / Price reference
        self.algo                 = algo
        self.symbol               = theSymbol
        self.lastDailyClose       = None
        self.lastClosePrice       = None
        
        ## Initialize our Indicators and rolling windows
        self.ema                  = ExponentialMovingAverage(self.algo.dailyEMAPeriod)
        self.momentum             = MomentumPercent(self.algo.hourlyMomPeriod)
        self.lastDailyCloseWindow = RollingWindow[float](2)  
        self.emaWindow            = RollingWindow[float](2)  
    
        ## These will hold our 'messages' used in our order notes
        self.ClosePositionMessage = ""
        self.OpenPositionMessage  = ""

    ## Seed Daily indicators with history.
    ## -----------------------------------
    def SeedDailyIndicators(self, dailyHistory):

        # Loop over the history data and update our indicators
        if dailyHistory.empty or 'close' not in dailyHistory.columns:
            # self.algo.Log(f"No Daily history for {self.symbol}")
            return
        
        else:
            for timeIndex, dailyBar in dailyHistory.loc[self.symbol].iterrows():
                if(self.ema is not None): 
                    self.ema.Update(timeIndex, dailyBar['close'])
                    self.lastDailyClose = dailyBar['close']
                    self.timeOflastDailyClose = timeIndex
                    
                    self.lastDailyCloseWindow.Add( dailyBar['close'] )
                    self.emaWindow.Add( self.ema.Current.Value )

    ## Seed intraday indicators with history
    ## These indicators might be have eitehr hourly or minute resolution
    ## -----------------------------------------------------------------
    def SeedIntradayIndicators(self, hourlyHistory=None, minuteHistory=None):
        
        # Loop over the history data and update our indicators
        if hourlyHistory is not None:    
            if hourlyHistory.empty or 'close' not in hourlyHistory.columns:
                self.algo.Log(f"Missing hourly history for {self.symbol}")
            else:
                for timeIndex, hourlyBar in hourlyHistory.loc[self.symbol].iterrows():
                    if(self.momentum is not None): 
                        self.momentum.Update(timeIndex, hourlyBar['close'])

        if minuteHistory is not None:    
            if  minuteHistory.empty or 'close' not in minuteHistory.columns:
                self.algo.Log(f"Missing minute history for {self.symbol}")
            else:
                for timeIndex, hourlyBar in hourlyHistory.loc[self.symbol].iterrows():
                    if(self.someIndicator is not None): 
                        self.someIndicator.Update(timeIndex, hourlyBar['close'])
            return
        
    ## Daily screening criteria. Called by the main algorithm. 
    ## Returns true if daily screening conditions are met. 
    ## Replace with your own criteria.
    ## -------------------------------------------------------
    def DailyScreeningCriteriaMet(self):
    
        ## Price is above ema.
        if(self.lastDailyCloseWindow[0]  > self.emaWindow[0]):
                return True
                
        return False

    ## Intraday screening criteria. Called by the main 
    ## algorithm. Returns true if entry conditions are met. 
    ## Replace with your own criteria.
    ## ----------------------------------------------------
    def IntradayScreeningCriteriaMet(self):

        ## If we have positive momentum
        if (self.momentum.Current.Value > 0):

            ## Informative message that will be submitted as order notes
            self.OpenPositionMessage = f"OPEN (${round(self.lastDailyCloseWindow[0],3)} > EMA: {round(self.emaWindow[0],3)})"
            return True
            
        return False

    ## Trade Exit criteria. Called by the main algorithm.
    ## Returns true if exit conditions are met. 
    ## Replace with your own criteria.
    ## ---------------------------------------------------
    def ExitCriteriaMet(self):

        ## Exit If price is below ema
        if( self.lastClosePrice < self.ema.Current.Value ):
            self.ClosePositionMessage = f"CLOSE (${round(self.lastClosePrice,3)} < EMA: {round(self.ema.Current.Value,3)})"
            return True
        return False

    ## Returns true if our daily indicators are ready.
    ## Called from the main algo
    ## -----------------------------------------------
    def DailyIndicatorsAreReady(self):
        return (self.ema.IsReady and self.lastDailyCloseWindow.IsReady)

    ## Returns true if our intraday indicators are ready.
    ## Called from the main algo
    ## --------------------------------------------------
    def IntradayIndicatorsAreReady(self):
        return (self.momentum.IsReady)
        
    ## Called by the main algorithm right after 
    ## a position is opened for this symbol.
    ## ----------------------------------------
    def OnPositionOpened(self):
        
        ## Register & warmup the indicators we need to track for exits
        self.algo.RegisterIndicator(self.symbol, self.ema, timedelta(1)) 
        self.algo.WarmUpIndicator(self.symbol, self.ema, Resolution.Daily)
        
    ## Called by the main algorithm right after 
    ## a position is closed for this symbol.
    ## ----------------------------------------
    def OnPositionClosed(self):
        # cleanup
        pass
##########################################################################
# Scheduled Intraday Universe Screening
# ---------------------------------------------
# FOR EDUCATIONAL PURPOSES ONLY. DO NOT DEPLOY.
#
# Entry:
# ------
# Daily: At midnight, screen for stocks trading above daily EMA 
# Intraday: In the afternoon, screen those daily stocks for postive momentum
# Open positions for the top 'X' stocks with highest positive momentum
#
# Exit:
# -----
# Exit when price falls below EMA.
# Optionally: exit at End of day if EoDExit flag is set. 
#
# ................................................................
# Copyright(c) 2021 Quantish.io - Granted to the public domain
# Do not remove this copyright notice | info@quantish.io
#########################################################################

from SymbolData import *

class EMAMOMUniverse(QCAlgorithm):
    
    def Initialize(self):
        self.InitBacktestParams()
        self.InitAssets()
        self.InitAlgoParams()
        self.InitUniverse()
        self.ScheduleRoutines()
        
    def InitBacktestParams(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2021, 1, 1)
        self.SetCash(100000)

    def InitAssets(self):
        self.AddEquity("SPY", Resolution.Hour) # benchmark 
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))

    def InitAlgoParams(self):
        self.dailyEMAPeriod  = 10 
        self.hourlyMomPeriod = 4
        self.minsAfterOpen   = 300
        self.useEoDExit      = 0

        self.maxCoarseSelections    = 30
        self.maxFineSelections      = 10
        self.maxIntradaySelections  = 5
        self.maxOpenPositions       = 5

    def InitUniverse(self):
        ## Init universe configuration, selectors 
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.CoarseUniverseSelection, self.FineUniverseSelection) 
        self.EnableAutomaticIndicatorWarmUp = True 
        
        ## Init vars for tracking universe state
        self.symDataDict = { }
        self.screenedDailyStocks = []
        self.screenedIntradayStocks = []
        self.queuedPositions        = []


    ## Schedule screening and liquidation routines, as needed.
    ## -------------------------------------------------------
    def ScheduleRoutines(self):
        ## Intraday selection
        self.Schedule.On(self.DateRules.EveryDay(), 
                         self.TimeRules.AfterMarketOpen("SPY", self.minsAfterOpen), 
                         self.IntraDaySelection)         
        
        ## End of Day Liquidation
        if(self.useEoDExit):
            self.Schedule.On(self.DateRules.EveryDay(), 
                             self.TimeRules.BeforeMarketClose("SPY", 2), 
                             self.LiquidateAtEoD)         

    ## ----------------------
    def LiquidateAtEoD(self):
         self.Liquidate(tag="EoD liquidatation")
        
    ## Process our queued positions, check for exits for held positions
    ## ----------------------------------------------------------------
    def OnData(self, dataSlice):
        
        self.ProcessQueuedPositions()        

        for symbol in dataSlice.Keys:
            if symbol in self.symDataDict:
                symbolData = self.symDataDict[symbol] 

                if( (symbol in dataSlice) and (dataSlice[symbol] is not None)):
                    symbolData.lastClosePrice = dataSlice[symbol].Close
                    if( symbolData.ExitCriteriaMet() ):
                        self.Liquidate(symbol, tag=symbolData.ClosePositionMessage)
                        # del self.symDataDict[symbol]
                        self.RemoveSecurity(symbol)
    
    
    ## Check if we are already holding the max # of open positions.
    ## ------------------------------------------------------------
    def PortfolioAtCapacity(self):
        numHoldings = len([x.Key for x in self.Portfolio if x.Value.Invested])
        return ( numHoldings >= self.maxOpenPositions )
        
        
    ## Coarse universe selection. Replace with your own universe filters.
    ## ------------------------------------------------------------------
    def CoarseUniverseSelection(self, universe): 
        
        if (self.PortfolioAtCapacity()):        
            return []
        else:
            coarseuniverse = sorted(universe, key=lambda c: c.DollarVolume, reverse=True)  
            coarseuniverse = [c for c in coarseuniverse if c.Price > 50][:self.maxCoarseSelections]
            return [x.Symbol for x in coarseuniverse]

    ## Fine universe selection. Replace with your own universe filters.
    ## ----------------------------------------------------------------
    def FineUniverseSelection(self, universe):
        if (self.PortfolioAtCapacity()):        
            return []    
        else:
            fineUniverse = [x for x in universe if x.SecurityReference.IsPrimaryShare
                                    and x.SecurityReference.SecurityType == "ST00000001"
                                    and x.SecurityReference.IsDepositaryReceipt == 0
                                    and x.CompanyReference.IsLimitedPartnership == 0]
            
            
            ## Fetch the stocks that match our daily screening criteria
            screenedStocks = self.GetDailyScreenedStocks(fineUniverse)
            self.screenedDailyStocks = screenedStocks[:self.maxFineSelections]  
            
            ## NOTE:
            ## If you plan to do intraday selection, then 
            ## return a blank array otherwise, comment out this line
            ## ...........................................................
            return []
            
            ## NOTE: 
            ## If there is no need for intraday selection, then 
            ## uncomment the below line to return daily screened stocks
            ## ...........................................................
            ## return self.screenedDailyStocks
    
        

    ## Intraday universe selection. Replace with your own Criteria.
    ## ------------------------------------------------------------
    def IntraDaySelection(self):

        if (not self.PortfolioAtCapacity()):

            ## Fetch the stocks that meet intraday screening criteria
            screenedStocks      = self.GetIntraDayScreenedStocks(self.screenedDailyStocks)
            
            ## Get the symboldata for the screened stocks, and rank by momentum  
            screenedStockData   = [ self.symDataDict[symbol] for symbol in screenedStocks]    
            screenedDataSorted = sorted(screenedStockData, key=lambda x: x.momentum, reverse=True)  
            screenedSymbolsSorted = [ stockData.symbol for stockData in screenedDataSorted ] 
            
            self.screenedIntradayStocks = screenedSymbolsSorted[:self.maxIntradaySelections]
    
            for stock in self.screenedIntradayStocks:
                self.AddSecurity(SecurityType.Equity, stock, Resolution.Minute)
    
        self.screenedDailyStocks = []
    
    ## Screen the given array of stocks for those matching *Daily* 
    ## screening criteria, and return them. 
    ##
    ## Seeds the stock symboldata class with daily history, and then
    ## calls the class's DailyScreeningCriteriaMet() method, where
    ## the actual daily screening logic lives (eg: indicator checks).
    ## --------------------------------------------------------------
    def GetDailyScreenedStocks(self, stocksToScreen):

        screenedStocks = []
        
        for stock in stocksToScreen:  
            symbol = stock.Symbol

            ## If we are already invested in this, skip it
            if (symbol in self.Portfolio) and (self.Portfolio[symbol].Invested):
                continue
            else:
                ## Store data for this symbol in our dictionary, seed it with some history     
                if symbol not in self.symDataDict:
                    self.symDataDict[symbol] = SymbolData(symbol, self) 
                    
                symbolData = self.symDataDict[symbol]
                
                ## we need at least 2 values for EMA for our 
                ## first signal, so we get the required history + 1 day 
                dailyHistory = self.History(symbol, self.dailyEMAPeriod+1, Resolution.Daily)
                
                ## Seed daily indicators so they can be calculated
                symbolData.SeedDailyIndicators(dailyHistory)
                    
                ## If the daily screening criteria is met, we return it
                if symbolData.DailyIndicatorsAreReady(): 
                    if symbolData.DailyScreeningCriteriaMet():
                        screenedStocks.append(symbol)
                    else:
                        ## if the criteria isnt met, we dont need this symboldata
                        del self.symDataDict[symbol]

        return screenedStocks
        
    ## Screen the given array of stocks for those matching *Intraday* 
    ## screening criteria, and return them. 
    ##
    ## Seeds the stock symboldata class with intraday history, and then
    ## calls the class's IntradayScreeningCriteriaMet() method, where
    ## the actual intraday screening logic lives (eg indicator checks).
    ## --------------------------------------------------------------
    def GetIntraDayScreenedStocks(self, stocksToScreen):

        screenedStockSymbols = []
        
        ## loop through stocks and seed their indicators
        for symbol in stocksToScreen:  

            ## If we are already invested in this, skip it
            if (symbol in self.Portfolio) and (self.Portfolio[symbol].Invested):
                continue
            else:
                if( symbol in self.symDataDict ):
                    symbolData = self.symDataDict[symbol]
                    history = self.History(symbol, self.hourlyMomPeriod, Resolution.Hour)
    
                    symbolData.SeedIntradayIndicators(history)
                    if( symbolData.IntradayIndicatorsAreReady() ):
                        if ( symbolData.IntradayScreeningCriteriaMet() ):
                            screenedStockSymbols.append( symbolData.symbol )
                        else:
                            ## if the criteria isnt met, we dont need this symboldata
                            del self.symDataDict[symbol]                    
                else:
                    self.Log(f"- - - - No symdata for {symbol}")
                    
        return screenedStockSymbols
      
      
    ## Called when we add/remove a security from the algo  
    ## --------------------------------------------------
    def OnSecuritiesChanged(self, changes):
        
        ## The trade actually takes place here, when the symbol
        ## passes all our screenings, and we call AddSecurity
        if (not self.PortfolioAtCapacity()):
            for security in changes.AddedSecurities:
                if(security.Symbol != "SPY"):
                    
                    # if we havent already queued this position, queue it. 
                    ## we queue it instead of opening the position, because
                    ## we dont yet have data for this symbol. We will get 
                    ## data for it in the next call to OnData, where we do
                    ## open the position
                    if( security.Symbol not in self.queuedPositions ):
                        self.queuedPositions.append(security.Symbol)
                        
        for security in changes.RemovedSecurities:
            if(security.Symbol != "SPY"):            
                if security.Symbol in self.symDataDict:
                    symbol = security.Symbol
                    symbolData = self.symDataDict[symbol]
                    symbolData.OnPositionClosed()
                    
                    ## remove this sumbol from our local cache
                    del self.symDataDict[symbol] 
                    self.screenedDailyStocks = [ x for x in self.screenedDailyStocks if x != symbol]                    
                    self.screenedIntradayStocks = [ x for x in self.screenedIntradayStocks if x != symbol]                    
        
        
    ## Loop through queued positions and open new trades  
    ## -------------------------------------------------
    def ProcessQueuedPositions(self):
        for symbol in list(self.queuedPositions):
            if self.CurrentSlice.ContainsKey(symbol) and self.CurrentSlice[symbol] is not None:
                symbolData = self.symDataDict[symbol]
                
                ## extra check to make sure we arent going above capacity
                if(not self.PortfolioAtCapacity()):
                    self.SetHoldings(symbol, 1/self.maxOpenPositions, tag=symbolData.OpenPositionMessage)
                    symbolData.OnPositionOpened()
                self.queuedPositions.remove(symbol)