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)