| 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)