Overall Statistics |
Total Trades 8 Average Win 0.00% Average Loss -0.01% Compounding Annual Return -0.044% Drawdown 0.000% Expectancy -0.464 Net Profit -0.015% Sharpe Ratio -2.171 Probabilistic Sharpe Ratio 0.005% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.07 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 0.357 Tracking Error 0.476 Treynor Ratio -118.35 Total Fees $14.80 |
from clr import AddReference AddReference("System") AddReference("QuantConnect.Algorithm") AddReference("QuantConnect.Indicators") AddReference("QuantConnect.Common") from System import * from QuantConnect import * from QuantConnect.Algorithm import * from QuantConnect.Indicators import * from datetime import datetime from math import floor import decimal import threading from enum import Enum, auto class Position(Enum): """Enum defining either a long position or short position.""" LONG = auto() SHORT = auto() class ForexAlgo(QCAlgorithm): """QuantConnect Algorithm Class for trading the EURUSD forex pair.""" # symbol of the forex pair: European Euros and US Dollars SYMBOL = Futures.Currencies.EUR; # number of periods where the fast moving average is # above or below the slow moving average before # a trend is confirmed HOURLY_TREND_PERIODS = 17 DAILY_TREND_PERIODS = 4 # limit for the number of trades per trend TREND_LIMIT_NUM_TRADES = 5 # maximum holdings for each market direction MAX_HOLDING_ONE_DIRECTION = 1 # units of currency for each trade; this will be updated based on # margin calls and port size TRADE_SIZE = 1 # take-proft and stop-loss offsets. TP_OFFSET = decimal.Decimal(0.06) SL_OFFSET = decimal.Decimal(0.06) #10/10000 # stochastic indicator levels for overbought and oversold STOCH_OVERBOUGHT_LEVEL = 80 STOCH_OVERSOLD_LEVEL = 20 # dictionary to keep track of associated take-profit and # stop-loss orders associatedOrders = {} # concurrency control for the dictionary associatedOrdersLock = threading.Lock() def Initialize(self): """Method called to initialize the trading algorithm.""" self.SetTimeZone("America/New_York") # backtest testing range self.SetStartDate(2020, 1, 1) self.SetEndDate(2020, 5, 1) # amount of cash to use for backtest self.SetCash(1000000) # We'll monitor the Forex trading pair self.eurPair = self.AddForex("EURUSD", Resolution.Minute) # But we'll buy and sell the futures contract self.forexPair = self.AddFuture(self.SYMBOL, Resolution.Minute) self.forexPair.SetFilter(timedelta(2), timedelta(90)) # brokerage model dictates the costs, slippage model, and fees # associated with the broker self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # 14 day ATR for to get the resolution self.fourteenDayATR = self.ATR(self.eurPair.Symbol, 14, MovingAverageType.Simple, Resolution.Daily) # # define a slow and fast moving average indicator # # slow moving average indicator: 200 periods # # fast moving average indicator: 50 periods # # these indicator objects are automatically updated self.hourlySlowSMA = self.SMA(self.eurPair.Symbol, 200, Resolution.Hour) self.hourlyFastSMA = self.SMA(self.eurPair.Symbol, 50, Resolution.Hour) # # define a pair of moving averages in order to confirm an # # alignment trend in the daily charts # # If both the hourly trend (using the 2 hourly SMAs above) and daily # # trend show the same trend direction, # # then the trend is a strong trend self.dailySlowSMA = self.SMA(self.eurPair.Symbol, 21, Resolution.Daily) self.dailyFastSMA = self.SMA(self.eurPair.Symbol, 7, Resolution.Daily) # counters defining the number of periods of the ongoing trend # (both the main hourly trend and the alignment daily trend) self.hourlySMATrend = 0 self.dailySMATrend = 0 # number of trades executed in this trend self.trendNumTrades = 0 # # stochastic indicator # # stochastic period: 9 # # stochastic k period: 9 # # stochastic d period: 5 self.stoch = self.STO(self.eurPair.Symbol, 9, 9, 5, Resolution.Hour) # keeps track of overbought/oversold conditions in the previous period self.previousIsOverbought = None self.previousIsOversold = None # keeps track of the time of the previous period self.previousTime = self.Time # Widen the free portfolio percentage to 30% to avoid margin calls for futures self.Settings.FreePortfolioValuePercentage = 0.30 # Setting a risk management model #self.SetRiskManagement(TrailingStopRiskManagementModel(0.06)) benchmark = self.AddEquity("SPY"); self.SetBenchmark(benchmark.Symbol); def OnData(self, data): """Method called when new data is ready for each period.""" for chain in data.FutureChains: self.popularContracts = [contract for contract in chain.Value if contract.OpenInterest > 1000] # If the length of contracts in this chain is zero, continue to the next chain if len(self.popularContracts) == 0: continue # Sort our contracts by open interest in descending order and save to sortedByOIContracts sortedByOIContracts = sorted(self.popularContracts, key=lambda k : k.OpenInterest, reverse=True) # Save the contract with the highest open interest to self.liquidContract self.liquidContract = sortedByOIContracts[0] #self.Debug(f"Symbol: {self.liquidContract.Symbol} Value: {self.liquidContract.LastPrice} Exp: {self.liquidContract.Expiry}") # if self.Portfolio[self.liquidContract.Symbol].Invested: # self.mom.Update(slice.Time, self.liquidContract.LastPrice) # self.Log(f"Current Value: {self.mom.Current.Value}") # return # self.SetHoldings(self.liquidContract.Symbol, 1) # self.mom = self.MOM(self.liquidContract.Symbol, self.lookback) # hist = self.History(self.liquidContract.Symbol, self.lookback)['close'] # self.Log(hist.to_string()) # for k, v in hist.items(): # self.mom.Update(k[2], v) # self.Log(f"Current Value: {self.mom.Current.Value}") # Contract properties # class FuturesContract: # self.Symbol # (Symbol) Symbol for contract needed to trade # self.Expiry # (datetime) When the future expires # self.LastPrice # (decimal) Last sale price # self.BidPrice # (decimal) Offered price for contract # self.AskPrice # (decimal) Asking price for contract # self.Volume # (long) Reported volume # self.OpenInterest # (decimal) Number of open contracts #self.Debug(f"Symbol: {self.liquidContract.Symbol} Value: {self.liquidContract.LastPrice} Exp: {self.liquidContract.Expiry}") # only trade when the indicators are ready if not self.hourlySlowSMA.IsReady or not self.hourlyFastSMA.IsReady or not self.stoch.IsReady or not self.fourteenDayATR.IsReady: #self.Debug(f"hourly: {self.hourlySlowSMA.IsReady} fast: {self.hourlyFastSMA.IsReady} stoch: {self.stoch.IsReady} 14days: {self.fourteenDayATR.IsReady}") return # trade only once per period # if self.previousTime.time().hour == self.Time.time().hour: # return # only trade once we're two above the renko noise # 14 day atr price change in tick self.periodPreUpdateStats() price = self.liquidContract.LastPrice #2. Save the contract security object to the variable future future = self.Securities[self.liquidContract.Symbol] #3. Calculate the number of contracts we can afford based on the margin required # Divide the margin remaining by the initial margin and save to self.contractsToBuy self.TRADE_SIZE = 1 #floor( (self.Portfolio.MarginRemaining / future.BuyingPowerModel.InitialOvernightMarginRequirement)) # if it is suitable to go long during this period if (self.entrySuitability() == Position.LONG): self.enterMarketOrderPosition( symbol=self.liquidContract.Symbol, position=Position.LONG, posSize=self.TRADE_SIZE, tp=round((price + (price * self.TP_OFFSET)), 5), #round(price * (1 + self.TP_OFFSET), 4), #(price + self.TP_OFFSET, 4), sl=round((price + (price * self.SL_OFFSET)), 5) ) #round(price * (1 - self.SL_OFFSET), 4) ) #round(price - self.SL_OFFSET, 4)) # it is suitable to go short during this period elif (self.entrySuitability() == Position.SHORT): self.enterMarketOrderPosition( symbol=self.liquidContract.Symbol, position=Position.SHORT, posSize=self.TRADE_SIZE, tp=round((price + (price * self.TP_OFFSET)), 5), #tp= round(price * (1 - self.TP_OFFSET), 4), #tp=round(price - self.TP_OFFSET, 4), sl=round((price + (price * self.SL_OFFSET)), 5) ) #sl= round(price * (1 + self.SL_OFFSET), 4) ) # self.periodPostUpdateStats() def entrySuitability(self): """Determines the suitability of entering a position for the current period. Returns either Position.LONG, Position.SHORT, or None""" # units of currency that the bot currently holds holdings = self.Portfolio[self.liquidContract.Symbol].Quantity # conditions for going long (buying) if ( # uptrend for a certain number of periods in both # the main hourly trend and alignment daily trend self.dailySMATrend >= self.DAILY_TREND_PERIODS and self.hourlySMATrend >= self.HOURLY_TREND_PERIODS and # if it is not oversold self.stoch.StochD.Current.Value > self.STOCH_OVERSOLD_LEVEL and # if it just recently stopped being oversold self.previousIsOversold is not None and self.previousIsOversold == True and # if holdings does not exceed the limit for a direction holdings < self.MAX_HOLDING_ONE_DIRECTION and # if number of trades during this trend does not exceed # the number of trades per trend self.trendNumTrades < self.TREND_LIMIT_NUM_TRADES ): return Position.LONG # conditions for going short (selling) elif ( # downtrend for a certain number of periods in both # the main hourly trend and alignment daily trend self.dailySMATrend <= -self.DAILY_TREND_PERIODS and self.hourlySMATrend <= -self.HOURLY_TREND_PERIODS and # if it is not overbought self.stoch.StochD.Current.Value < self.STOCH_OVERBOUGHT_LEVEL and # if it just recently stopped being overbought self.previousIsOverbought is not None and self.previousIsOverbought == True and # if holdings does not exceed the limit for a direction holdings > -self.MAX_HOLDING_ONE_DIRECTION and # if number of trades during this trend does not exceed # the number of trades per trend self.trendNumTrades < self.TREND_LIMIT_NUM_TRADES ): return Position.SHORT # unsuitable to enter a position for now return None def periodPreUpdateStats(self): """Method called before considering trades for each period.""" # since this class's OnData() method is being called in each new # tick period, the daily stats should only be updated if # the current date is different from the date of the previous # invocation if self.previousTime.date() != self.Time.date(): # uptrend: if the fast moving average is above the slow moving average if self.dailyFastSMA.Current.Value > self.dailySlowSMA.Current.Value: if self.dailySMATrend < 0: self.dailySMATrend = 0 self.dailySMATrend += 1 # downtrend: if the fast moving average is below the slow moving average elif self.dailyFastSMA.Current.Value < self.dailySlowSMA.Current.Value: if self.dailySMATrend > 0: self.dailySMATrend = 0 self.dailySMATrend -= 1 # uptrend: if the fast moving average is above the slow moving average if self.hourlyFastSMA.Current.Value > self.hourlySlowSMA.Current.Value: if self.hourlySMATrend < 0: self.hourlySMATrend = 0 self.trendNumTrades = 0 self.hourlySMATrend += 1 # downtrend: if the fast moving average is below the slow moving average elif self.hourlyFastSMA.Current.Value < self.hourlySlowSMA.Current.Value: if self.hourlySMATrend > 0: self.hourlySMATrend = 0 self.trendNumTrades = 0 self.hourlySMATrend -= 1 def periodPostUpdateStats(self): """Method called after considering trades for each period.""" if self.stoch.StochD.Current.Value <= self.STOCH_OVERSOLD_LEVEL: self.previousIsOversold = True else: self.previousIsOversold = False if self.stoch.StochD.Current.Value >= self.STOCH_OVERBOUGHT_LEVEL: self.previousIsOverbought = True else: self.previousIsOverbought = False self.previousTime = self.Time def enterMarketOrderPosition(self, symbol, position, posSize, tp, sl): """Enter a position (either Position.LONG or Position.Short) for the given symbol with the position size using a market order. Associated take-profit (tp) and stop-loss (sl) orders are entered.""" self.associatedOrdersLock.acquire() self.notionalValue = self.liquidContract.AskPrice * self.forexPair.SymbolProperties.ContractMultiplier self.Debug(f"Sym: {symbol} Margin: {self.Portfolio.MarginRemaining} Price: {self.liquidContract.LastPrice} NotionalValue: {self.notionalValue} Pos: {position} Size: {posSize}, Tp: {tp}, Sl: {sl}") if position == Position.LONG: self.Buy(symbol, posSize) takeProfitOrderTicket = self.LimitOrder(symbol, -posSize, tp) stopLossOrderTicket = self.StopMarketOrder(symbol, -posSize, sl) elif position == Position.SHORT: self.Sell(symbol, posSize) takeProfitOrderTicket = self.LimitOrder(symbol, posSize, tp) stopLossOrderTicket = self.StopMarketOrder(symbol, posSize, sl) # associate the take-profit and stop-loss orders with one another self.associatedOrders[takeProfitOrderTicket.OrderId] = stopLossOrderTicket self.associatedOrders[stopLossOrderTicket.OrderId] = takeProfitOrderTicket self.associatedOrdersLock.release() self.trendNumTrades += 1 def OnOrderEvent(self, orderEvent): """Method called when an order has an event.""" # if the event associated with the order is about an # order being fully filled if orderEvent.Status == OrderStatus.Filled: order = self.Transactions.GetOrderById(orderEvent.OrderId) # if the order is a take-profit or stop-loss order if order.Type == OrderType.Limit or order.Type == OrderType.StopMarket: self.associatedOrdersLock.acquire() # during volatile markets, the associated order and # this order may have been triggered in quick # succession, so this method is called twice # with this order and the associated order. # this prevents a runtime error in this case. if order.Id not in self.associatedOrders: self.associatedOrdersLock.release() return # obtain the associated order and cancel it. associatedOrder = self.associatedOrders[order.Id] associatedOrder.Cancel() # remove the entries of this order and its # associated order from the hash table. del self.associatedOrders[order.Id] del self.associatedOrders[associatedOrder.OrderId] self.associatedOrdersLock.release() def OnEndOfAlgorithm(self): """Method called when the algorithm terminates.""" # Liquidate entire portfolio (all unrealized profits/losses will be realized). # long and short positions are closed irrespective of profits/losses. self.Liquidate()