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)