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 }