Overall Statistics |
Total Trades 1048 Average Win 0.48% Average Loss -0.42% Compounding Annual Return 97.765% Drawdown 21.700% Expectancy 0.589 Net Profit 299.074% Sharpe Ratio 3.087 Probabilistic Sharpe Ratio 98.811% Loss Rate 26% Win Rate 74% Profit-Loss Ratio 1.15 Alpha 0.459 Beta 0.141 Annual Standard Deviation 0.206 Annual Variance 0.043 Information Ratio -1.08 Tracking Error 0.571 Treynor Ratio 4.501 Total Fees $15272.46 Estimated Strategy Capacity $59000000.00 Lowest Capacity Asset BNBUSDT 18N |
################################################### # # Smart Rolling window # ======================== # Convenience object to build on RollingWindow functionality # # Methods: # ------------------------- # mySmartWindow.IsRising() # mySmartWindow.IsFalling() # mySmartWindow.crossedAboveValue(value) # mySmartWindow.crossedBelowValue(value) # mySmartWindow.crossedAbove(otherWindow) # mySmartWindow.crossedBelow(otherWindow) # mySmartWindow.IsFlat(decimalPrecision) # mySmartWindow.hasAtLeastThisMany(value) # # # Author:ekz ################################################### class SmartRollingWindow(): def __init__(self, windowType, windowLength): self.window = None self.winLength = windowLength if (windowType is "int"):self.window = RollingWindow[int](windowLength) elif (windowType is "bool"):self.window = RollingWindow[bool](windowLength) elif (windowType is "float"):self.window = RollingWindow[float](windowLength) elif (windowType is "TradeBar"):self.window = RollingWindow[TradeBar](windowLength) def crossedAboveValue(self, value):return (self.window[1] <= value < self.window[0]) def crossedBelowValue(self, value): return (self.window[1] >= value > self.window[0]) def crossedAbove(self, series): return (self.window[1] <= series[1] and self.window[0] > series[0]) def crossedBelow(self, series): return (self.window[1] >= series[1] and self.window[0] < series[0]) def isAbove(self, series): return (self.window[0] > series[0]) def isBelow(self, series): return (self.window[0] < series[0]) def isFlat(self): return (self.window[1] == self.window[0]) def isFalling(self): return (self.window[1] > self.window[0]) def isRising(self): return (self.window[1] < self.window[0]) def Add(self,value): self.window.Add(value) def IsReady(self): return (self.window is not None) and \ (self.window.Count >= self.winLength) ## TODO: just use rw.IsReady? def __getitem__(self, index): return self.window[index]
########################################################################## # Inspired by @nitay-rabinovich at QuqntConnect # https://www.quantconnect.com/forum/discussion/12768/share-kalman-filter-crossovers-for-crypto-and-smart-rollingwindows/p1/comment-38144 ########################################################################## # # EMA Crossover In a Crypto Universe # --------------------------------------------- # FOR EDUCATIONAL PURPOSES ONLY. DO NOT DEPLOY. # # # Entry: # ------- # Minimum volume threshold traded # and # Price > Fast Daily EMA # and # Fast Daily EMA > Slow Daily EMA # # Exit: # ------ # Price < Slow Daily EMA # or # Slow Daily EMA < Fast Daily EMA # # Additional Consideration: # -------------------------- # Max exposure pct: Total % of available capital to trade with at any time # Max holdings: Total # of positions that can be held simultaneously # Rebalance Weekly: If false, only rebalance when we add/remove positions # UseMomWeight: If true, rebalance w/momentum-based weights (top gainers=more weight) # ######################################################################### from SmartRollingWindow import * class EMACrossoverUniverse(QCAlgorithm): ## def Initialize(self): self.InitAlgoParams() self.InitAssets() self.InitUniverse() self.InitBacktestParams() self.ScheduleRoutines() ## Set backtest params: dates, cash, etc. Called from Initialize(). ## ---------------------------------------------------------------- def InitBacktestParams(self): self.SetStartDate(2020, 1, 1) # self.SetEndDate(2019, 2, 1) self.SetCash(100000) self.SetBenchmark(Symbol.Create("BTCUSDT", SecurityType.Crypto, Market.Binance)) def InitUniverse(self): self.UniverseSettings.Resolution = Resolution.Daily self.symDataDict = { } self.UniverseTickers = ["SOLUSDT", "ETHUSDT", "BNBUSDT", "ADAUSDT", "BTCUSDT"] universeSymbols = [] for symbol in self.UniverseTickers: universeSymbols.append(Symbol.Create(symbol, SecurityType.Crypto, Market.Binance)) self.SetUniverseSelection(ManualUniverseSelectionModel(universeSymbols)) # -------------------- def InitAlgoParams(self): self.emaSlowPeriod = int(self.GetParameter('emaSlowPeriod')) self.emaFastPeriod = int(self.GetParameter('emaFastPeriod')) self.mompPeriod = int(self.GetParameter('mompPeriod')) # used for momentum based weight self.minimumVolPeriod = int(self.GetParameter('minimumVolPeriod')) # used for volume threshold self.warmupPeriod = max(self.emaSlowPeriod, self.mompPeriod, self.minimumVolPeriod) self.useMomWeight = (int(self.GetParameter("useMomWeight")) == 1) self.maxExposurePct = float(self.GetParameter("maxExposurePct"))/100 self.rebalanceWeekly = (int(self.GetParameter("rebalanceWeekly")) == 1) self.minimumVolume = int(self.GetParameter("minimumVolume")) self.maxHoldings = int(self.GetParameter("maxHoldings")) ## Experimental: ## self.maxSecurityDrawDown = float(self.GetParameter("maxSecurityDrawDown")) # -------------------- def InitAssets(self): self.symbol = "BTCUSDT" self.SetBrokerageModel(BrokerageName.Binance, AccountType.Cash) self.SetAccountCurrency("USDT") self.AddCrypto(self.symbol, Resolution.Daily) self.EnableAutomaticIndicatorWarmUp = True self.SetWarmUp(timedelta(self.warmupPeriod)) self.SelectedSymbolsAndWeights = {} ## Experimental: ## self.AddRiskManagement(MaximumUnrealizedProfitPercentPerSecurity(self.maxSecurityDrawDown)) # ------------------------ def ScheduleRoutines(self): if(self.rebalanceWeekly): self.Schedule.On( self.DateRules.WeekStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol, 31), self.RebalanceHoldings ) ## Check if we are already holding the max # of open positions. ## ------------------------------------------------------------ @property def PortfolioAtCapacity(self): numHoldings = len([x.Key for x in self.Portfolio if x.Value.Invested]) return numHoldings >= self.maxHoldings ## In the OnData Event handler, check for signals ## ------------------------------------------------ def OnData(self, dataSlice): ## loop through the symbols in the slice for symbol in dataSlice.Keys: ## if we have this symbol in our data dictioary if symbol in self.symDataDict: symbolData = self.symDataDict[symbol] ## Update the symbol with the data slice data symbolData.OnSymbolData(self.Securities[symbol].Price, dataSlice[symbol]) ## If we're invested in this symbol, manage any open positions if(self.Portfolio[symbolData.symbol.Value].Invested): symbolData.ManageOpenPositions() ## otherwise, if we're not invested, check for entry signal else: ## First check if we are at capacity for new positions. ## ## TODO: ## For Go-Live, note that the portfolio capacity may not be accurate while ## checking it inside this for-loop. It will be accurate after the positions ## have been open. IE: When the orders are actually filled. if(not self.PortfolioAtCapacity): if( symbolData.EntrySignalFired() ): self.OpenNewPosition(symbolData.symbol) ## TODO: ## For Go-Live, call OnNewPositionOpened only after ## the order is actually filled symbolData.OnNewPositionOpened() ## Logic to rebalance our portfolio of holdings. ## We will either rebalance with equal weighting, ## or assign weights based on momentum. ## ---------------------------------------------- def RebalanceHoldings(self): try: if self.useMomWeight: momentumSum = sum(self.symDataDict[symbol].momp.Current.Value for symbol in self.SelectedSymbolsAndWeights) for symbol in self.SelectedSymbolsAndWeights: if self.useMomWeight: symbolWeight = round((self.symDataDict[symbol].momp.Current.Value / momentumSum),4) else: symbolWeight = round(1/len(self.SelectedSymbolsAndWeights),4) ## Truncate symbolweight decimal places truncFactor = 10.0 ** 2 symbolWeight = math.trunc(symbolWeight * truncFactor) / truncFactor self.SelectedSymbolsAndWeights[symbol] = symbolWeight ## TODO: Calculate order qty instead of using % setholdings ## https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/BasicTemplateCryptoAlgorithm.py orderMsg = f"{symbol} | {round(symbolWeight*100,2)}% alloc. | price: {round(self.Securities[symbol].Close,2)}" if(self.Portfolio[symbol].Invested): orderMsg = f"[Re-Balancing] {orderMsg}" else: orderMsg = f"[NEW Addition] {orderMsg}" self.SetHoldings(symbol, symbolWeight * self.maxExposurePct, tag=orderMsg) except: self.Debug(f"Failed to rebalance") ## Adding the symbol to our dictionary will ensure ## that it gets processed in the rebalancing routine ## ------------------------------------------------- def OpenNewPosition(self, symbol): self.SelectedSymbolsAndWeights[symbol] = 0 self.RebalanceHoldings() ## Removing the symbol from our dictionary will ensure ## that it wont get processed in the rebalancing routine ## ----------------------------------------------------- def ExitPosition(self, symbol, exitMsg=""): profitPct = round(self.Securities[symbol].Holdings.UnrealizedProfitPercent,2) self.Liquidate(symbol, tag=f"SELL {symbol.Value} ({profitPct}% profit) [{exitMsg}]") self.SelectedSymbolsAndWeights.pop(symbol) self.RebalanceHoldings() return ## Create new symboldata object and add to our dictionary ## ------------------------------------------------------ def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol if( symbol in self.UniverseTickers and \ symbol not in self.symDataDict.keys()): self.symDataDict[symbol] = SymbolData(symbol, self) ################################## # SymbolData Class ################################## class SymbolData(): def __init__(self, theSymbol, algo): ## Algo / Symbol / Price reference self.algo = algo self.symbol = theSymbol self.lastPrice = 0 self.price = 0 ## Initialize indicators self.InitIndicators() ## ---------------------------------------- def InitIndicators(self): self.indicators = { 'EMA_FAST' : self.algo.EMA(self.symbol,self.algo.emaFastPeriod,Resolution.Daily), 'EMA_SLOW' : self.algo.EMA(self.symbol,self.algo.emaSlowPeriod,Resolution.Daily), '30DAY_VOL' : IndicatorExtensions.Times( self.algo.SMA(self.symbol,self.algo.minimumVolPeriod, Resolution.Daily, Field.Volume), self.algo.SMA(self.symbol,self.algo.minimumVolPeriod, Resolution.Daily, Field.Close)), 'MOMP' : self.algo.MOMP(self.symbol,self.algo.mompPeriod,Resolution.Daily)} ## for easy reference from main algo self.momp = self.indicators['MOMP'] for key, indicator in self.indicators.items(): self.algo.WarmUpIndicator(self.symbol, indicator, Resolution.Minute) self.emaFastWindow = SmartRollingWindow("float", 2) self.emaSlowWindow = SmartRollingWindow("float", 2) self.lastPriceWindow = SmartRollingWindow("float", 2) ## ---------------------------------------- def OnSymbolData(self, lastKnownPrice, tradeBar): self.lastPrice = lastKnownPrice self.UpdateRollingWindows() self.PlotCharts() ## ---------------------------------------- def UpdateRollingWindows(self): self.emaFastWindow.Add(self.indicators['EMA_FAST'].Current.Value) self.emaSlowWindow.Add(self.indicators['EMA_SLOW'].Current.Value) self.lastPriceWindow.Add(self.lastPrice) ## ---------------------------------------- def IsReady(self): return (self.indicators['EMA_FAST'].IsReady and self.indicators['EMA_SLOW'].IsReady \ and self.indicators['30DAY_VOL'].IsReady) ## ---------------------------------------- def MinimumVolTraded(self): if( self.indicators['30DAY_VOL'].IsReady ): dollarVolume = self.indicators['30DAY_VOL'].Current.Value if( dollarVolume >= self.algo.minimumVolume ): return True return False ## ---------------------------------------- def EntrySignalFired(self): if( self.IsReady() ): if( self.MinimumVolTraded() ): if( self.emaFastWindow.isAbove(self.emaSlowWindow) and \ self.lastPriceWindow.isAbove(self.emaFastWindow) ): return True return False ## ---------------------------------------- def ExitSignalFired(self): if( self.IsReady() ): if ( self.lastPriceWindow.isBelow(self.emaSlowWindow) or \ self.emaSlowWindow.isAbove(self.emaFastWindow) ): return True return False ## Logic to run immediately after a new position is opened. ## --------------------------------------------------------- def OnNewPositionOpened(self): # self.algo.Log(f"[BOUGHT {self.symbol.Value}] @ ${self.lastPrice:.2f}") return ## Manage open positions if any. ie: close them, update stops, add to them, etc ## Called periodically, eg: from a scheduled routine ## ## TODO: ## Consilder also liquidating if volume or liquidity thresholds arent met ## ----------------------------------------------------------------------------- def ManageOpenPositions(self): ## if( not self.MinimumVolTraded() ): ## self.ExitPosition(exitMsg="No longer liquid") if(self.ExitSignalFired()): self.ExitPosition(exitMsg="Exit Signal Fired") # ---------------------------------------- def ExitPosition(self, exitMsg): # self.algo.Log(f"[SELL {self.symbol.Value}] @ ${self.lastPrice:.2f}") self.algo.ExitPosition(self.symbol, exitMsg) # ---------------------------------------- def PlotCharts(self): ## To Plot charts, comment out the below # self.algo.Plot(f"{self.symbol}-charts", "Price", self.lastPriceWindow[0]) # self.algo.Plot(f"{self.symbol}-charts", "EMA Fast", self.indicators['EMA_FAST'].Current.Value) # self.algo.Plot(f"{self.symbol}-charts", "EMA Slow", self.indicators['EMA_SLOW'].Current.Value) return