Overall Statistics |
Total Trades 23 Average Win 35.90% Average Loss -4.95% Compounding Annual Return 135.133% Drawdown 32.500% Expectancy 5.004 Net Profit 706.851% Sharpe Ratio 2.418 Probabilistic Sharpe Ratio 92.199% Loss Rate 27% Win Rate 73% Profit-Loss Ratio 7.26 Alpha 0.477 Beta 0.417 Annual Standard Deviation 0.39 Annual Variance 0.152 Information Ratio -0.383 Tracking Error 0.459 Treynor Ratio 2.262 Total Fees $2535.25 |
################################################################################ # The BTC Breakout Trend Climber # ---------------------------------- # # Entry: # ------- # Price closes above the highest high (25 day look-back), # or above the upper Bollinger Band (50 day look-back). # # Exit: # ------- # Exit with volatility-based stop loss (based on hourly ATR). # After entering a position, we set an initial stop loss, and an # 'activation' level above the current price. If price closes below # the stop loss, exit. If price closes above the activation level, # set a new, wider, trailing stop loss. All levels are ATR-based. # # Initial-Stop-level : Entry Price - ( 3.5 * ATR(5)) # Activation-Level : Last Price + ( 1 * ATR(15)) # Trailing-Level : Last Price - ( 11 * ATR(10)) # ################################################################################ from QuantConnect.Indicators import * class BTCBreakoutTrendClimber(QCAlgorithm): # ================================================================================== # Main entry point for the algo # ================================================================================== def Initialize(self): self.InitAlgoParams() self.InitBacktestParams() self.InitIndicators() self.ScheduleRoutines() # ================================================================================== # Set algo params: Symbol, broker model, ticker, etc. Called from Initialize(). # ================================================================================== def InitAlgoParams(self): # Set params for data and brokerage # ----------------------------------- self.symbol = self.GetParameter("tickerSymbol") self.SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash) self.AddCrypto(self.symbol, Resolution.Hour) # Set params for tracking stop losses # ----------------------------------------------- self.activateStopCoef = float(self.GetParameter("atrCoeff_ActivateTrail")) # activate trail when price rises 'x * ATR' self.initialStopCoef = float(self.GetParameter("atrCoeff_InitialStop")) # exit when price dips 'x * ATR' self.trailStopCoef = float(self.GetParameter("atrCoeff_TrailStop")) # exit when price dips 'x * ATR'(trailing) self.trailActivationPrice = 0 self.trailStopActivated = False self.trailingStopLoss = 0 self.initialStopLoss = 0 self.price = 0 self.atrValue = 0 # ================================================================== # Set backtest params: dates, cash, etc. Called from Initialize(). # ================================================================== def InitBacktestParams(self): self.initCash = 20000 # todo: use this to track buy+hold self.SetStartDate(2018, 9, 10) # Set Start Date self.SetCash(self.initCash) # Set Strategy Cash # ====================================================== # Initialize indicators. Called from Initialize(). # ====================================================== def InitIndicators(self): self.indicators = { 'BB' : self.BB(self.symbol, 50, 2.1, MovingAverageType.Exponential, Resolution.Daily), 'MAX' : self.MAX(self.symbol,25, Resolution.Daily), 'ATR_init' : self.ATR(self.symbol, 5, MovingAverageType.Simple, Resolution.Hour), 'ATR_trail' : self.ATR(self.symbol,10, MovingAverageType.Simple, Resolution.Hour), 'ATR_activate' : self.ATR(self.symbol,15, MovingAverageType.Simple, Resolution.Hour) } # ====================================================================== # Schedule Routines (similar to chron jobs). Called from Initialize(). # ====================================================================== def ScheduleRoutines(self): # schedule routine to run every day at market close # ------------------------------------------------------------------ # todo: explore relevance of "BeforeMarketClose" in 24-hr BTC market self.Schedule.On(self.DateRules.EveryDay(), \ self.TimeRules.BeforeMarketClose(self.symbol), \ self.RunBeforeMarketClose) # schedule routine to run every 60 minutes # ----------------------------------------- self.Schedule.On(self.DateRules.EveryDay( ), \ self.TimeRules.Every(timedelta(minutes=60)), \ self.RunEveryHour) # ============================================================================== # Logic to run once a day, before market close. Scheduled by ScheduleRoutines(). # ============================================================================== def RunBeforeMarketClose(self): self.PlotCharts() # ==================================================================== # Logic to run every 60 minutes. Scheduled by ScheduleRoutines(). # ==================================================================== def RunEveryHour(self): self.ManageOpenPositions() # ============================================================================ # OnData handler. Triggered by data event (i.e. every time there is new data). # ============================================================================ # todo: track 'close' on multiple resolutions (daily bars and hourly bars) def OnData(self, dataSlice): self.price = dataSlice[self.symbol].Close if not self.Portfolio.Invested: if self.IndicatorsAreReady(): if self.EntrySignalDetected(): self.OpenNewPositions() # ============================================================================ # Check if indicators are ready. Called before attempting to check signals. # ============================================================================ def IndicatorsAreReady(self): return ( self.indicators['BB'].IsReady and \ self.indicators['MAX'].IsReady ) # ======================================================================== # Logic to check for entry signal. Should be called on bar close. # ======================================================================== def EntrySignalDetected(self): bbUpperBand = self.indicators['BB'].UpperBand.Current.Value maximumPrice = self.indicators['MAX'].Current.Value # if price has broken out of the upper bollinger band # or if price is above our highest high # ---------------------------------------------------- if(self.price >= bbUpperBand) or (self.price >= maximumPrice): self.Debug(f"{self.Time} - [Entry Signal Detected] \t\tBTC: ${self.price:.2f} | BBUpper: ${bbUpperBand:.2f} | MAX: ${maximumPrice:.2f} ") return True else: return False # ============================================================================ # Logic to open new positions. Called whenever we want to open a new position. # ============================================================================ def OpenNewPositions(self): self.Debug(f"{self.Time} - [BUY BTC] \t\t\tBTC: ${self.price:.2f}") # Buy bitcoin with 100% of portfolio # ------------------------------------------ self.SetHoldings("BTCUSD", 1) # 1 means 100% # set initial stops # -------------------- self.SetInitialStops() # ======================================================================== # Set initial stop and activation level. Called after new position opened. # ======================================================================== def SetInitialStops(self): self.price = self.CurrentSlice[self.symbol].Close self.atrValue_init = self.indicators['ATR_init'].Current.Value self.atrValue_activate = self.indicators['ATR_activate'].Current.Value self.trailStopActivated = False self.initialStopLoss = self.price - (self.atrValue_init * self.initialStopCoef) self.trailActivationPrice = self.price + (self.atrValue_activate * self.activateStopCoef) self.PlotCharts() # Plot charts for debugging # ============================================================================ # Manage open positions if any. ie: close them, update stops, add to them, etc # Called periodically, eg: from a scheduled routine # ============================================================================ def ManageOpenPositions(self): if( self.Portfolio.Invested ): self.UpdateStopsAndExitIfWarranted() # ============================================================================ # Update stop losses, and exit if warranted. Called when managing positions. # ============================================================================ def UpdateStopsAndExitIfWarranted(self): # Store atr value for calculating the trailing stop # ------------------------------------------------------------- self.atrValue_trail = self.indicators['ATR_trail'].Current.Value # If trailing stop loss is activated, check if price closed below it. # If it did, then exit. If not, update the trailing stop loss. # ------------------------------------------------------------------- if( self.trailStopActivated ): if( self.price < self.trailingStopLoss ): self.Debug(f"{self.Time} - [Trailing Stop Triggered] \tBTC: ${self.price:.2f}") self.ExitPositions() else: # Udpate trailing stop loss if price has gone up # ----------------------------------------------- if self.price > self.highestPrice: self.highestPrice = self.price self.trailingStopLoss = self.highestPrice - (self.atrValue_trail * self.trailStopCoef) # If trailing stop loss NOT activated, check if price crossed under # initial stop loss level, or above trailing stop activation level. # ------------------------------------------------------------------ else: if( self.price < self.initialStopLoss ): # if price goes below initial stop loss, exit # --------------------------------------------- self.Debug(f"{self.Time} - [Initial Stop Triggered] \tBTC: ${self.price:.2f}") self.ExitPositions() elif( self.price > self.trailActivationPrice ): # if price goes above activation price, start trailing # ------------------------------------------------------ self.Debug(f"{self.Time} - [Trailing Stop Activated] \tBTC: ${self.price:.2f}") self.highestPrice = self.price self.trailStopActivated = True self.trailingStopLoss = self.price - (self.atrValue_trail * self.trailStopCoef) # reset activation price and initial stop loss # ----------------------------------------------- self.trailActivationPrice = 0 self.initialStopLoss = 0 # Update our stops and check for exits # ------------------------------------- self.UpdateStopsAndExitIfWarranted() # ======================================================================== # Logic for exiting position: Liquidate, reset stops, etc. # ======================================================================== def ExitPositions(self): self.Debug(f"{self.Time} - [SELL BTC] \t\t\tBTC: ${self.price:.2f}") self.Liquidate() self.trailActivationPrice = 0 self.trailStopActivated = False self.trailingStopLoss = 0 self.initialStopLoss = 0 # ======================================================================== # Plot Charts. Called whenever we want to plot current values. # ======================================================================== def PlotCharts(self): self.Plot('Price Chart', 'Price', self.price) self.Plot('Price Chart', 'Initial Stop Loss', self.initialStopLoss) self.Plot('Price Chart', 'Trailing Stop Loss', self.trailingStopLoss) self.Plot('Price Chart', 'Trailing Stop Activation', self.trailActivationPrice) # self.Plot('BBands', 'Price', self.price) # self.Plot('BBands', 'BollingerUpperBand', self.indicators['BB'].UpperBand.Current.Value) # self.Plot('BBands', 'BollingerMiddleBand', self.indicators['BB'].MiddleBand.Current.Value) # self.Plot('BBands', 'BollingerLowerBand', self.indicators['BB'].LowerBand.Current.Value)