Overall Statistics |
Total Trades 11 Average Win 0% Average Loss 0% Compounding Annual Return -21.321% Drawdown 15.200% Expectancy 0 Net Profit -7.639% Sharpe Ratio -0.926 Probabilistic Sharpe Ratio 10.011% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.161 Beta 0.089 Annual Standard Deviation 0.18 Annual Variance 0.032 Information Ratio -0.361 Tracking Error 0.282 Treynor Ratio -1.88 Total Fees $34.65 |
import numpy as np import pandas as pd import statsmodels.api as sm from sklearn.decomposition import PCA class PcaStatArbitrageAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2001, 1, 1) # Set Start Date self.SetEndDate(2001, 5, 10) # Set End Date self.SetCash(100000) # Set Strategy Cash self.nextRebalance = self.Time # Initialize next rebalance time self.rebalance_days = 0 # Rebalance every 30 days self.lookback = 61 # Length(days) of historical data self.num_components = 15 # Number of principal components in PCA self.num_equities = 500 # Number of the equities pool self.weights_buy = pd.DataFrame() # Pandas data frame (index: symbol) that stores the weight self.weights_sell = pd.DataFrame() self.weights_liquidate = pd.DataFrame() self.UniverseSettings.Resolution = Resolution.Daily # Use hour resolution for speed self.AddUniverse(self.CoarseSelectionAndPCA) # Coarse selection + PCA #self.AddRiskManagement(MaximumDrawdownPercentPerSecurity(0.03)) def CoarseSelectionAndPCA(self, coarse): '''Drop securities which have too low prices. Select those with highest by dollar volume. Finally do PCA and get the selected trading symbols. ''' # Before next rebalance time, just remain the current universe #if self.Time < self.nextRebalance: # return Universe.Unchanged ### Simple coarse selection first # Sort the equities in DollarVolume decendingly selected = sorted([x for x in coarse if x.Price > 5], key=lambda x: x.DollarVolume, reverse=True) symbols = [x.Symbol for x in selected[:self.num_equities]] ### After coarse selection, we do PCA and linear regression to get our selected symbols # Get historical data of the selected symbols history = self.History(symbols, self.lookback, Resolution.Daily).close.unstack(level=0) # Select the desired symbols and their weights for the portfolio from the coarse-selected symbols try: self.weights_buy,self.weights_sell,self.weights_liquidate = self.GetWeights(history) except: self.weights_buy,self.weights_sell,self.weights_liquidate = pd.DataFrame(),pd.DataFrame(),pd.DataFrame() # If there is no final selected symbols, return the unchanged universe if self.weights_buy.empty or self.weights_sell.empty or self.weights_liquidate.empty: return Universe.Unchanged return [x for x in symbols if str(x) in self.weights_buy.index or str(x) in self.weights_sell.index or str(x) in self.weights_liquidate] def GetWeights(self, history): ''' Get the finalized selected symbols and their weights according to their level of deviation of the residuals from the linear regression after PCA for each symbol ''' # Sample data for PCA sample = history.dropna(axis=1).pct_change().dropna() sample_mean = sample.mean() sample_std = sample.std() sample = ((sample-sample_mean)/(sample_std)) * 252 **(1/2) # Center it column-wise # Fit the PCA model for sample data model = PCA().fit(sample) #Distributing eigenportfolios EigenPortfolio = pd.DataFrame(model.components_) EigenPortfolio.columns = sample.columns EigenPortfolio = EigenPortfolio/sample_std EigenPortfolio = ( EigenPortfolio.T / EigenPortfolio.sum(axis=1) ) # Get the first n_components factors factors = np.dot(sample, EigenPortfolio)[:,:self.num_components] # Add 1's to fit the linear regression (intercept) factors = sm.add_constant(factors) # Train Ordinary Least Squares linear model for each stock OLSmodels = {ticker: sm.OLS(sample[ticker], factors).fit() for ticker in sample.columns} # Get the residuals from the linear regression after PCA for each stock resids = pd.DataFrame({ticker: model.resid for ticker, model in OLSmodels.items()}) # Get the OU parameters shifted_residuals = resids.cumsum().iloc[1:,:] resids = resids.cumsum().iloc[:-1,:] resids.index = shifted_residuals.index OLSmodels2 = {ticker: sm.OLS(resids[ticker],sm.add_constant(shifted_residuals[ticker])).fit() for ticker in resids.columns} # Get the new residuals resids2 = pd.DataFrame({ticker: model.resid for ticker, model in OLSmodels2.items()}) # Get the mean reversion parameters a = pd.DataFrame({ticker : model.params[0] for ticker , model in OLSmodels2.items()},index=["a"]) b = pd.DataFrame({ticker: model.params[1] for ticker , model in OLSmodels2.items()},index=["a"]) b = b[ b < 0.97 ].dropna() e = resids2.std() * 252 **( 1 / 2) k = -np.log(b) * 252 k = k[ k > 252 / 30].dropna() #Get the z-score var = (e**2 /(2 * k) )*(1 - np.exp(-2 * k * 252)) num = -a * np.sqrt(1 - b**2) den =( ( 1-b ) * np.sqrt( var )).dropna(axis=1) m = ( a / ( 1 - b ) ).dropna(axis=1) zscores=(num / den ).iloc[0,:]# zscores of the most recent day # Get the stocks far from mean (for mean reversion) selected_buy = zscores[zscores < -1.5] selected_sell = zscores[zscores > 1.5] selected_liquidate = zscores[abs(zscores) < 0.50 ] #summing all orders sum_orders = selected_buy.abs().sum() + selected_sell.abs().sum() # Return the weights for each selected stock weights_buy = selected_buy * (1 / sum_orders) weights_sell = selected_sell * (1 / sum_orders) weights_liquidate = selected_liquidate return weights_buy.sort_values(),weights_sell.sort_values(),weights_liquidate.sort_values() def OnData(self, data): ''' Rebalance every self.rebalance_days ''' ### Do nothing until next rebalance #if self.Time < self.nextRebalance: # return ### Open positions for symbol, weight in self.weights_buy.items(): # If the residual is way deviated from 0, we enter the position in the opposite way (mean reversion) if self.Securities[symbol].Invested: continue self.SetHoldings(symbol, -weight) ### short positions for symbol, weight in self.weights_sell.items(): if self.Securities[symbol].Invested: continue self.SetHoldings(symbol,-weight) for symbol, weight in self.weights_liquidate.items(): self.Liquidate(symbol) ### Update next rebalance time #self.nextRebalance = self.Time + timedelta(self.rebalance_days) def OnSecuritiesChanged(self, changes): ''' Liquidate when the symbols are not in the universe ''' for security in changes.RemovedSecurities: if security.Invested: self.Liquidate(security.Symbol, 'Removed from Universe') # self.SetHoldings("SPY", 1)