Overall Statistics |
Total Trades 6 Average Win 0% Average Loss 0% Compounding Annual Return -4.808% Drawdown 0.300% Expectancy 0 Net Profit -0.054% Sharpe Ratio -9.007 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0.001 Beta -0.755 Annual Standard Deviation 0.004 Annual Variance 0 Information Ratio -9.22 Tracking Error 0.009 Treynor Ratio 0.046 Total Fees $6.00 Estimated Strategy Capacity $4300000.00 Lowest Capacity Asset CMG TFNWQ96IBZ39 |
# Pair Trading from AlgorithmImports import * import numpy as np import pandas as pd import statsmodels.api as sm from sklearn.decomposition import PCA from sklearn.preprocessing import * from statsmodels.tsa.stattools import adfuller, coint, grangercausalitytests import statsmodels.api as sm import datetime # https://medium.com/@financialnoob/granger-causality-test-in-pairs-trading-bf2fd939e575 class PairTradingGrangerAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 2, 8) self.SetEndDate(2019, 2, 11) self.SetCash(100000) self.lookback = 500 self.num_equities = 20 self.selected_pairs = None self.UniverseSettings.Resolution = Resolution.Minute self.AddUniverse(self.CoarseSelection) self.AddEquity("SPY") self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 10), self.EveryDayBeforeMarketClose) self.day = 0 def CoarseSelection(self, coarse): sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True) symbols = [ x.Symbol for x in sortedByDollarVolume if x.Price > 50 and x.DollarVolume > 1000000000 ] self.symbols = symbols[:self.num_equities] return [x for x in self.symbols] def Normalize(self, df_history): scaler = MinMaxScaler() scaler.fit(df_history) history = scaler.transform(df_history) history = pd.DataFrame(history) history.columns = df_history.columns.values.tolist() return history # returns array with cointegrated pairs def CreateCointegratedPairs(self, stocks, df_log_prices): selected_pairs = [] selected_stocks = [] for s1 in stocks: for s2 in stocks: if (s1 != s2) and (s1 not in selected_stocks) and (s2 not in selected_stocks): if (coint(df_log_prices[s1], df_log_prices[s2])[1] < 0.1): selected_stocks.append(s1) selected_stocks.append(s2) selected_pairs.append((s1, s2)) return selected_pairs # returns array with cointegrated pairs def SelectGrangerPairs(self, selected_pairs, df_log_prices): maxlag = 1 limit = 0.1 selected_pairs_gc = [] for index, pair in enumerate(selected_pairs): s1 = pair[0] s2 = pair[1] if s1 in df_log_prices.columns and s2 in df_log_prices.columns: gct12 = grangercausalitytests(df_log_prices[[s1, s2]], maxlag=maxlag) pvals12 = [gct12[x][0]['ssr_ftest'][1] for x in range(1, maxlag + 1)] pvals12 = np.array(pvals12) if len(pvals12[pvals12 < limit]) > 0: selected_pairs_gc.append((s1, s2)) else: # switch Granger-leader and Granger-follower gct21 = grangercausalitytests(df_log_prices[[s2, s1]], maxlag=maxlag) pvals21 = [gct21[x][0]['ssr_ftest'][1] for x in range(1, maxlag + 1)] pvals21 = np.array(pvals21) if len(pvals21[pvals21 < limit]) > 0: selected_pairs_gc.append((s2, s1)) return selected_pairs_gc def CalculateWeights(self, selected_pairs, df_log_prices): r = 1 positions = pd.Series() for index, pair in enumerate(selected_pairs): s1 = pair[0] s2 = pair[1] self.AddEquity(s1) self.AddEquity(s2) if s1 in df_log_prices.columns and s2 in df_log_prices.columns: model = sm.OLS(df_log_prices[s1], sm.add_constant(df_log_prices[s2])) res = model.fit() mu = res.resid.mean() # spread historical mean sigma = res.resid.std() # spread historical sd # calculate spread spread = df_log_prices[s1] - res.predict(sm.add_constant(df_log_prices[s2])) spread = spread.iloc[-1] if spread > mu + r * sigma: positions[s1] = -1 positions[s2] = 1 elif spread < mu - r * sigma: positions[s1] = 1 positions[s2] = -1 else: positions[s1] = 0 positions[s2] = 0 if positions.abs().sum() == 0: weights = positions else: weights = positions * (0.475 / positions.abs().sum()) return weights def EveryDayBeforeMarketClose(self): if not self.IsWarmingUp and self.Time.date(): self.Trade() def Trade(self): df_history = self.History(self.symbols, self.lookback, Resolution.Daily).close.unstack(level=0) df_history_last = self.History(self.symbols, 1, Resolution.Minute).close.unstack(level=0) df_history = pd.concat([df_history, df_history_last]) df_history = df_history.dropna('columns') log_prices = self.Normalize(df_history) self.day += 1 if self.selected_pairs is None or len(self.selected_pairs) or self.day % 10 == 0: stocks = df_history.columns pairs = self.CreateCointegratedPairs(stocks, log_prices) self.selected_pairs = self.SelectGrangerPairs(pairs, log_prices) weights = self.CalculateWeights(self.selected_pairs, log_prices) # self.Debug("weights=" + str(weights)) portfolioTargets = [] for symbol, weight in weights.items(): self.SetHoldings(symbol, weight)