| Overall Statistics |
|
Total Orders 2028 Average Win 0.12% Average Loss -0.13% Compounding Annual Return -0.681% Drawdown 11.500% Expectancy -0.031 Start Equity 100000 End Equity 94945.46 Net Profit -5.055% Sharpe Ratio -0.484 Sortino Ratio -0.598 Probabilistic Sharpe Ratio 0.008% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.93 Alpha -0.008 Beta -0.071 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -0.791 Tracking Error 0.13 Treynor Ratio 0.201 Total Fees $2393.93 Estimated Strategy Capacity $11000.00 Lowest Capacity Asset ADRU SJNVOV70T9B9 Portfolio Turnover 3.66% |
#region imports
from AlgorithmImports import *
from math import floor
from collections import deque
import itertools as it
#endregion
# https://quantpedia.com/Screener/Details/55
class PairsTradingwithCountryETFsAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2011, 1, 1)
self.set_end_date(2018, 8, 1)
self.set_cash(100000)
# choose ten sector ETFs
tickers = ["GAF", # SPDR S&P Emerging Middle East & Africa ETF 2007.4
"ENZL", # iShares MSCI New Zealand Investable Market Index Fund 2010.9
"NORW", # Global X FTSE Norway 30 ETF 2011
"EWY", # iShares MSCI South Korea Index ETF 2000.6
"EWP", # iShares MSCI Spain Index ETF 1996
"EWD", # iShares MSCI Sweden Index ETF 1996
"EWL", # iShares MSCI Switzerland Index ETF 1996
"GXC", # SPDR S&P China ETF 2007.4
"EWC", # iShares MSCI Canada Index ETF 1996
"EWZ", # iShares MSCI Brazil Index ETF 2000.8
# "AND", # Global X FTSE Andean 40 ETF 2011.3
"AIA", # iShares S&P Asia 50 Index ETF 1996
"EWO", # iShares MSCI Austria Investable Mkt Index ETF 1996
"EWK", # iShares MSCI Belgium Investable Market Index ETF 1996
"ECH", # iShares MSCI Chile Investable Market Index ETF 2018 2008
# "EGPT", # Market Vectors Egypt Index ETF 2011
"EWJ", # iShares MSCI Japan Index ETF 1999
"EZU", # iShares MSCI Eurozone ETF 2000
"EWW", # iShares MSCI Mexico Inv. Mt. Idx 2000
# "ERUS", # iShares MSCI Russia ETF 2011
"IVV", # iShares S&P 500 Index 2001
"AAXJ", # iShares MSCI All Country Asia ex Japan Index ETF 2008.8
"EWQ", # iShares MSCI France Index ETF 2000
"EWH", # iShares MSCI Hong Kong Index ETF 1999
# "EPI", # WisdomTree India Earnings ETF 2008.3
"EIDO", # iShares MSCI Indonesia Investable Market Index ETF 2008.3
"EWI", # iShares MSCI Italy Index ETF 1996
"ADRU"] # BLDRS Europe 100 ADR Index ETF 2003
self._threshold = 0.5
self._symbols = []
for i in tickers:
self._symbols.append(self.add_equity(i, Resolution.DAILY).symbol)
formation_period = 121
self._history_price = {}
for symbol in self._symbols:
hist = self.history([symbol.value], formation_period+1, Resolution.DAILY)
if hist.empty:
self._symbols.remove(symbol)
else:
self._history_price[symbol.value] = deque(maxlen=formation_period)
for tuple in hist.loc[str(symbol)].itertuples():
self._history_price[symbol.value].append(float(tuple.close))
if len(self._history_price[symbol.value]) < formation_period:
self._symbols.remove(symbol)
self._history_price.pop(symbol.value)
self._symbol_pairs = list(it.combinations(self._symbols, 2))
# Add the benchmark
self.add_equity("SPY", Resolution.DAILY)
self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.after_market_open("SPY"), self._rebalance)
self._sorted_pairs = None
def on_data(self, data):
# Update the price series everyday
for symbol in self._symbols:
if data.bars.contains_key(symbol) and symbol.value in self._history_price:
self._history_price[symbol.value].append(float(data[symbol].close))
if self._sorted_pairs is None: return
for i in self._sorted_pairs:
pair = Pair(i[0], i[1], self._history_price[i[0].value], self._history_price[i[1].value])
index_a = pair.index_a[-1]
index_b = pair.index_b[-1]
delta = pair.distance()
if index_a - index_b > self._threshold*delta:
if not self.portfolio[pair.symbol_a].invested and not self.portfolio[pair.symbol_b].invested:
ratio = self.portfolio[pair.symbol_a].price / self.portfolio[pair.symbol_b].price
quantity = int(self.calculate_order_quantity(pair.symbol_a, 0.05))
self.sell(pair.symbol_a, quantity)
self.buy(pair.symbol_b, floor(ratio*quantity))
elif index_a - index_b < -self._threshold*delta:
if not self.portfolio[pair.symbol_a].invested and not self.portfolio[pair.symbol_b].invested:
ratio = self.portfolio[pair.symbol_b].price / self.portfolio[pair.symbol_a].price
quantity = int(self.calculate_order_quantity(pair.symbol_b, 0.05))
self.sell(pair.symbol_b, quantity)
self.buy(pair.symbol_a, floor(ratio*quantity))
# the position is closed when prices revert back
elif self.portfolio[i[0]].invested and self.portfolio[i[1]].invested:
self.liquidate(pair.symbol_a)
self.liquidate(pair.symbol_b)
def _rebalance(self):
# schedule the event to fire every half year to select pairs with the smallest historical distance
distances = {}
for i in self._symbol_pairs:
if i[0].value in self._history_price and i[1].value in self._history_price:
distances[i] = Pair(i[0], i[1], self._history_price[i[0].value], self._history_price[i[1].value]).distance()
self._sorted_pairs = sorted(distances, key=lambda x: distances[x])[:5]
class Pair:
def __init__(self, symbol_a, symbol_b, price_a, price_b):
self.symbol_a = symbol_a
self.symbol_b = symbol_b
price_a = np.array(price_a)
price_b = np.array(price_b)
# compute normalized cumulative price indices
self.index_a = np.cumprod(price_a[1:]/price_a[:-1])
self.index_b = np.cumprod(price_b[1:]/price_b[:-1])
def distance(self):
return 1/120*sum(abs(self.index_a - self.index_b))