Overall Statistics |
Total Trades 1327 Average Win 0.36% Average Loss -0.28% Compounding Annual Return 12.702% Drawdown 22.600% Expectancy 0.257 Net Profit 78.701% Sharpe Ratio 0.831 Probabilistic Sharpe Ratio 30.580% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.28 Alpha 0.039 Beta 0.571 Annual Standard Deviation 0.137 Annual Variance 0.019 Information Ratio -0.135 Tracking Error 0.12 Treynor Ratio 0.199 Total Fees $1327.00 |
import numpy as np import pandas as pd import statsmodels.api as sm from QuantConnect.Data.UniverseSelection import * from scipy.stats import linregress from datetime import datetime, timedelta class Momentum(QCAlgorithm): def Initialize(self): self.reb1 = 1 # set the flag for momentum stock rebalancement self.initial = 0 self.scale_equities = 1 self.target_vol = 0.125 self.num_coarse = 500 # Number of stocks to pass CoarseSelection process self.num_fine = 20 # Number of stocks to long self.SetStartDate(2017, 1, 1) # Set Start Date self.SetEndDate(datetime.now()) # Set End Date self.SetCash(15000) # Set Strategy Cash self.AddUniverse(self.CoarseSelectionFunction,self.FineSelectionFunction) self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.gld = self.AddEquity("GLD", Resolution.Minute).Symbol # gold hedge self.iei = self.AddEquity("IEI", Resolution.Minute).Symbol # bond hedge self.tlt = self.AddEquity("TLT", Resolution.Minute).Symbol # long term bond hedge self.hedge = [self.gld, self.iei, self.tlt] self.Schedule.On(self.DateRules.MonthStart(self.spy), self.TimeRules.AfterMarketOpen(self.spy,5), Action(self.rebalance)) self.SetSecurityInitializer(self.CustomSecurityInitializer) self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash) def CustomSecurityInitializer(self, security): security.SetDataNormalizationMode(DataNormalizationMode.SplitAdjusted) def CoarseSelectionFunction(self, coarse): # if the rebalance flag is not 1, return null list to save time. if self.reb1 != 1: return Universe.Unchanged # make universe selection once a month sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True) filtered = [x.Symbol for x in sortedByDollarVolume if x.HasFundamentalData] # filtered down to the 500 most liquid stocks return filtered[:self.num_coarse] def FineSelectionFunction(self, fine): # return null list if it's not time to rebalance if self.reb1 != 1: return Universe.Unchanged # drop counter (will update back to 1 after rebalancement has occurred) self.reb1 = 0 # create dictionaries to store the indicator values stock_filter = {} # filter by market cap self.market_cap = [x for x in fine if x.MarketCap > 2e9] # prepare data hist_spy = self.History(self.spy, timedelta(days=760), Resolution.Daily).droplevel(level=0) hist_spy.rename(columns={"close":"spy close"}, inplace=True) # we now want to calculate the monthly market returns SPY_monthly_returns = hist_spy["spy close"].resample("M").ffill().pct_change().dropna() # sort the list by their price momentum for security in self.market_cap: hist_stock = self.History(security.Symbol, timedelta(days=760), Resolution.Daily) if hist_stock.index.nlevels > 1: hist_stock = hist_stock.droplevel(level=0) if "close" in hist_stock.columns and "open" in hist_stock.columns and len(hist_stock) > 520: hist_stock.rename(columns={"close":"stock close"}, inplace=True) # we now want to calculate the monthly tesla returns stock_monthly_returns = hist_stock["stock close"].resample("M").ffill().pct_change().dropna() if len(stock_monthly_returns) == len(SPY_monthly_returns): df = pd.concat([stock_monthly_returns, SPY_monthly_returns], axis=1) df.rename(columns={"stock close":"stock monthly returns","spy close":"spy monthly returns"}, inplace=True) Y = df["stock monthly returns"] X = df["spy monthly returns"] X = sm.add_constant(X) model = sm.OLS(Y,X).fit() residual_values = model.resid # residual values end = date.today() start = date(end.year-1, end.month, end.day) data_12mth = residual_values.loc[start:end] x = np.sum(data_12mth) y = np.std(data_12mth) resid_mom = x/y # we now have a dictionary storing the values stock_filter[security.Symbol] = resid_mom # we only want the highest values for the coeff self.sortedLong = sorted(stock_filter.items(), key=lambda d:d[1],reverse=True) sorted_symbolLong = [x[0] for x in self.sortedLong] # long the top 20 self.long = sorted_symbolLong[:self.num_fine] return self.long def OnData(self, data): pass def rebalance(self): if self.initial == 0: for i in self.long: self.SetHoldings(i, self.scale_equities/self.num_fine) self.SetHoldings(self.iei, (1-self.scale_equities)/2) self.SetHoldings(self.gld, (1-self.scale_equities)/4) self.SetHoldings(self.tlt, (1-self.scale_equities)/4) self.initial += 1 else: # volatility scaling # we first have to calculate the correlation matrix of the various stocks in the portfolio # initialise n = 0 # count how many symbols in portfolio invested = [x.Symbol.Value for x in self.Portfolio.Values if x.Invested] holdings = len(invested) for i in self.Portfolio.Values: if i.Invested: hist = self.History(i.Symbol, timedelta(days=30), Resolution.Daily) hist = hist.drop(columns=["high","low","open","volume"]) if hist.index.nlevels > 1: hist = hist.droplevel(level=0) stock_daily_ret = hist["close"].pct_change().dropna() # first loop if n == 0: cov_matrix = stock_daily_ret n = n+1 else: cov_matrix = pd.concat([stock_daily_ret, cov_matrix], axis=1) # so as to annualise the portfolio volatility cov_annual = cov_matrix.cov()*252 weights = np.full(holdings, 1/holdings).reshape(holdings, 1) port_variance = np.dot(weights.T, np.dot(cov_annual, weights)) port_vol = np.sqrt(port_variance) # target annual volatility is set scaling = round((self.target_vol/port_vol).item(), 1) # dynamic scaling according to volatility - deterministic weights; capped at 1 i.e. no leverage self.scale_equities = min(scaling, 1) if self.scale_equities < 0.4: self.scale_equities = 0.4 self.scale_hedge = 1 - self.scale_equities # to update the momentum stocks only monthly # this removes stocks no longer on long list for i in self.Portfolio.Values: if i.Invested and i.Symbol not in self.long and i.Symbol not in self.hedge: self.Liquidate(i.Symbol) # monthly rebalancement of momentum stocks for i in self.long: self.SetHoldings(i, self.scale_equities/self.num_fine) self.SetHoldings(self.iei, self.scale_hedge/2) self.SetHoldings(self.gld, self.scale_hedge/4) self.SetHoldings(self.tlt, self.scale_hedge/4) self.Log("Invested:" + str(invested)) self.Log(self.scale_equities) self.Log("Holdings:" + str(holdings)) self.reb1 = 1