Overall Statistics
Total Trades
35
Average Win
5.42%
Average Loss
-2.69%
Compounding Annual Return
17.525%
Drawdown
11.300%
Expectancy
0.596
Net Profit
35.636%
Sharpe Ratio
0.791
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
2.01
Alpha
0.103
Beta
0.036
Annual Standard Deviation
0.155
Annual Variance
0.024
Information Ratio
-0.594
Tracking Error
0.717
Treynor Ratio
3.384
Total Fees
$0.00
from collections import deque
from datetime import datetime, timedelta
from numpy import sum
import decimal as d
from Order_codes import (OrderTypeCodes, OrderDirectionCodes, OrderStatusCodes)
from System.Drawing import Color

class BTCStrategyIndicators(QCAlgorithm):


    def Initialize(self):
        self.SetStartDate(2017, 9, 1)  # Set Start Date
        self.SetEndDate(2019, 7, 21)    # Set End Date
        self.SetCash(100000)             # Set Strategy Cash
        self.AddCrypto("BTCUSD", Resolution.Hour, Market.Bitfinex)


        self.symbol = 'BTCUSD'
        self.SetTimeZone(TimeZones.Utc)
        # Create a Consolidator with 4 hours resolution. Register the RSI Indicator within the Consolidator.
        consFourHours = TradeBarConsolidator(4)
        self.rsiFourHour = RelativeStrengthIndex(14)
        consFourHours.DataConsolidated += self.OnFourHoursData
        self.SubscriptionManager.AddConsolidator("BTCUSD", consFourHours)
        self.RegisterIndicator("BTCUSD", self.rsiFourHour, consFourHours)
        
    
        # Create a Consolidator with 1 day resolution. Register the RSI Indicator within the Consolidator.
        consOneDay = TradeBarConsolidator(timedelta(days=1))
        self.rsiDaily = RelativeStrengthIndex(14)
        consOneDay.DataConsolidated += self.OnDayData
        self.SubscriptionManager.AddConsolidator("BTCUSD",consOneDay)
        self.RegisterIndicator("BTCUSD", self.rsiDaily, consOneDay)
     
        
        
        # Define flags variable to manage orders
        self.stopLimitTicket = None
        self.buyStopOrder = None
        
        # Create the buy and sell tickets ojects to track if orders are filled
        self.buyTicket = None
        self.sellTicket = None 
        
        # Flags to avoid multiple orders while the first order to buy is not filled.
        self.buySignal = None
        
        # The daySignal variable would tell if the condition in the daily timeframe is reached 
        self.daySignal = None
        
        # Define variables to calculate pnl of each trade
        self.buyPrice = None
        self.sellPrice = None 
    
        # Define two rolling window for storing 4 hour and daily prices. Each rolling 
        # window have 14 periods.
        self.rsiWindow = RollingWindow[float](14)
        self.rsiDailyWindow = RollingWindow[float](14)
        
        # Initialize the k and d Lines in each Resolution 
        self.kLineDaily = SimpleMovingAverage('klineDaily',3)
        self.dLineDaily = SimpleMovingAverage('dlineDaily',3)
        self.kLineFourHour = SimpleMovingAverage("ThreeSMAkline", 3)
        self.dLineFourHour = SimpleMovingAverage("ThreeSMAdline", 3)
       
        
        # Buy and sells order filled are marked with black diamond(buys) and green light square(sells)
        btcPrice = Chart('BTCPrice')
        btcPrice.AddSeries(Series("BTCUSD", SeriesType.Line,0))
        btcPrice.AddSeries(Series("Buy", SeriesType.Scatter, 0))
        btcPrice.AddSeries(Series("Sell", SeriesType.Scatter, 0))
        self.AddChart(btcPrice)
        
        overlayPlot = Chart("OverlayPlot")
        
        # Buy Signals are marked with a green triangle in the stochastic lines
        overlayPlot.AddSeries(Series("RedLine", SeriesType.Line, '$',Color.Red,0))
        overlayPlot.AddSeries(Series("BlueLine", SeriesType.Line, '$' , Color.Blue,0))
        overlayPlot.AddSeries(Series('BuySignals', SeriesType.Scatter,'$', Color.Green,0))
        overlayPlot.AddSeries(Series("RSI", SeriesType.Line,1))
        self.AddChart(overlayPlot)
        
        # Get the time in which the day and four hour bars closed
        self.DayBarTime = None
        self.FourHourBar = None 
        
        # set a warm-up period to initialize the indicator
        self.SetWarmUp(30)
        
        self.SetBenchmark('BTCUSD')
        

    def OnFourHoursData(self, sender, bar):
        '''
        This function has a Four Hour Consolidator where the data is 
        pumped in 4 hours interval. The rsiWindow store the values of
        RSI in a 14 period rolling window. With these values we create 
        the Stoch RSI
        '''
        # Don't do anything if the algorithm is warming up
        if self.IsWarmingUp:  return
        
        #if not self.rsiFourHour.IsReady: return 
       # self.rsiFourHour.Update(bar.EndTime, bar.Close)
        # Don't do anything if the rolling window has not enough values
        # Add the values of rsi in the rsiWindow rolling window
        self.rsiWindow.Add(self.rsiFourHour.Current.Value)
        
        if not self.rsiWindow.IsReady: return
      
       
        # Debugging messages to check consolidator and system times 
    #   self.Debug('Four Hour Consolidation EndTime Time %s  %s price %s close %s period %s' % (bar.EndTime,bar.Time,bar.Price,bar.Close,bar.Period))
        #self.Debug('Time %s' % self.Time)
        
        # Get the values of the current, max and min RSI since 14 periods
        rsiFourHour = self.rsiWindow[0]
        maxRSI = max(self.rsiWindow)
        minRSI = min(self.rsiWindow)
        
        # Define the Stoch variable that is the Stochastic formula with the RSI values
        
        try:
            Stoch = (rsiFourHour - minRSI) / (maxRSI - minRSI)
            # Get the kline of the Stochastic, which is the 3 SMA period
            self.kLineFourHour.Update(bar.EndTime,Stoch)
        except:
            pass
     
        
        # Geth the dline of the Stochastic which is the 3 SMA of the kline 
        if self.kLineFourHour.IsReady:                      
            self.dLineFourHour.Update(bar.EndTime,self.kLineFourHour.Current.Value)
                                                          
        if not self.kLineFourHour.IsReady or not self.dLineFourHour.IsReady: return
     
     
        #self.Debug('length of kLineFour %s' % self.kLineFourHour.Count)
        #self.Debug('length of kLineFour %s' % self.dLineFourHour.Count)
        kLine = round(self.kLineFourHour.Current.Value * 100,2)
        dLine = round(self.dLineFourHour.Current.Value * 100,2)
        dLineDaily = round(self.dLineDaily.Current.Value * 100,2)
        kLineDaily = round(self.kLineDaily.Current.Value * 100,2)
        
        #if self.rsiFourHour.IsReady and self.rsiDaily.IsReady:
        #    self.Debug('RSI Four Hour %s, kLine, Dline four hour %s %s at time %s' % (self.rsiFourHour, kLine,dLine,bar.EndTime))
        #    self.Debug('RSI Daily %s kLine, dLine daily  %s %s at time %s ' % (self.rsiDaily,kLineDaily, dLineDaily,bar.EndTime))
        #if self.dLineFourHour.IsReady:
        #self.Debug('kLine Four hour %s' % self.kLineFourHour)
        #self.Debug('Stoch is %s' % Stoch)
        #self.Debug('max RSI %s' % maxRSI)
        #self.Debug('min RSI %s' % minRSI)
        #self.Debug('RSI FourHour Value %s price %s time %s' % (rsiFourHour, bar.Close, bar.EndTime))
       
        #self.Debug('Four Hour RSI %s kLine %s, dLine %s , price %s consolidated time %s time %s' % (rsiFourHour,kLine,dLine,round(bar.Close,2),bar.EndTime,self.Time))
        # If the daily signal is True and kLine and dLine are lower than 20 and kLine
        # is higher than dLine, send a Limit Order to buy.
        
        if self.daySignal == True:
            
            if (kLine > dLine) and (kLine < 20) and not self.buySignal:
                currentPrice = round(self.Securities[self.symbol].Close,2)
                self.Debug('Send a LimitOrder to BUY BTCUSD at time %s with close bar time on four hours %s daily close on %s CurrentPrice %s' % (self.Time,bar.EndTime, self.DayBarTime, currentPrice))
                self.Debug('On bar time %s, kLineFourHour %s, dLineFourHour %s, kLineDaily %s dLineDaily %s' % (bar.EndTime, kLine,dLine,kLineDaily,dLineDaily))
                halfPortfolio = self.Portfolio.TotalPortfolioValue * 0.5
                self.Plot("OverlayPlot", "BuySignals", kLineDaily)
                quantity = round(halfPortfolio/currentPrice,2)
                # Set limitPrice to buy 15 usd below past close of the bar
                orderPrice = round(bar.Close-15)
                self.buySignal = True
                self.buyTicket = self.LimitOrder('BTCUSD',quantity,orderPrice)
                
    def OnDayData(self,sender,bar):
        '''
        This function has daily timeframe, so the data is pumped one time a day.
        The rsiDailyWindow would store the values of RSI and then, generate the 
        Stoch RSI with the curr, min and max values of the window. Then are gene-
        rated the k and d lines.
        '''
        # Don't do anything if the algorithm is warming up
        if self.IsWarmingUp:  return
    
        
        #if not self.rsiDaily.IsReady: return
        # Add the RSI Values to the rolling window to store 14 values of RSI
        self.rsiDailyWindow.Add(self.rsiDaily.Current.Value)    
        if not self.rsiDailyWindow.IsReady: return
    
        
        # Debugging messages to check consolidators and system times
        #self.Debug('Day Consolidation Time %s' % bar.EndTime)
        #self.Debug('Time %s' % self.Time)
        rsiDaily = self.rsiDailyWindow[0]
        maxRSI = max(self.rsiDailyWindow)
        minRSI = min(self.rsiDailyWindow)
        
        # Define the Stoch variable that is the Stochastic formula with the RSI values
        try:
            Stoch = (rsiDaily - minRSI) / (maxRSI - minRSI)
            # Get the kline of the Stochastic, which is the 3 SMA period
            self.kLineDaily.Update(bar.EndTime,Stoch)
        except:
            pass
        # Geth the dline of the Stochastic which is the 3 SMA of the kline 
        if self.kLineDaily.IsReady:
            self.dLineDaily.Update(bar.EndTime,self.kLineDaily.Current.Value)
        
            
        if not self.kLineDaily.IsReady or not self.dLineDaily.IsReady: return
        
        #self.Debug('RSI Daily Value %s price %s time %s' % (rsiDaily, bar.Close, bar.EndTime))
        #self.Debug('RSI MAX and MIN %s %s at time %s' % (round(maxRSI,2), round(minRSI,2),self.Time))
        #self.Debug('Daily kline is %s' % self.kLineDaily.Current.Value)
        #self.Debug('Daily dline is %s' % self.dLineDaily.Current.Value)
        kLineDaily = round(self.kLineDaily.Current.Value * 100,2) 
        dLineDaily = round(self.dLineDaily.Current.Value * 100,2)
        
        self.Plot("OverlayPlot", "BlueLine", kLineDaily)
        self.Plot("OverlayPlot", "RedLine", dLineDaily)
        self.Plot("OverlayPlot", "RSI", rsiDaily)
        self.Plot('BTCPrice', 'BTCUSD', bar.Close)
        
        #kLineFourHour = round(self.kLineFourHour.Current.Value * 100,2)
        #dLineFourHour = round(self.dLineFourHour.Current.Value * 100,2)
        #self.Debug('RSI Daily %s %s maxRSI %s minRSI %s , kLine, dLine daily  %s %s at time %s ' % (self.rsiDaily,round(self.rsiDaily.Current.Value,2), maxRSI,minRSI,kLineDaily, dLineDaily,bar.EndTime))

        self.DayBarTime = bar.EndTime
        #self.Debug('Daily RSI Value %s daykLine %s ,daydLine %s, klineFourHour %s , dlineFourHour %s, price %s at consolidator time %s and time %s' % (round(self.rsiDaily.Current.Value,2), kLineDaily, dLineDaily, kLineFourHour, dLineFourHour, round(bar.Close,2), bar.EndTime,self.Time))
        # If kLine and dLine are less than 15 the flag variable self.daySignal is set to True
        if (kLineDaily) < d.Decimal(15) and  (dLineDaily) < d.Decimal(15):
            self.daySignal = True
           # self.Debug('At %s daySignal is True as kLine and dLine daily are %s %s' % (bar.EndTime,kLineDaily, dLineDaily))
            #self.Debug('%s k line and d line daily are %s %s' % (self.Time,Stoch, self.kLineDaily.Current.Value))#,self.dLineDaily.Current.Value))

        else:
            self.daySignal = False
            
        currentPrice = self.Securities['BTCUSD'].Price
        
        
        # If the stopOrder was submitted, and the current Price is higher than
        # previous price, we update the stopPrice and limitPrice of the stopLimitOrder. 
        if self.stopLimitTicket is not None:
            if currentPrice > self.previousPrice:
                updateOrderFields = UpdateOrderFields()
                # Update stop and limit price of the stopLimit order each time current Price is higher than previous price
                newStop = round(currentPrice * d.Decimal(0.93),3)
                limitPrice = round(newStop - 15,3)
                updateDate = self.stopLimitTicket.Time.date()
                updateOrderFields.StopPrice = newStop
                updateOrderFields.LimitPrice = limitPrice
                self.stopLimitTicket.Update(updateOrderFields)
              #  self.Debug('Update stopLimitOrder at %s  with current price %s higher than previous price %s new stop is %s' % (updateDate,currentPrice, self.previousPrice,newStop))
            
        # Track the dLine and kLine differences once the BTCUSD asset is in 
        # the Portfolio
        if self.Portfolio['BTCUSD'].Invested:
            dLine = round(self.dLineDaily.Current.Value * 100,3)
            kLine = round(self.kLineDaily.Current.Value * 100,3)
            diff = round(dLine - kLine,2)
            
            if (diff) > d.Decimal(3):
                # Cancel open orders that are in the market: stopOrder
                self.CancelOpenOrders()
                quantity = self.Portfolio['BTCUSD'].Quantity
                # Set limitPrice to sell 15 usd above past close
                sellPrice = round(bar.Close + 15,2)
                
                self.sellTicket = self.LimitOrder('BTCUSD',-quantity,sellPrice)
                self.Debug('On %s sell BTC with a difference between dLine and KLine of %s dLine is higher than kLine by 3' % (self.Time.date(),diff))
     
        self.previousPrice = currentPrice
        
    def CancelOpenOrders(self):
        oo = self.Transactions.GetOpenOrders()
        for order in oo:
            #self.Debug('On Time %s' % self.Time)
            #self.Debug('At %s cancel %s Open Order with %s direction submitted on %s last update on %s' % (self.Time,OrderTypeCodes[order.Type], OrderDirectionCodes[order.Direction],order.Time, order.LastUpdateTime))
            self.Transactions.CancelOrder(order.Id)
            
            
    def OnOrderEvent(self, event):
        # Handle filling of buy & sell orders:
        # Determine if order is the buy or the sell or the stop
            
        order = self.Transactions.GetOrderById(event.OrderId)
        #self.Log("{0}: {1}: {2}".format(self.Time, order.Type, event))
            
        ## CHECK IF BUY ORDER WAS FILLED ##
        #if OrderStatusCodes[order.Status] == 'Filled' and OrderDirectionCodes[order.Direction] == 'Buy' and not self.buyStopOrder:
        
        if self.buyTicket is not None and not self.buyStopOrder:
            if OrderStatusCodes[self.buyTicket.Status] == 'Filled':
                quantity = self.buyTicket.Quantity
                self.buyPrice = self.buyTicket.AverageFillPrice
                self.Plot("BTCPrice", "Buy", self.buyPrice)
                self.Debug("Buy Limit order filled at time %s with price %s" % (self.Time, self.buyPrice)) 
                # Define a stopLimitOrder witha a stopPrice 7% far away the filled price
                stopPrice = round(self.buyPrice * d.Decimal(0.93),2)
                # LimitPrice 15 usd below the stopPrice. This price would be updated if current BTC price go up
                stopLimitPrice = round(stopPrice - d.Decimal(15),2)
                # This variable is set to true in order to send the stopOrder one time only
                self.buyStopOrder = True  
               # self.Debug("Submit Stop Limit Order with Stop and Limit price of %s %s" % (stopPrice, stopLimitPrice))                
                self.stopLimitTicket = self.StopLimitOrder('BTCUSD',-quantity,stopPrice,stopLimitPrice)
                
                   
        ## CHECK IF SELL ORDER WAS FILLED ##
        if self.sellTicket is not None and self.buySignal:
            # Reset buystopOrder 
            if OrderStatusCodes[self.sellTicket.Status] == 'Filled':
                self.sellPrice = self.sellTicket.AverageFillPrice
                dLine = round(self.dLineDaily.Current.Value * 100,2) 
                kLine = round(self.kLineDaily.Current.Value * 100,2)
                self.Debug("At %s SELL LIMIT order filled at price %s time filled %s with k and d Daily Lines %s %s" % (self.Time, self.sellPrice, order.LastFillTime, kLine,dLine)) 
                self.Plot("BTCPrice", "Sell", self.sellPrice)
                if self.sellPrice > self.buyPrice:
                    pnl = (self.sellPrice - self.buyPrice) * (-self.sellTicket.Quantity)
                    self.Debug('Win trade with Pnl %s' % round(pnl,2))
                else:
                    pnl = (self.sellPrice - self.buyPrice) *  (-self.sellTicket.Quantity)
                    self.Debug('Loss trade with Pnl %s' % round(pnl,2))
                self.buyStopOrder = None
                self.buyTicket = None
                self.buySignal = None
                self.sellTicket = None
                self.sellPrice = None
                self.buyPrice = None
                if self.stopLimitTicket is not None:
                    self.stopLimitTicket.Cancel()
                    self.stopLimitTicket = None

        ## CHECK IF STOP LIMIT ORDER WAS FILLED ##          
        if self.stopLimitTicket is not None and self.buySignal:
            if OrderStatusCodes[self.stopLimitTicket.Status] == 'Filled':
                self.sellPrice = self.stopLimitTicket.AverageFillPrice
                self.Plot("BTCPrice", "Sell", self.sellPrice)
                # If stop order is filled, cancel the sell order, if any:
               # self.Debug("On %s StopLimit order filled with price %s time filled %s" % (self.Time,self.sellPrice, order.LastFillTime))
                if self.sellPrice > self.buyPrice:
                    pnl = (self.sellPrice - self.buyPrice) * (-self.stopLimitTicket.Quantity)
                    self.Debug('Win trade with Pnl %s' % round(pnl,2))
                else:
                    pnl = (self.sellPrice - self.buyPrice) * (-self.stopLimitTicket.Quantity)
                    self.Debug('Loss trade with Pnl %s' % round(pnl,2))
                self.stopLimitTicket = None
                self.buyStopOrder = None
                self.buyTicket = None
                self.buySignal = None
                self.sellPrice = None
                self.buyPrice = None
                if self.sellTicket is not None:
                    self.sellTicket.Cancel()
                    self.sellTicket = None
"""
This file contains QuantConnect order codes for easy conversion and more 
intuitive custom order handling

References:
    https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderTypes.cs
    https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderRequestStatus.cs
"""

OrderTypeKeys = [
    'Market', 'Limit', 'StopMarket', 'StopLimit', 'MarketOnOpen',
    'MarketOnClose', 'OptionExercise',
]

OrderTypeCodes = dict(zip(range(len(OrderTypeKeys)), OrderTypeKeys))

OrderDirectionKeys = ['Buy', 'Sell', 'Hold']
OrderDirectionCodes = dict(zip(range(len(OrderDirectionKeys)), OrderDirectionKeys))

## NOTE ORDERSTATUS IS NOT IN SIMPLE NUMERICAL ORDER

OrderStatusCodes = {
    0:'New', # new order pre-submission to the order processor
    1:'Submitted', # order submitted to the market
    2:'PartiallyFilled', # partially filled, in market order
    3:'Filled', # completed, filled, in market order
    5:'Canceled', # order cancelled before filled
    6:'None', # no order state yet
    7:'Invalid', # order invalidated before it hit the market (e.g. insufficient capital)
    8:'CancelPending', # order waiting for confirmation of cancellation
}