| 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
}