Overall Statistics |
Total Trades 37 Average Win 0.28% Average Loss -1.27% Compounding Annual Return 2525.865% Drawdown 6.800% Expectancy 0.035 Net Profit 12.680% Sharpe Ratio 27.627 Probabilistic Sharpe Ratio 77.542% Loss Rate 15% Win Rate 85% Profit-Loss Ratio 0.22 Alpha 0 Beta 0 Annual Standard Deviation 0.628 Annual Variance 0.394 Information Ratio 27.627 Tracking Error 0.628 Treynor Ratio 0 Total Fees $39.85 |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pandas as pd import numpy as np from scipy.optimize import minimize class myTrailingStopRiskManagementModel: ''' Credit goes to: Alex Catarino and many of his friends at QuantConnect https://github.com/QuantConnect/Lean/blob/master/Algorithm.Framework/Risk/TrailingStopRiskManagementModel.py Description: Limits the maximum possible loss measured from the highest unrealized profit ''' def __init__(self, maximumDrawdownPercent = 0.05): '''initializes the class Args: maximumDrawdownPercent: The maximum percentage drawdown allowed for algorithm portfolio compared with the highest unrealized profit, defaults to 5% drawdown ''' self.maximumDrawdownPercent = -abs(maximumDrawdownPercent) self.trailingHighs = dict() def SetWTtoZeroIfDDtooHigh(self, algo, targets=None): '''If drawdown is too high, set wt[symbol] to zero algo.wt[symbol] = weights which will be set to 0 in case drawdown exceeds the maximum ''' for kvp in algo.Securities: symbol = kvp.Key security = kvp.Value # Remove from trailingHighs dict if not invested if not security.Invested: self.trailingHighs.pop(symbol, None) continue # Add newly invested securities to trailingHighs dict if symbol not in self.trailingHighs: self.trailingHighs[symbol] = security.Holdings.AveragePrice continue # Check for new highs and update trailingHighs dict if self.trailingHighs[symbol] < security.High: self.trailingHighs[symbol] = security.High continue # Calc the drawdown securityHigh = self.trailingHighs[symbol] drawdown = (security.Low / securityHigh) - 1 # If drawdown is too high, set symbol weight to zero if drawdown < self.maximumDrawdownPercent: algo.wt[symbol] = 0 return class myPortfolioOptimizer: ''' Credit goes to: Emilio Freire / InnoQuantivity https://innoquantivity.com/blogs/inno-blog/portfolio-optimization-quantconnect-research-algorithm https://www.quantconnect.com/forum/discussion/8128/portfolio-optimization-research-amp-algorithm-for-better-workflows/p1/comment-22952 Description: Implementation of a custom optimizer that calculates the weights for each asset to optimize a given objective function Details: Optimization can be: - Equal Weighting - Maximize Portfolio Return - Minimize Portfolio Standard Deviation - Mean-Variance (minimize Standard Deviation given a target return) - Maximize Portfolio Sharpe Ratio - Maximize Portfolio Sortino Ratio - Risk Parity Portfolio Constraints: - Weights must be between some given boundaries - Weights must sum to 1 ''' def __init__(self, minWeight = 0, maxWeight = 1): ''' Description: Initialize the CustomPortfolioOptimizer Args: minWeight(float): The lower bound on portfolio weights maxWeight(float): The upper bound on portfolio weights ''' self.minWeight = minWeight self.maxWeight = maxWeight def CalcWeights(self, algo, symbols, objectiveFunction='riskParity', lookback=63, targetReturn=None): ''' Description: Calculate weights from daily returns, return a pandas Series ''' history = algo.History(symbols, lookback, Resolution.Daily)['close'].unstack(level = 0) returnsDf = history.pct_change().dropna() returnsDf.columns = map(lambda x : x.Value, symbols) weights = self.Optimize(objectiveFunction, returnsDf, targetReturn) return pd.Series(weights, index=returnsDf.columns, name='weights') def Optimize(self, objFunction, dailyReturnsDf, targetReturn = None): ''' Description: Perform portfolio optimization given a series of returns Args: objFunction: The objective function to optimize (equalWeighting, maxReturn, minVariance, meanVariance, maxSharpe, maxSortino, riskParity) dailyReturnsDf: DataFrame of historical daily arithmetic returns Returns: Array of double with the portfolio weights (size: K x 1) ''' # initial weights: equally weighted size = dailyReturnsDf.columns.size # K x 1 self.initWeights = np.array(size * [1. / size]) # get sample covariance matrix covariance = dailyReturnsDf.cov() # get the sample covariance matrix of only negative returns for sortino ratio negativeReturnsDf = dailyReturnsDf[dailyReturnsDf < 0] covarianceNegativeReturns = negativeReturnsDf.cov() if objFunction == 'equalWeighting': return self.initWeights bounds = tuple((self.minWeight, self.maxWeight) for x in range(size)) constraints = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0}] if objFunction == 'meanVariance': # if no target return is provided, use the resulting from equal weighting if targetReturn is None: targetReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, self.initWeights) constraints.append( {'type': 'eq', 'fun': lambda weights: self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights) - targetReturn} ) opt = minimize(lambda weights: self.ObjectiveFunction(objFunction, dailyReturnsDf, covariance, covarianceNegativeReturns, weights), x0 = self.initWeights, bounds = bounds, constraints = constraints, method = 'SLSQP') return opt['x'] def ObjectiveFunction(self, objFunction, dailyReturnsDf, covariance, covarianceNegativeReturns, weights): ''' Description: Compute the objective function Args: objFunction: The objective function to optimize (equalWeighting, maxReturn, minVariance, meanVariance, maxSharpe, maxSortino, riskParity) dailyReturnsDf: DataFrame of historical daily returns covariance: Sample covariance covarianceNegativeReturns: Sample covariance matrix of only negative returns weights: Portfolio weights ''' if objFunction == 'maxReturn': f = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights) return -f # convert to negative to be minimized elif objFunction == 'minVariance': f = self.CalculateAnnualizedPortfolioStd(covariance, weights) return f elif objFunction == 'meanVariance': f = self.CalculateAnnualizedPortfolioStd(covariance, weights) return f elif objFunction == 'maxSharpe': f = self.CalculateAnnualizedPortfolioSharpeRatio(dailyReturnsDf, covariance, weights) return -f # convert to negative to be minimized elif objFunction == 'maxSortino': f = self.CalculateAnnualizedPortfolioSortinoRatio(dailyReturnsDf, covarianceNegativeReturns, weights) return -f # convert to negative to be minimized elif objFunction == 'riskParity': f = self.CalculateRiskParityFunction(covariance, weights) return f else: raise ValueError(f'PortfolioOptimizer.ObjectiveFunction: objFunction input has to be one of equalWeighting,' + ' maxReturn, minVariance, meanVariance, maxSharpe, maxSortino or riskParity') def CalculateAnnualizedPortfolioReturn(self, dailyReturnsDf, weights): annualizedPortfolioReturns = np.sum( ((1 + dailyReturnsDf.mean())**252 - 1) * weights ) return annualizedPortfolioReturns def CalculateAnnualizedPortfolioStd(self, covariance, weights): annualizedPortfolioStd = np.sqrt( np.dot(weights.T, np.dot(covariance * 252, weights)) ) if annualizedPortfolioStd == 0: raise ValueError(f'PortfolioOptimizer.CalculateAnnualizedPortfolioStd: annualizedPortfolioStd cannot be zero. Weights: {weights}') return annualizedPortfolioStd def CalculateAnnualizedPortfolioNegativeStd(self, covarianceNegativeReturns, weights): annualizedPortfolioNegativeStd = np.sqrt( np.dot(weights.T, np.dot(covarianceNegativeReturns * 252, weights)) ) if annualizedPortfolioNegativeStd == 0: raise ValueError(f'PortfolioOptimizer.CalculateAnnualizedPortfolioNegativeStd: annualizedPortfolioNegativeStd cannot be zero. Weights: {weights}') return annualizedPortfolioNegativeStd def CalculateAnnualizedPortfolioSharpeRatio(self, dailyReturnsDf, covariance, weights): annualizedPortfolioReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights) annualizedPortfolioStd = self.CalculateAnnualizedPortfolioStd(covariance, weights) annualizedPortfolioSharpeRatio = annualizedPortfolioReturn / annualizedPortfolioStd return annualizedPortfolioSharpeRatio def CalculateAnnualizedPortfolioSortinoRatio(self, dailyReturnsDf, covarianceNegativeReturns, weights): annualizedPortfolioReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights) annualizedPortfolioNegativeStd = self.CalculateAnnualizedPortfolioNegativeStd(covarianceNegativeReturns, weights) annualizedPortfolioSortinoRatio = annualizedPortfolioReturn / annualizedPortfolioNegativeStd return annualizedPortfolioSortinoRatio def CalculateRiskParityFunction(self, covariance, weights): ''' Spinu formulation for risk parity portfolio ''' assetsRiskBudget = self.initWeights portfolioVolatility = self.CalculateAnnualizedPortfolioStd(covariance, weights) x = weights / portfolioVolatility riskParity = (np.dot(x.T, np.dot(covariance, x)) / 2) - np.dot(assetsRiskBudget.T, np.log(x)) return riskParity
''' v1.5. Intersection of ROC comparison using OUT_DAY approach by Vladimir (with dynamic stocks selector by fundamental factors and momentum) eliminated fee saving part of the code plus daily rebalance inspired by Peter Guenther, Tentor Testivis, Dan Whitnable, Thomas Chang, Miko M, Leandro Maia Updates: - Logic for Trailing Stop Loss from Quant Connect - Logic for Portfolio Optimization from Emilio Freire - Option for Weighted Fundamentals from some other nice person ''' from QuantConnect.Data.UniverseSelection import * import numpy as np import pandas as pd from helpers import myPortfolioOptimizer from helpers import myTrailingStopRiskManagementModel # ------------------------------------------------------------------------------------------------------------------------------------------------ BONDS = ['TLT']; VOLA = 126; BASE_RET = 85; STK_MOM = 126; N_COARSE = 100; N_FACTOR = 20; N_MOM = 5; PFO = 0; LEV = 1.50; HEDGE = 0.00; TSL = 0.00 # ------------------------------------------------------------------------------------------------------------------------------------------------ class Fundamental_Factors_Momentum_ROC_Comparison_OUT_DAY(QCAlgorithm): def Initialize(self): self.SetStartDate(2021, 1, 1) self.SetEndDate(2021, 1, 13) self.InitCash = 100000 self.SetCash(self.InitCash) self.MKT = self.AddEquity("SPY", Resolution.Hour).Symbol self.mkt = [] self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) res = Resolution.Minute # <- changed self.BONDS = [self.AddEquity(ticker, res).Symbol for ticker in BONDS] self.INI_WAIT_DAYS = 15 self.wait_days = self.INI_WAIT_DAYS self.GLD = self.AddEquity('GLD', res).Symbol self.SLV = self.AddEquity('SLV', res).Symbol self.XLU = self.AddEquity('XLU', res).Symbol self.XLI = self.AddEquity('XLI', res).Symbol self.UUP = self.AddEquity('UUP', res).Symbol self.DBB = self.AddEquity('DBB', res).Symbol self.pairs = [self.GLD, self.SLV, self.XLU, self.XLI, self.UUP, self.DBB] self.bull = 1 self.bull_prior = 0 self.count = 0 self.outday = (-self.INI_WAIT_DAYS+1) self.SetWarmUp(timedelta(350)) self.UniverseSettings.Resolution = res self.AddUniverse(self.CoarseFilter, self.FineFilter) self.data = {} self.RebalanceFreq = 60 self.UpdateFineFilter = 0 self.symbols = None self.RebalanceCount = 0 self.wt = {} self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 30), self.daily_check) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 60), self.trade) if TSL != 0: # <- added self.tsl_max = float(self.GetParameter("tsl_max")) self.tsl = myTrailingStopRiskManagementModel(maximumDrawdownPercent=self.tsl_max) # <- added self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(60)), self.stop_loss) # <- added if PFO != 0: # <- added self.pfo = myPortfolioOptimizer(minWeight=0, maxWeight=1) # <- added self.weights_stks = self.pfo.CalcWeights(self, symbols=self.BONDS) # <- added self.weights_bnds = self.pfo.CalcWeights(self, symbols=self.BONDS) # <- added symbols = [self.MKT] + self.pairs for symbol in symbols: self.consolidator = TradeBarConsolidator(timedelta(days=1)) self.consolidator.DataConsolidated += self.consolidation_handler self.SubscriptionManager.AddConsolidator(symbol, self.consolidator) self.history = self.History(symbols, VOLA, Resolution.Daily) if self.history.empty or 'close' not in self.history.columns: return self.history = self.history['close'].unstack(level=0).dropna() def consolidation_handler(self, sender, consolidated): self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close self.history = self.history.iloc[-VOLA:] def derive_vola_waitdays(self): sigma = 0.6 * np.log1p(self.history[[self.MKT]].pct_change()).std() * np.sqrt(252) wait_days = int(sigma * BASE_RET) period = int((1.0 - sigma) * BASE_RET) return wait_days, period def CoarseFilter(self, coarse): if not (((self.count-self.RebalanceCount) == self.RebalanceFreq) or (self.count == self.outday + self.wait_days - 1)): self.UpdateFineFilter = 0 return Universe.Unchanged self.UpdateFineFilter = 1 selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)] filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in filtered[:N_COARSE]] def FineFilter(self, fundamental): if self.UpdateFineFilter == 0: return Universe.Unchanged filtered_fundamental = [x for x in fundamental if (x.ValuationRatios.EVToEBITDA > 0) and (x.EarningReports.BasicAverageShares.ThreeMonths > 0) and float(x.EarningReports.BasicAverageShares.ThreeMonths) * x.Price > 2e9 and x.ValuationRatios.PricetoEBITDA and x.ValuationRatios.PERatio and x.SecurityReference.IsPrimaryShare and x.SecurityReference.SecurityType == "ST00000001" and x.SecurityReference.IsDepositaryReceipt == 0 and x.CompanyReference.IsLimitedPartnership == 0 ] # https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Reference-Tables s1 = sorted(filtered_fundamental, key=lambda x: x.ValuationRatios.EVToEBITDA, reverse=False) # <- added s2 = sorted(filtered_fundamental, key=lambda x: x.ValuationRatios.PricetoEBITDA, reverse=False) # <- added s3 = sorted(filtered_fundamental, key=lambda x: x.ValuationRatios.PERatio, reverse=False) # <- added dict = {} for i, elem in enumerate(s1): # <- added i1 = i # <- added i2 = s2.index(elem) # <- added i3 = s3.index(elem) # <- added score = sum([i1 * 1.0, i2 * 0.0, i3 * 0.0]) # <- added dict[elem] = score # <- added #top = sorted(filtered_fundamental, key = lambda x: x.ValuationRatios.EVToEBITDA, reverse=True)[:N_FACTOR] #self.symbols = [x.Symbol for x in top] top = sorted(dict.items(), key = lambda x: x[1], reverse=True)[:N_FACTOR] # <- changed self.symbols = [x[0].Symbol for x in top] # <- changed self.UpdateFineFilter = 0 self.RebalanceCount = self.count return self.symbols def OnSecuritiesChanged(self, changes): addedSymbols = [] for security in changes.AddedSecurities: addedSymbols.append(security.Symbol) if security.Symbol not in self.data: self.data[security.Symbol] = SymbolData(security.Symbol, STK_MOM, self) if len(addedSymbols) > 0: history = self.History(addedSymbols, 1 + STK_MOM, Resolution.Daily).loc[addedSymbols] for symbol in addedSymbols: try: self.data[symbol].Warmup(history.loc[symbol]) except: self.Debug(str(symbol)) continue def calc_return(self, stocks): ret = {} for symbol in stocks: try: ret[symbol] = self.data[symbol].Roc.Current.Value except: self.Debug(str(symbol)) continue df_ret = pd.DataFrame.from_dict(ret, orient='index') df_ret.columns = ['return'] sort_return = df_ret.sort_values(by = ['return'], ascending = False) return sort_return def daily_check(self): self.wait_days, period = self.derive_vola_waitdays() r = self.history.pct_change(period).iloc[-1] self.bear = ((r[self.SLV] < r[self.GLD]) and (r[self.XLI] < r[self.XLU]) and (r[self.DBB] < r[self.UUP])) self.down2x = ((r[self.SLV] < r[self.GLD]) and (r[self.XLI] < r[self.XLU])) or \ ((r[self.XLI] < r[self.XLU]) and (r[self.DBB] < r[self.UUP])) or \ ((r[self.SLV] < r[self.GLD]) and (r[self.DBB] < r[self.UUP])) # <- changed self.up2x = ((r[self.SLV] > r[self.GLD]) and (r[self.XLI] > r[self.XLU])) or \ ((r[self.XLI] > r[self.XLU]) and (r[self.DBB] > r[self.UUP])) or \ ((r[self.SLV] > r[self.GLD]) and (r[self.DBB] > r[self.UUP])) # <- changed self.up3x = ((r[self.SLV] > r[self.GLD]) and (r[self.XLI] > r[self.XLU]) and (r[self.DBB] > r[self.UUP])) # <- changed if self.bear: self.bull = False self.outday = self.count if (self.count >= self.outday + self.wait_days): self.bull = True self.bull_prior = self.bull self.count += 1 def trade(self): if self.symbols is None: return output = self.calc_return(self.symbols) stocks = output.iloc[:N_MOM].index for sec in self.Portfolio.Keys: if sec not in stocks and sec not in self.BONDS: self.wt[sec] = 0. if PFO == 0: # <- added for sec in stocks: # <- added self.wt[sec] = LEV*(1.0 - HEDGE)/len(stocks) if self.bull else LEV*HEDGE/len(stocks); else: # <- added self.weights_stks = self.pfo.CalcWeights(self, symbols=stocks.values.tolist()) # <- added for sec in stocks: # <- added sec_wt = self.weights_stks[self.weights_stks.index==str(sec.Value)][0] # <- added self.wt[sec] = LEV*(1.0 - HEDGE)*sec_wt if self.bull else LEV*HEDGE*sec_wt # <- added for sec in self.BONDS: self.wt[sec] = LEV*HEDGE/len(self.BONDS) if self.bull else LEV*(1.0 - HEDGE)/len(self.BONDS); for sec, weight in self.wt.items(): if weight == 0. and self.Portfolio[sec].IsLong: self.Liquidate(sec) for sec, weight in self.wt.items(): if weight != 0.: self.SetHoldings(sec, weight) def stop_loss(self): # <- added if self.symbols is None: return # <- added if TSL != 0: # <- added self.tsl.SetWTtoZeroIfDDtooHigh(self) # <- added for sec, weight in self.wt.items(): # <- added if weight == 0. and self.Portfolio[sec].IsLong: # <- added self.Liquidate(sec) # <- added def OnEndOfDay(self): mkt_price = self.Securities[self.MKT].Close self.mkt.append(mkt_price) mkt_perf = self.InitCash * self.mkt[-1] / self.mkt[0] self.Plot('Strategy Equity', self.MKT, mkt_perf) account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue self.Plot('Holdings', 'leverage', round(account_leverage, 2)) self.Plot('Holdings', 'Target Leverage', LEV) class SymbolData(object): def __init__(self, symbol, roc, algorithm): self.Symbol = symbol self.Roc = RateOfChange(roc) self.algorithm = algorithm self.consolidator = algorithm.ResolveConsolidator(symbol, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.Roc, self.consolidator) def Warmup(self, history): for index, row in history.iterrows(): self.Roc.Update(index, row['close'])