Overall Statistics |
Total Trades 91 Average Win 0.31% Average Loss -0.63% Compounding Annual Return 14.769% Drawdown 8.700% Expectancy 0.120 Net Profit 4.714% Sharpe Ratio 0.972 Probabilistic Sharpe Ratio 47.770% Loss Rate 25% Win Rate 75% Profit-Loss Ratio 0.49 Alpha 0.135 Beta 0.12 Annual Standard Deviation 0.167 Annual Variance 0.028 Information Ratio -0.268 Tracking Error 0.232 Treynor Ratio 1.35 Total Fees $91.00 |
#insights.append(Insight(symbol, self.insightsTimeDelta, InsightType.Price, symbolData.InsightDirection, None,None, None,0.1)) #algorithm.Log(f"{symbol}\tMOM\t[{symbolData.fmom}]\t{round(symbolData.mom.Current.Value,2)}\tKAMA\t[{symbolData.fkama}]\t{round(symbolData.kama.Current.Value,2)}\ # \tPrice\t{symbolData.price}\tROC\t[{symbolData.froc}]\t{round(symbolData.roc.Current.Value,4)}\tEMA\t[{symbolData.fema}]\tEMA-13\t{round(symbolData.ema13.Current.Value,2)}\ # \tEMA-63\t{round(symbolData.ema63.Current.Value,2)}\tEMA-150\t{round(symbolData.ema150.Current.Value,2)}\taction\t{symbolData.InsightDirection}") #self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) #self.SetPortfolioConstruction(MeanVarianceOptimizationPortfolioConstructionModel(param.resolution,PortfolioBias.LongShort,1,63,param.resolution,0.02,MaximumSharpeRatioPortfolioOptimizer(0,1,0))) # self.rebalancingPeriod = Expiry.EndOfMonth #pcm = InsightWeightingPortfolioConstructionModel(lambda time: param.rebalancingPeriod(time)) #self.InsightDirection = InsightDirection.Up #self.InsightDirection = InsightDirection.Flat # liqudates position - work around InsightDirection.Down which may sell and then short
import numpy as np import pandas as pd from datetime import datetime, date from datetime import datetime, timedelta from PortfolioOptimizerClass import PortfolioOptimizer from clr import AddReference AddReference("QuantConnect.Indicators") from QuantConnect.Indicators import * # TODO : # fix buying daily # Universe selection # short selling model # selling hourly # rebalance weekly (weight based on RS?) # look into small stocks large moves 35.65->33.51 which is 6% ; control via draw down? # self.SetBrokerageModel(AlphaStreamsBrokerageModel()) # learn more about this # fix if dt >9 and dt<18 # DONE: fix hourlyHouseKeeping # 10 days daily STD for ROKU on 7 Jan 21 is 12.15529, mine (based on open) is 12.3907448 from System.Drawing import Color class ModelA(AlphaModel): def __init__(self, param): self.param = param self.symbolDataBySymbol = {} self.modelResolution = param.resolution self.insightsTimeDelta = param.timedelta self.objectiveFunction = param.pcmObjectiveFunction self.lookbackOptimization = param.pcmLookbackOptimization self.portOpt = PortfolioOptimizer(minWeight = 0, maxWeight = 1) self.startingMaxHoldingLimit = param.startingMaxHoldingLimit def OnSecuritiesChanged(self, algorithm, changes): for added in changes.AddedSecurities: symbolData = self.symbolDataBySymbol.get(added.Symbol) if symbolData is None: symbolData = SymbolData(added.Symbol, algorithm, self.param) self.symbolDataBySymbol[added.Symbol] = symbolData def Update(self, algorithm, data): insights=[] liquidate_now=[] invested = [ x.Symbol.Value for x in algorithm.Portfolio.Values if x.Invested ] # can we make this easier via key query? for symbol, symbolData in self.symbolDataBySymbol.items(): isInvested= str(symbol) in invested if symbol != self.param.benchmark: symbolData.getInsight(algorithm.Securities[symbol].Price, isInvested) # Latest known price; we are at 12:00 and the last trade at 10.57 if symbolData.trade: if symbolData.liquidate: invested.remove(str(symbol)) liquidate_now.append(str(symbol)) else: invested.append(str(symbol)) # calculate optimal weights if invested: weights = self.CalculateOptimalWeights(algorithm, invested, self.objectiveFunction, self.lookbackOptimization) for symbol in invested: weight = weights[str(symbol)] if weight>self.startingMaxHoldingLimit and len(invested)<1/self.startingMaxHoldingLimit: weight=self.startingMaxHoldingLimit insights.append(Insight.Price(symbol, self.insightsTimeDelta, InsightDirection.Up, None, None, None, weight)) if liquidate_now: for symbol in liquidate_now: insights.append(Insight.Price(symbol, self.insightsTimeDelta, InsightDirection.Flat, None, None, None, 1)) return insights def CalculateOptimalWeights(self, algorithm, symbols, objectiveFunction, lookbackOptimization): # get historical close prices historyClosePrices = algorithm.History(symbols, lookbackOptimization, Resolution.Daily)['close'].unstack(level = 0) # calculate daily returns returnsDf = historyClosePrices.pct_change().dropna() # rename the columns in the dataframe in order to have tickers and not symbol strings columnsList = list(returnsDf.columns) returnsDf.rename(columns = {columnsList[i]: algorithm.ActiveSecurities[columnsList[i]].Symbol.Value for i in range(len(columnsList))}, inplace = True) # calculate optimal weights weights = self.portOpt.Optimize(objectiveFunction, returnsDf) # convert the weights to a pandas Series weights = pd.Series(weights, index = returnsDf.columns, name = 'weights') return weights class FrameworkAlgorithm(QCAlgorithm): def Initialize(self): param=paramData() symbols = [Symbol.Create(x, SecurityType.Equity, Market.USA) for x in param.tickers] self.SetStartDate(param.dateFrom[0],param.dateFrom[1],param.dateFrom[2]) self.SetEndDate(param.dateTo[0],param.dateTo[1],param.dateTo[2]) self.SetCash(param.cash) self.liquidationBarrier=param.cash*param.stopLoss*-1 self.SetBenchmark(param.benchmarkTicker) param.setBenchmark(self.AddEquity(param.benchmarkTicker,param.resolution).Symbol) self.UniverseSettings.Resolution = param.resolution self.SetWarmUp(timedelta(param.warmup)) self.SetUniverseSelection(ManualUniverseSelectionModel(symbols)) self.SetBrokerageModel(AlphaStreamsBrokerageModel()) # learn more about this self.SetAlpha(ModelA(param)) myPCM = InsightWeightingPortfolioConstructionModel(rebalance = timedelta(days=252), portfolioBias = PortfolioBias.Long) myPCM.RebalanceOnInsightChanges = False myPCM.RebalanceOnSecurityChanges = False self.SetPortfolioConstruction(myPCM) self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(param.maxDrawDown)) # NullRiskManagementModel() or MaximumDrawdownPercentPerSecurity(param.maxDrawDown) > drop in profit from the max >> done daily / TODO: redo hourly? or self.SetExecution(ImmediateExecutionModel()) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(param.runEveryXminutes)), self.hourlyHouseKeeping) def hourlyHouseKeeping(self): # Fail Safe - If our strategy is losing than acceptable (something is wrong) # Strategy suddenly losing money or logic problem/bug we did't catch when testing pnl= sum([self.Portfolio[symbol].NetProfit for symbol in self.Portfolio.Keys]) #if self.LiveMode: if pnl < self.liquidationBarrier: self.Debug(f"Fallback event triggered, liquidating with total portfolio loss of {pnl}") self.Liquidate() self.Quit() dt=int(self.Time.hour) if dt >9 and dt<18: # if not set still prints out of hours for self.IsMarketOpen("SPY") if (self.IsMarketOpen("SPY") and self.Portfolio.Invested): #self.Log("\n\nPortfolio") summary = {} invested = [ x.Symbol.Value for x in self.Portfolio.Values if x.Invested ] for symbol in invested: hold_val = round(self.Portfolio[symbol].HoldingsValue, 2) abs_val = round(self.Portfolio[symbol].AbsoluteHoldingsValue, 2) pnl = round(self.Portfolio[symbol].UnrealizedProfit, 2) qty = self.Portfolio[symbol].Quantity price = self.Portfolio[symbol].Price summary[symbol]=[hold_val,abs_val,pnl,qty,price] df=pd.DataFrame(summary) df.index = ['hold_val', 'abs_val', 'pnl', 'qty','price'] df=df.T hold_val_total= abs(df['hold_val']).sum() df = df.assign(weight=abs(df['hold_val'])/hold_val_total) #self.Log(df) #self.Log("\n\n") class paramData: def __init__(self): self.dateFrom = (2020,9,1) self.dateTo = (2021,1,1) self.cash = 50000 # how to top this up after going live? self.warmup = 28 # starts from self.dateFrom self.resolution = Resolution.Hour # 10-11, etc Daily data is midnight to mifnight, 12AM EST self.tickers = ["MSFT","ROKU","ANET","FSLY"] # how do I change this on request? #self.resolution = Resolution.Daily # 10-11, etc Daily data is midnight to mifnight, 12AM EST #self.tickers = ["MSFT","MRNA","MELI","ROKU","ANET","XRX","SHOP","EBAY","CSCO","ORCL","NOW","THO","BIDU","SPOT","DOCU","DDOG","SQ","FSLY","TMO","PFE","IOVA","EXEL","ACLS","BNTX","IBM"] self.tickers_len = len(self.tickers) self.timedelta = timedelta(hours=240) self.maxDrawDown = 0.05 self.runEveryXminutes = 60 # Schedule frequency self.benchmarkTicker = 'SPY' # can be ticker as a part of the dictionary ["MSFT:SPY"] self.pcmObjectiveFunction = 'equalWeighting' #'equalWeighting' 'maxReturn' 'riskParity' self.pcmLookbackOptimization = 63 self.stopLoss = 0.15 # 15% of the total cash invested self.startingMaxHoldingLimit = 0.17 # we do not allocate more than this % for each security def setBenchmark(self, symbol): self.benchmark = symbol class SymbolData: def __init__(self, symbol, algorithm, param): self.symbol = symbol self.algorithm = algorithm self.param = param self.resolution = param.resolution self.price = 0.00 # last trading price self.lastPricePaidRef = 0.00 # last purchase price reference; update with an actual price self.kama = algorithm.KAMA(symbol, 10,2,30, self.resolution) self.variationRate = 0.95 # tolerance level to avoid buy and immediate sell scenario self.mom = algorithm.MOM(symbol, 14, self.resolution) self.roc = algorithm.ROC(symbol, 9, self.resolution) self.ema13 = algorithm.EMA(symbol, 13, self.resolution) self.ema63 = algorithm.EMA(symbol, 63, self.resolution) self.ema150 = algorithm.EMA(symbol, 150, self.resolution) self.fkama = False self.fmom = False self.froc = False self.fema = False self.rsStock = False self.rsIdx = False self.fbenchmark = False self.lookback = 10 self.std = algorithm.STD(symbol, self.lookback,self.resolution) self.magnitude = 0.025#algorithm.IndicatorExtensions.SMA(RateOfChangePercent(1),self.lookback).Current.Value self.lastDateTraded = self.algorithm.Time.date() # Chart Plotting self.kama.Updated += self.getRSL self.kama.Updated += self.OnSymbolDataUpdate self.dataPlot = Chart('Detail'+str(self.symbol)) self.dataPlot.AddSeries(Series('Price', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('Kama', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('MOM', SeriesType.Line, '')) self.dataPlot.AddSeries(Series('EMA13', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('EMA63', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('EMA150', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('ROC', SeriesType.Line, '')) self.dataPlot.AddSeries(Series('RS-idx', SeriesType.Line, '')) self.dataPlot.AddSeries(Series('Std', SeriesType.Line, '$')) self.dataPlot.AddSeries(Series('Buy', SeriesType.Scatter, '$', Color.Green,ScatterMarkerSymbol.Circle)) self.dataPlot.AddSeries(Series('Sell', SeriesType.Scatter, '$', Color.Red,ScatterMarkerSymbol.Circle)) self.algorithm.AddChart(self.dataPlot) def getInsight(self, price, isInvested): self.price = price self.fkama_buy = self.price>self.kama.Current.Value self.fkama_sell = self.price<self.kama.Current.Value*self.variationRate self.fmom = self.mom.Current.Value>0 self.froc = self.roc.Current.Value>0 self.fema = self.ema13.Current.Value>self.ema63.Current.Value>self.ema150.Current.Value self.trade = False self.liquidate = False self.fbenchmark = self.rsStock>self.rsIdx self.dateTradedDelta = (self.algorithm.Time.date()-self.lastDateTraded).days # and self.froc self.fmom and self.algorithm.Log(f"{str(self.symbol)}\t{str(self.algorithm.Time.date())}\tTraded\t{str(self.lastDateTraded)}\tDt\t{str(self.dateTradedDelta)}\tstd\t{self.std}\tclose\t{self.price}") if not isInvested and self.fkama_buy and self.fema and self.fbenchmark: self.trade = True self.lastDateTraded = self.algorithm.Time.date() self.algorithm.Plot('Detail'+str(self.symbol),'Buy', self.price) self.algorithm.Log(f"\n>>>>>> Buy\t{str(self.symbol)}\tPrice\t{self.price}[{self.lastPricePaidRef}]\tMOM:{self.fmom}\trKAMA\t{self.price}\t \ \nKAMA:{self.kama.Current.Value}\tFEMA:{self.fema}\tRS:{self.fbenchmark}\tSTD\t{self.std}") self.lastPricePaidRef = self.price # or not self.froc not self.fmom or if isInvested and (self.fkama_sell or not self.fema or not self.fbenchmark \ or (self.dateTradedDelta<3 and self.price<self.lastPricePaidRef-float(str(self.std)))): # we avoid selling on the same/next day if move less than x std self.trade = True self.liquidate = True self.algorithm.Plot('Detail'+str(self.symbol),'Sell',self.price) self.algorithm.Log(f"\n<<<<<<< Sell\t{str(self.symbol)}\tMOM\t{self.fmom}\tPrice\t{self.price}[{self.lastPricePaidRef}]\trKAMA\t{self.price}\t \ \nKAMA\t{self.kama.Current.Value}\tFEMA\t{self.fema}\tStock\t{self.rsStock}\tIdx\t{self.rsIdx}\tSTD\t{self.std}\tPriceDrop{str(self.lastPricePaidRef-float(str(self.std)))}") def OnSymbolDataUpdate(self, sender, updated): self.algorithm.Plot('Detail'+str(self.symbol),'Price', self.price) self.algorithm.Plot('Detail'+str(self.symbol),'Kama', self.kama.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'ROC', self.roc.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'MOM', self.mom.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'EMA13', self.ema13.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'EMA63', self.ema63.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'EMA150', self.ema150.Current.Value) self.algorithm.Plot('Detail'+str(self.symbol),'Std', self.std.Current.Value) def getRSL(self, sender, updated): # lookback days : algo weight days = {40:0.5,80:0.25,160:0.25} rs = {} for symbol in [self.symbol,self.param.benchmark]: result =[] df=pd.DataFrame(self.algorithm.History(symbol, 300, Resolution.Daily)) df=df.iloc[::-1] df=df.reset_index(level=0, drop=True) symbol = str(symbol) for x in days: result.append([symbol, x, df.iloc[0]['close'], df.iloc[x-1]['close'],days[x]]) df = pd.DataFrame(result,columns=['Symbol','Days','Ref_Price','Close_Price','Weight'],dtype=float) df = df.assign(Rsl=(df['Ref_Price'])/df['Close_Price']*df['Weight']) rs[symbol] = (abs(df['Rsl']).sum()*1000)-1000 self.rsStock = rs[str(self.symbol)] self.rsIdx = rs[str(self.param.benchmark)] self.algorithm.Plot('Detail'+str(self.symbol),'RS-idx', self.rsStock/self.rsIdx)
from clr import AddReference AddReference("QuantConnect.Research") #clr.AddReference('QuantConnect.Research') from QuantConnect.Research import QuantBook class RelativeStrengthLineCalc(): def getRSL(self, ref_date, symbols): self.rsl_target_days = [40,80,160] self.rsl_target_weights = [0.5,0.25,0.25] qb = QuantBook() date_end = datetime(ref_date) date_start = date_end - timedelta(days=300) for symbol in symbols: smbl = qb.AddEquity(symbol) # add equity data result =[] history = qb.History(smbl.Symbol, date_start, date_end, Resolution.Daily) df=pd.DataFrame(history) df=df.iloc[::-1] df=df.reset_index(level=0, drop=True) i=0 for x in rsl_target_days: result.append([symbol, x, df.iloc[0]['close'], df.iloc[x-1]['close'],rsl_target_weights[i]]) i=i+1 df = pd.DataFrame(result,columns=['Symbol','Days','Ref_Price','Close_Price','Weight'],dtype=float) df = df.assign(Rsl=(df['Ref_Price'])/df['Close_Price']*df['Weight']) rsl=(abs(df['Rsl']).sum()*1000)-1000 return rsl
class RelativeStrengthLineCalc(): def getRSL(): rsl_target_days = [40,80,160] rsl_target_weights = [0.5,0.25,0.25] return 1
import pandas as pd import numpy as np from scipy.optimize import minimize class PortfolioOptimizer: ''' 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 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