| 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()