Overall Statistics |
Total Trades 24 Average Win 0.22% Average Loss -1.28% Compounding Annual Return 19.926% Drawdown 8.800% Expectancy -0.033 Net Profit 6.242% Sharpe Ratio 1.434 Probabilistic Sharpe Ratio 57.239% Loss Rate 18% Win Rate 82% Profit-Loss Ratio 0.17 Alpha 0.175 Beta -0.04 Annual Standard Deviation 0.117 Annual Variance 0.014 Information Ratio -0.068 Tracking Error 0.21 Treynor Ratio -4.145 Total Fees $24.00 |
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): #if self.Portfolio.Invested: return 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 #self.algorithm.MarketOrder(symbol, 1) algorithm.SetHoldings(symbol, weight); if liquidate_now: for symbol in liquidate_now: weight=self.startingMaxHoldingLimit algorithm.Liquidate(symbol) return [] def OnOrderEvent(self, orderEvent): order = self.Transactions.GetOrderById(orderEvent.OrderId) if orderEvent.Status == OrderStatus.Filled: self.algorithm.Debug("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent)) 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_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 # % 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.Debug(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.Debug(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.Debug(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
#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