| Overall Statistics |
|
Total Orders 184 Average Win 2.27% Average Loss -0.72% Compounding Annual Return 10.835% Drawdown 18.700% Expectancy 0.424 Start Equity 100000 End Equity 181633.05 Net Profit 81.633% Sharpe Ratio 0.53 Sortino Ratio 0.541 Probabilistic Sharpe Ratio 24.899% Loss Rate 66% Win Rate 34% Profit-Loss Ratio 3.17 Alpha 0.021 Beta 0.293 Annual Standard Deviation 0.1 Annual Variance 0.01 Information Ratio -0.377 Tracking Error 0.146 Treynor Ratio 0.18 Total Fees $99.73 Estimated Strategy Capacity $0 Lowest Capacity Asset CME_EC1.QuantpediaFutures 2S Portfolio Turnover 0.72% |
# https://quantpedia.com/strategies/trendfollowing-in-futures-markets/
#
# The investment universe consists of 20 futures markets (4 currencies, 4 financials, 2 metals, 3 softs, 3 grains, 2 meats, and 2 energies).
# The initial position size is based on the maximum amount of risk the investor is willing to take for each position. The system described
# (and all simulation in the source paper) has an initial risk of 2%, i.e. if the market works against the investor, he will lose a maximum
# of 2% of his equity in this position. The exposure of each position is additionally restricted to 10% of current equity.
#
# The selected strategy is a Donchian Channel Breakout 100 (chosen due to its lower probability of curve-fitting as it depends only on 3 parameters:
# the number of days used to calculate the Donchian channel and 2 parameters in the ATR stop). The Donchian channel is formed by taking the highest
# high and the lowest low of the last n (100) periods.
#
# Rules:
# – The investor goes long/short at Stop as soon as the price crosses the upper/lower Donchian band of 100 days.
# – The investor exits and reverses position as soon as the opposite Donchian band is crossed.
# – He/she uses 4 times the ATR of 10 days as a parameter for the maximum risk to calculate the position size.
#
# Position sizing:
# 2% based on the initial stop loss (based on 4 times the 10-day-ATR).
#
# QC implementation:
# - Daily close prices are traded only.
from math import ceil
from AlgorithmImports import *
class ReturnsSignalMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2019, 1, 1)
self.SetCash(100000)
self.symbols = [
"CME_AD1", # Australian Dollar Futures, Continuous Contract #1
"CME_BP1", # British Pound Futures, Continuous Contract #1
"CME_EC1", # Euro FX Futures, Continuous Contract #1
"CME_JY1", # Japanese Yen Futures, Continuous Contract #1
"ICE_DX1", # US Dollar Index Futures, Continuous Contract #1
"CME_NQ1", # E-mini NASDAQ 100 Futures, Continuous Contract #1
"EUREX_FDAX1", # DAX Futures, Continuous Contract #1
"CME_ES1", # E-mini S&P 500 Futures, Continuous Contract #1
"CME_GC1", # Gold Futures, Continuous Contract
"CME_SI1", # Silver Futures, Continuous Contract
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_SB1", # Sugar No. 11 Futures, Continuous Contract
"CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat Futures, Continuous Contract
"CME_C1", # Corn Futures, Continuous Contract
"CME_LC1", # Live Cattle Futures, Continuous Contract
"CME_FC1", # Feeder Cattle Futures, Continuous Contract
"CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"ICE_O1" # Heating Oil Futures, Continuous Contract
]
period = 100
self.SetWarmUp(period)
self.dch = {}
self.atr = {}
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(10)
self.dch[symbol] = self.DCH(symbol, period, Resolution.Daily)
self.atr[symbol] = self.ATR(symbol, 10, Resolution.Daily)
def OnData(self, data):
if self.IsWarmingUp: return
dollar_pos_size = {}
equity = self.Portfolio.TotalPortfolioValue
long = []
short = []
for symbol in self.symbols:
if self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days >= 5:
continue
if symbol not in data or not data[symbol]: continue
if not self.dch[symbol].IsReady or not self.atr[symbol].IsReady: continue
price = data[symbol].Price
atr = self.atr[symbol].Current.Value
upper_band = self.dch[symbol].UpperBand.Current.Value
lower_band = self.dch[symbol].LowerBand.Current.Value
# Close position.
if self.Portfolio[symbol].IsLong:
if price < lower_band:
self.Liquidate(symbol)
if self.Portfolio[symbol].IsShort:
if price > upper_band:
self.Liquidate(symbol)
# Calculate positon size.
unit_size = (equity * 0.1 * 0.02) / (atr*4)
if price > upper_band:
if not self.Portfolio[symbol].IsLong:
long.append(symbol)
dollar_pos_size[symbol] = unit_size * price
elif price < lower_band:
if not self.Portfolio[symbol].IsShort:
short.append(symbol)
dollar_pos_size[symbol] = unit_size * price
# Rebalance opened positions and open new ones.
for symbol in long + short:
percentage = dollar_pos_size[symbol] / equity
if symbol in long: self.SetHoldings(symbol, percentage)
if symbol in short: self.SetHoldings(symbol, -percentage)
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFutures()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['back_adjusted'] = float(split[1])
data['spliced'] = float(split[2])
data.Value = float(split[1])
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))