Overall Statistics
Total Trades
757
Average Win
0.69%
Average Loss
-0.71%
Compounding Annual Return
-2.694%
Drawdown
39.800%
Expectancy
-0.169
Net Profit
-18.721%
Sharpe Ratio
-0.135
Loss Rate
58%
Win Rate
42%
Profit-Loss Ratio
0.98
Alpha
-0.201
Beta
9.228
Annual Standard Deviation
0.134
Annual Variance
0.018
Information Ratio
-0.283
Tracking Error
0.134
Treynor Ratio
-0.002
Total Fees
$2413.12
 
 
# https://quantpedia.com/Screener/Details/55
import numpy as np
import pandas as pd
from scipy import stats
from math import floor
from datetime import timedelta
from collections import deque
import itertools as it
from decimal import Decimal

class PairsTradingwithCountryETFsAlgorithm(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2011, 1, 1)  
        self.SetEndDate(2018, 8, 1)  
        self.SetCash(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.AddEquity(i, Resolution.Daily).Symbol)
        
        self.pairs = {}
        self.formation_period = 121

        self.history_price = {}
        for symbol in self.symbols:
            hist = self.History([symbol.Value], self.formation_period+1, Resolution.Daily)
            if hist.empty: 
                self.symbols.remove(symbol)
            else:
                self.history_price[symbol.Value] = deque(maxlen=self.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]) < self.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.AddEquity("SPY", Resolution.Daily) 
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY"), self.Rebalance)
        self.sorted_pairs = None


    def OnData(self, data):
        # Update the price series everyday
        for symbol in self.symbols:
            if data.Bars.ContainsKey(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.CalculateOrderQuantity(pair.symbol_a, 0.2))
                    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.CalculateOrderQuantity(pair.symbol_b, 0.2))
                    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
        self.price_a = np.array(price_a)
        self.price_b = np.array(price_b)
        # compute normalized cumulative price indices 
        self.index_a = np.cumprod(self.price_a[1:]/self.price_a[:-1])
        self.index_b = np.cumprod(self.price_b[1:]/self.price_b[:-1])
        
    
    def distance(self):
        return 1/120*sum(abs(self.index_a -self.index_b))