| Overall Statistics |
|
Total Orders 2718 Average Win 0.17% Average Loss -0.15% Compounding Annual Return -0.795% Drawdown 6.500% Expectancy -0.016 Start Equity 100000 End Equity 96086.38 Net Profit -3.914% Sharpe Ratio -1.871 Sortino Ratio -2.371 Probabilistic Sharpe Ratio 0.044% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.12 Alpha -0.042 Beta 0.013 Annual Standard Deviation 0.022 Annual Variance 0 Information Ratio -0.792 Tracking Error 0.142 Treynor Ratio -3.299 Total Fees $3874.88 Estimated Strategy Capacity $2900000.00 Lowest Capacity Asset EWD R735QTJ8XC9X Portfolio Turnover 14.86% Drawdown Recovery 0 |
#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(self.end_date - timedelta(5*365))
self.set_cash(100_000)
# Define some parameters.
self._threshold = 0.5
formation_period = 121
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
]
# Add the Country ETFs.
for ticker in tickers:
security = self.add_equity(ticker, Resolution.DAILY)
# Add a RollingWindow to track historical prices over the formation period.
security.history = RollingWindow[float](formation_period+1)
# Warm up the RollingWindow
for bar in self.history[TradeBar](security, formation_period+1):
security.history.add(bar.close)
# Add a consolidator to update the RollingWindow each day going forward.
self.consolidate(
security, Resolution.DAILY,
lambda bar: self.securities[bar.symbol].history.add(bar.close)
)
# Define all the possible pairs of ETFs.
self._pairs = [Pair(self, a, b) for a, b in list(it.combinations(self.securities.values(), 2))]
self._selected_pairs = []
# Add a Scheduled Event to select pairs at the start of each month.
self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.midnight, self._select_pairs)
def on_data(self, data):
# Trade the selected pairs.
for pair in self._selected_pairs:
index_a = pair.a_norm[-1]
index_b = pair.b_norm[-1]
delta = pair.distance()
if index_a - index_b > self._threshold*delta:
pair.enter(-1, 1)
elif index_a - index_b < -self._threshold*delta:
pair.enter(1, -1)
else: # Close the position when prices revert back.
pair.exit()
def _select_pairs(self):
# At the start of each month, select pairs with the
# smallest historical distance.
ready_pairs = [p for p in self._pairs if p.is_ready]
new_selected_pairs = sorted(ready_pairs, key=lambda p: p.distance())[:5]
for pair in self._selected_pairs:
if pair not in new_selected_pairs:
pair.exit()
self._selected_pairs = new_selected_pairs
class Pair:
def __init__(self, algorithm, security_a, security_b):
self._algorithm = algorithm
self._a = security_a
self._b = security_b
self._ticket_by_security = {}
@property
def is_ready(self):
return self._a.history.is_ready and self._b.history.is_ready
def distance(self):
a = np.array(list(self._a.history)[::-1])
b = np.array(list(self._b.history)[::-1])
# compute normalized cumulative price indices
self.a_norm = a[1:] / a[0]
self.b_norm = b[1:] / b[0]
return 1/120*sum(abs(self.a_norm - self.b_norm))
def enter(self, direction_a, direction_b):
if self._ticket_by_security:
# If the direction of each security is the same, do nothing.
if np.sign(self._ticket_by_security[self._a].quantity) == direction_a:
return
# Otherwise exit the current positions.
self.exit()
# Enter the new position.
order_size = 0.1 * self._algorithm.portfolio.total_portfolio_value
quantity_a = direction_a * int(order_size / self._a.price)
quantity_b = direction_b * int(order_size / self._b.price)
if quantity_a and quantity_b:
self._ticket_by_security = {
self._a: self._algorithm.market_order(self._a, quantity_a),
self._b: self._algorithm.market_order(self._b, quantity_b)
}
def exit(self):
for security, ticket in self._ticket_by_security.items():
self._algorithm.market_order(security, -ticket.quantity)
self._ticket_by_security.clear()