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