Overall Statistics |
Total Trades 140 Average Win 0.02% Average Loss -0.02% Compounding Annual Return -21.203% Drawdown 0.700% Expectancy -0.629 Net Profit -0.629% Sharpe Ratio -7.017 Sortino Ratio -5.607 Probabilistic Sharpe Ratio 0.655% Loss Rate 81% Win Rate 19% Profit-Loss Ratio 0.94 Alpha -0.218 Beta 0.032 Annual Standard Deviation 0.026 Annual Variance 0.001 Information Ratio -22.941 Tracking Error 0.056 Treynor Ratio -5.594 Total Fees $208.12 Estimated Strategy Capacity $230000.00 Lowest Capacity Asset BXS R735QTJ8XC9X Portfolio Turnover 122.81% |
# https://quantpedia.com/Screener/Details/12 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 from AlgorithmImports import * class PairsTradingAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2018,1,1) self.SetEndDate(2018,1,10) self.SetCash(100000) tickers = [ 'XLK', 'QQQ', 'BANC', 'BBVA', 'BBD', 'BCH', 'BLX', 'BSBR', 'BSAC', 'SAN', 'CIB', 'BXS', 'BAC', 'BOH', 'BMO', 'BK', 'BNS', 'BKU', 'BBT','NBHC', 'OFG', 'BFR', 'CM', 'COF', 'C', 'VLY', 'WFC', 'WAL', 'WBK','RBS', 'SHG', 'STT', 'STL', 'SCNB', 'STI'] # 'DKT', 'DB', 'EVER', 'KB', 'KEY', , 'MTB', 'BMA', 'MFCB', 'MSL', 'MTU', 'MFG', # 'PVTD', 'PB', 'PFS', 'RF', 'RY', 'RBS', 'SHG', 'STT', 'STL', 'SCNB', 'SMFG', 'STI', # 'SNV', 'TCB', 'TD', 'USB', 'UBS', 'VLY', 'WFC', 'WAL', 'WBK', 'WF', 'YDKN', 'ZBK'] self.threshold = 2 self.symbols = [] for i in tickers: self.symbols.append(self.AddEquity(i, Resolution.Minute).Symbol) self.pairs = {} self.formation_period = 50*60*10 self.history_price = {} for symbol in self.symbols: hist = self.History([symbol], self.formation_period+1, Resolution.Minute) if hist.empty: self.symbols.remove(symbol) else: self.history_price[str(symbol)] = deque(maxlen=self.formation_period) for tuple in hist.loc[str(symbol)].itertuples(): self.history_price[str(symbol)].append(float(tuple.close)) if len(self.history_price[str(symbol)]) < self.formation_period: self.symbols.remove(symbol) self.history_price.pop(str(symbol)) self.symbol_pairs = list(it.combinations(self.symbols, 2)) # Add the benchmark self.AddEquity("SPY", Resolution.Minute) self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.Every(timedelta(hours=1)), self.Rebalance) self.count = 0 self.sorted_pairs = None self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) self.AddRiskManagement(NullRiskManagementModel()) self.SetExecution(ImmediateExecutionModel()) self.SetWarmUp(300) self.Settings.FreePortfolioValuePercentage = 0.20 def OnData(self, data): # Update the price series everyday if self.IsWarmingUp: return for symbol in self.symbols: if data.Bars.ContainsKey(symbol) and str(symbol) in self.history_price: self.history_price[str(symbol)].append(float(data[symbol].Close)) if self.sorted_pairs is None: return for i in self.sorted_pairs: # calculate the spread of two price series spread = np.array(self.history_price[str(i[0])]) - np.array(self.history_price[str(i[1])]) mean = np.mean(spread) std = np.std(spread) if self.Portfolio[i[0]].Price!=0 and self.Portfolio[i[1]].Price!=0: factor = 0.1 ratio = (self.Portfolio[i[0]].Price / self.Portfolio[i[1]].Price) # long-short position is opened when pair prices have diverged by two standard deviations if spread[-1] > mean + self.threshold * std: if not self.Portfolio[i[0]].Invested and not self.Portfolio[i[1]].Invested: quantity = int(self.CalculateOrderQuantity(i[0], 0.2)) self.Sell(i[0], quantity) self.Buy(i[1], floor(factor*ratio*quantity)) elif spread[-1] < mean - self.threshold * std: quantity = int(self.CalculateOrderQuantity(i[0], 0.2)) if not self.Portfolio[i[0]].Invested and not self.Portfolio[i[1]].Invested: self.Sell(i[0], quantity) self.Buy(i[1], floor(factor*ratio*quantity)) # the position is closed when prices revert back elif self.Portfolio[i[0]].Invested and self.Portfolio[i[1]].Invested: self.Liquidate(i[0]) self.Liquidate(i[1]) def Rebalance(self): distances = {} if len(self.symbol_pairs) > 2: for i in self.symbol_pairs: try: distances[i] = Pair(i[0], i[1], self.history_price[str(i[0])], self.history_price[str(i[1])]).distance() except KeyError as e: continue self.sorted_pairs = sorted(distances, key = lambda x: distances[x])[:4] 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 = price_a self.price_b = price_b def distance(self): # calculate the sum of squared deviations between two normalized price series norm_a = np.array(self.price_a)/self.price_a[0] norm_b = np.array(self.price_b)/self.price_b[0] return sum((norm_a - norm_b)**2)