Overall Statistics |
Total Trades 121 Average Win 0.59% Average Loss -0.33% Compounding Annual Return 51.725% Drawdown 8.800% Expectancy 0.526 Net Profit 7.461% Sharpe Ratio 1.541 Probabilistic Sharpe Ratio 55.692% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.76 Alpha 0.404 Beta 0.595 Annual Standard Deviation 0.247 Annual Variance 0.061 Information Ratio 1.817 Tracking Error 0.231 Treynor Ratio 0.64 Total Fees $164.60 Estimated Strategy Capacity $73000000.00 Lowest Capacity Asset GOOG T1AZ164W5VTX |
#region imports from AlgorithmImports import * #endregion from QuantConnect import Resolution, Extensions from QuantConnect.Algorithm.Framework.Alphas import * from QuantConnect.Algorithm.Framework.Portfolio import * from itertools import groupby from datetime import datetime, timedelta from pytz import utc UTCMIN = datetime.min.replace(tzinfo=utc) #endregion class InsightWeigtedPortfolio(PortfolioConstructionModel): def __init__(self, rebalancingFunc = Expiry.EndOfMonth): self.insightCollection = InsightCollection() self.removedSymbols = [] """ self.nextRebalance = None self.rebalancingFunc = rebalancingFunc """ def CreateTargets(self, algorithm, insights): targets = [] if len(insights) == 0: return targets # apply rebalancing logic """ if self.nextRebalance is not None and algorithm.Time < self.nextRebalance: return targets self.nextRebalance = self.rebalancingFunc(algorithm.Time) """ # here we get the new insights and add them to our insight collection for insight in insights: self.insightCollection.Add(insight) # create flatten target for each security that was removed from the universe if len(self.removedSymbols) > 0: #get the invested tickers invested = [x.Symbol.Value for x in algorithm.Portfolio.Values if x.Invested] #check if the tickers is in invested, otherwise, do nothing universeDeselectionTargets = [ PortfolioTarget(symbol, 0) for symbol in self.removedSymbols if symbol.Value in invested] targets.extend(universeDeselectionTargets) algorithm.Log('(Portfolio module) liquidating: ' + str([x.Value for x in self.removedSymbols]) + ' if they are active, due to not being in the universe') self.removedSymbols = [] expiredInsights = self.insightCollection.RemoveExpiredInsights(algorithm.UtcTime) expiredTargetsLog = [] expiredTargets = [] for symbol, f in groupby(expiredInsights, lambda x: x.Symbol): if not self.insightCollection.HasActiveInsights(symbol, algorithm.UtcTime): expiredTargets.append(PortfolioTarget(symbol, 0)) expiredTargetsLog.append(symbol) continue algorithm.Log(f'(Portfolio module) sold {expiredTargetsLog} due to insight being expired') targets.extend(expiredTargets) # get insight that have not expired of each symbol that is still in the universe activeInsights = self.insightCollection.GetActiveInsights(algorithm.UtcTime) # get the last generated active insight for each insight, and not symbol lastActiveInsights = [] for symbol, g in groupby(activeInsights): lastActiveInsights.append(sorted(g, key = lambda x: x.GeneratedTimeUtc)[-1]) calculatedTargets = {} for insight in lastActiveInsights: if insight.Symbol not in calculatedTargets.keys(): calculatedTargets[insight.Symbol] = insight.Direction else: calculatedTargets[insight.Symbol] += insight.Direction # determine target percent for the given insights weightFactor = 1.0 weightSums = sum(abs(direction) for symbol, direction in calculatedTargets.items()) boughtTargetsLog = [] if weightSums > 1: weightFactor = 1 / weightSums for symbol, weight in calculatedTargets.items(): allocationPercent = weight * weightFactor target = PortfolioTarget.Percent(algorithm, symbol, allocationPercent) boughtTargetsLog.append(symbol) targets.append(target) algorithm.Log(f'(Portfolio module) Bought {boughtTargetsLog} stocks, that expires at {Expiry.EndOfMonth}') return targets def OnSecuritiesChanged(self, algorithm, changes): newRemovedSymbols = [x.Symbol for x in changes.RemovedSecurities if x.Symbol not in self.removedSymbols] # get removed symbol and invalidate them in the insight collection self.removedSymbols.extend(newRemovedSymbols) self.insightCollection.Clear(self.removedSymbols) removedList = [x.Value for x in self.removedSymbols] algorithm.Log('(Portfolio module) securities removed from Universe: ' + str(removedList))
from AlgorithmImports import * class MarketOrderModel(ExecutionModel): def __init__(self): self.targetsCollection = PortfolioTargetCollection() def Execute(self, algorithm, targets): # for performance we check count value, OrderByMarginImpact and ClearFulfilled are expensive to call self.targetsCollection.AddRange(targets) if self.targetsCollection.Count > 0: for target in self.targetsCollection.OrderByMarginImpact(algorithm): security = algorithm.Securities[target.Symbol] # calculate remaining quantity to be ordered quantity = OrderSizing.GetUnorderedQuantity(algorithm, target, security) if quantity != 0: aboveMinimumPortfolio = BuyingPowerModelExtensions.AboveMinimumOrderMarginPortfolioPercentage(security.BuyingPowerModel, security, quantity, algorithm.Portfolio, algorithm.Settings.MinimumOrderMarginPortfolioPercentage) if aboveMinimumPortfolio: algorithm.MarketOrder(security, quantity) self.targetsCollection.ClearFulfilled(algorithm)
from AlgorithmImports import * import pandas as pd import numpy as np import statsmodels.api as sm from enum import Enum from collections import deque class PairsTradingAlphaModel(AlphaModel): def __init__(self, lookback, resolution, prediction, minimumCointegration, std, stoplossStd): self.resolution = resolution self.lookback = lookback self.prediction = prediction self.minimumCointegration = minimumCointegration self.upperStd = std self.lowerStd = -abs(std) self.upperStoploss = stoplossStd self.lowerStoploss = -abs(stoplossStd) self.mean = 0 self.pairs = {} self.Securities = [] def Update(self, algorithm, data): #implement the update features here. Update the RollingWindow insights = [] for symbol in self.Securities: if not algorithm.IsMarketOpen(symbol.Symbol): return [] #update the rolling window with same slices, or ols wont fit for keys, symbolData in self.pairs.items(): if keys[0] in data.Bars and keys[1] in data.Bars: symbolData.symbol1Bars.Add(data[keys[0]]) symbolData.symbol2Bars.Add(data[keys[1]]) #Rebalacing logic here? if symbolData.symbol1Bars.IsReady and symbolData.symbol2Bars.IsReady: state = symbolData.state S1 = algorithm.PandasConverter.GetDataFrame[IBaseDataBar](symbolData.symbol1Bars).close.unstack(level=0) S2 = algorithm.PandasConverter.GetDataFrame[IBaseDataBar](symbolData.symbol2Bars).close.unstack(level=0) S1 = S1.dropna(axis=1) S2 = S2.dropna(axis=1) S1 = sm.add_constant(S1) results = sm.OLS(S2, S1).fit() S1 = S1[keys[0]] S2 = S2[keys[1]] b = results.params[keys[0]] #If S2 moves higher, the spread becomes higher. Therefore, short S2, long S1 if spread moves up, mean reversion spread = S2 - b * S1 zscore = self.ZScore(spread) insight, state = self.TradeLogic(keys[0], keys[1], zscore[-1], state) #if we have changed state, append insight if symbolData.state != state: insights.extend(insight) symbolData.state = state else: continue return insights def TradeLogic(self, stock1, stock2, zscore, state): insights = [] if state == State.FlatRatio: if zscore > self.upperStd: longS1 = Insight.Price(stock1, self.prediction, InsightDirection.Up, weight=1) shortS2 = Insight.Price(stock2, self.prediction, InsightDirection.Down, weight=-1) return Insight.Group(longS1, shortS2), State.LongRatio elif zscore < self.lowerStd: shortS1 = Insight.Price(stock1, self.prediction, InsightDirection.Down, weight=-1) longS2 = Insight.Price(stock2, self.prediction, InsightDirection.Up, weight=1) return Insight.Group(shortS1, longS2), State.ShortRatio else: return [], State.FlatRatio if state == State.ShortRatio: if zscore > self.mean: #liquidate flatS1 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) flatS2 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) return Insight.Group(flatS1, flatS2), State.FlatRatio else: return [], State.ShortRatio """ elif zscore > self.lowerStoploss: #stop loss #when stop loss is trickered, we dont send the state to flat, as we will wait for the spread to cross below the mean, before doing that flatS1 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) flatS2 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) return Insight.Group(flatS1, flatS2), AlphaSymbolData.State.ShortRatio """ if state == State.LongRatio: if zscore < self.mean: #liquidate flatS1 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) flatS2 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) return Insight.Group(flatS1, flatS2), State.ShortRatio else: return [], State.LongRatio """ elif zscore < self.lowerStoploss: #stop loss #when stop loss is trickered, we dont send the state to flat, as we will wait for the spread to cross below the mean, before doing that flatS1 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) flatS2 = Insight.Price(stock1, self.prediction, InsightDirection.Flat, weight=0) return Insight.Group(flatS1, flatS2), AlphaSymbolData.State.LongRatio """ def ZScore(self, series): return (series - series.mean()) / np.std(series) def OnSecuritiesChanged(self, algorithm, changes): for security in changes.AddedSecurities: self.Securities.append(security) for security in changes.RemovedSecurities: if security in self.Securities: self.Securities.remove(security) symbols = [x.Symbol for x in self.Securities] history = algorithm.History(symbols, self.lookback, self.resolution).close.unstack(level=0) #method to calculate how cointegrated the stocks are n = history.shape[1] keys = history.columns #smart looping technique. Looks at every stocks and tests it cointegration. It is kind of slow though, could be improved. for i in range(n): for ii in range(i+1, n): stock1 = history[keys[i]] stock2 = history[keys[ii]] asset1 = keys[i] asset2 = keys[ii] pair_symbol = (asset1, asset2) invert = (asset2, asset1) if pair_symbol in self.pairs or invert in self.pairs: continue if stock1.hasnans or stock2.hasnans: algorithm.Debug(f'WARNING! {asset1} and {asset2} has Nans. Did not perform coint') continue #The cointegration part, that calculates cointegration between 2 stocks result = sm.tsa.stattools.coint(stock1, stock2) pvalue = result[1] if pvalue < self.minimumCointegration: self.pairs[pair_symbol] = AlphaSymbolData(algorithm, asset1, asset2, 500, self.resolution) for security in changes.RemovedSecurities: keys = [k for k in self.pairs.keys() if security.Symbol in k] for key in keys: self.pairs.pop(key) class AlphaSymbolData: def __init__(self, algorithm, symbol1, symbol2, lookback, resolution): self.state = State.FlatRatio self.lookback = lookback self.symbol1 = symbol1 self.symbol2 = symbol2 self.symbol1Bars = RollingWindow[IBaseDataBar](lookback) self.symbol2Bars = RollingWindow[IBaseDataBar](lookback) class State(Enum): ShortRatio = -1 FlatRatio = 0 LongRatio = 1
#region imports from AlgorithmImports import * #endregion class NoRiskManagment(RiskManagementModel): def ManageRisk(self, algorithm, targets): return []
from AlgorithmImports import * from EqualPCM import InsightWeigtedPortfolio from PairsTradingAlpha import PairsTradingAlphaModel from ExecutionModel import MarketOrderModel from RiskModel import NoRiskManagment from datetime import timedelta ### <summary> ### Framework algorithm that uses the PearsonCorrelationPairsTradingAlphaModel. ### This model extendes BasePairsTradingAlphaModel and uses Pearson correlation ### to rank the pairs trading candidates and use the best candidate to trade. ### </summary> class PairsTradingV2(QCAlgorithm): '''Framework algorithm that uses the PearsonCorrelationPairsTradingAlphaModel. This model extendes BasePairsTradingAlphaModel and uses Pearson correlation to rank the pairs trading candidates and use the best candidate to trade.''' def Initialize(self): self.Debug('Algorithm started. Wait for warmup') self.SetStartDate(2018, 12, 1) self.SetEndDate(2019, 2, 1) self.SetWarmup(100) self.num_coarse = 20 self.UniverseSettings.Resolution = Resolution.Hour self.AddUniverse(self.CoarseUniverse) self.SetAlpha(PairsTradingAlphaModel(lookback = 200, resolution = Resolution.Hour, prediction = timedelta(days=20), minimumCointegration = .05, std=2, stoplossStd=2.5 )) self.SetPortfolioConstruction(InsightWeigtedPortfolio()) self.SetExecution(MarketOrderModel()) self.SetRiskManagement(NoRiskManagment()) self.rebalancingFunc = Expiry.EndOfMonth self.nextRebalance = None def CoarseUniverse(self, coarse): if self.nextRebalance is not None and self.Time < self.nextRebalance: return Universe.Unchanged self.nextRebalance == self.rebalancingFunc(self.Time) #Exclude stocks like BRKA that cost 500.000 dollars selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 15], key = lambda x: x.DollarVolume, reverse = True) self.Log(f'(Universe module)Sent {len([x.Symbol for x in selected[:self.num_coarse]])} symbols to the alpha module') return [x.Symbol for x in selected[:self.num_coarse]] def OnEndOfDay(self): self.Plot("Positions", "Num", len([x.Symbol for x in self.Portfolio.Values if self.Portfolio[x.Symbol].Invested])) self.Plot(f"Margin", "Used", self.Portfolio.TotalMarginUsed) self.Plot(f"Margin", "Remaning", self.Portfolio.MarginRemaining) self.Plot(f"Cash", "Remaining", self.Portfolio.Cash)