Overall Statistics |
Total Trades 5 Average Win 0% Average Loss -2.26% Compounding Annual Return -43.859% Drawdown 13.100% Expectancy -1 Net Profit -9.287% Sharpe Ratio -1.674 Probabilistic Sharpe Ratio 6.554% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.272 Beta -0.23 Annual Standard Deviation 0.213 Annual Variance 0.045 Information Ratio -2.38 Tracking Error 0.305 Treynor Ratio 1.549 Total Fees $5.42 |
#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 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) 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=[] 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.append(str(symbol)) algorithm.Debug(f"sell {str(symbol)}") else: invested.append(str(symbol)) algorithm.Debug(f"buy {str(symbol)}") # calculate optimal weights if invested: weights = self.CalculateOptimalWeights(algorithm, invested, self.objectiveFunction, self.lookbackOptimization) for symbol in invested: weight = weights[str(symbol)] insights.append(Insight.Price(symbol, self.insightsTimeDelta, InsightDirection.Up, None, None, None, weight)) if liquidate: for symbol in liquidate: 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.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(NullRiskManagementModel()) # 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 < -5000: # can't pass value via parametr? self.Log(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,10) self.dateTo = (2020,11,10) self.cash = 50000 # how to top this up after going live? self.warmup = 28 # starts from self.dateFrom self.resolution = Resolution.Daily #10-11, etc Daily data is midnight to mifnight, 12AM EST self.tickers = ["MSFT"] # how do I change this on request? self.fallback_barrier = -1000 self.timedelta = timedelta(hours=240) self.maxDrawDown = 0.02 self.runEveryXminutes = 60 # Schedule frequency self.benchmarkTicker = 'SPY' # can be ticker as a part of the dictionary ["MSFT:SPY"] self.pcmObjectiveFunction = 'equalWeighting' self.pcmLookbackOptimization = 63 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 self.kama = algorithm.KAMA(symbol, 10,2,30, self.resolution) self.variationRate = 1.03 # 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 #algorithm.STD() # 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-stock', SeriesType.Line, '')) self.dataPlot.AddSeries(Series('RS-idx', 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*1/self.variationRate<self.kama.Current.Value 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 # and self.fkama_buy and self.fema and self.froc if not isInvested and self.fmom: self.trade = True self.algorithm.Plot('Detail'+str(self.symbol),'Buy', self.price) # or self.fkama_sell or not self.fema or not self.froc if isInvested and (not self.fmom): self.trade = True self.liquidate = True self.algorithm.Plot('Detail'+str(self.symbol),'Sell',self.price) 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) 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-stock',self.rsStock ) self.algorithm.Plot('Detail'+str(self.symbol),'RS-idx', 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