Overall Statistics |
Total Trades 215 Average Win 0.48% Average Loss -0.18% Compounding Annual Return 209.850% Drawdown 7.400% Expectancy 0.540 Net Profit 17.482% Sharpe Ratio 4.558 Probabilistic Sharpe Ratio 75.467% Loss Rate 58% Win Rate 42% Profit-Loss Ratio 2.66 Alpha 1.821 Beta -0.773 Annual Standard Deviation 0.404 Annual Variance 0.163 Information Ratio 4.251 Tracking Error 0.44 Treynor Ratio -2.384 Total Fees $230.18 Estimated Strategy Capacity $310000000.00 Lowest Capacity Asset BBBY R735QTJ8XC9X |
from clr import AddReference AddReference("System") AddReference("QuantConnect.Algorithm") AddReference("QuantConnect.Algorithm.Framework") AddReference("QuantConnect.Common") AddReference("QuantConnect.Indicators") from System import * from QuantConnect import * from QuantConnect.Indicators import * from QuantConnect.Algorithm import * from QuantConnect.Algorithm.Framework import * from QuantConnect.Algorithm.Framework.Portfolio import * from MeanCVaRPortfolioOptimizer import MeanCVaRPortfolioOptimizer from datetime import timedelta import numpy as np import pandas as pd ### <summary> ### Provides an implementation of Mean-CVaR portfolio optimization adjusting modern portfolio theory's risk measure to coherent conditional value at risk (CVaR). ### The default model uses the MinimumCVaRPortfolioOptimizer that accepts a 63-row matrix of 1-day returns. ### </summary> class MeanCVaROptimizationPortfolioConstructionModel(PortfolioConstructionModel): def __init__(self, rebalance = Resolution.Daily, portfolioBias = PortfolioBias.Long, lookback = 1, period = 63, resolution = Resolution.Daily, targetReturn = None, optimizer = None, alpha = 0.05, riskAverse = 3.): """Initialize the model Args: rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function. If None will be ignored. The function returns the next expected rebalance time for a given algorithm UTC DateTime. The function returns null if unknown, in which case the function will be called again in the next loop. Returning current time will trigger rebalance. portfolioBias: Specifies the bias of the portfolio (Long only for convex CVaR optimization) lookback(int): Historical return lookback period period(int): The time interval of history price to calculate the weight resolution: The resolution of the history price optimizer(class): Method used to compute the portfolio weights alpha(float): Percentage CVaR using riskAverse: tradeoff mean CVaR weighing""" self.lookback = lookback self.period = period self.resolution = resolution self.portfolioBias = PortfolioBias.Long self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0) lower = 0 upper = 1 self.optimizer = MeanCVaRPortfolioOptimizer(lower, upper, targetReturn) if optimizer is None else optimizer self.alpha = alpha self.riskAverse = riskAverse self.symbolDataBySymbol = {} # If the argument is an instance of Resolution or Timedelta # Redefine rebalancingFunc rebalancingFunc = rebalance if isinstance(rebalance, int): rebalance = Extensions.ToTimeSpan(rebalance) if isinstance(rebalance, timedelta): rebalancingFunc = lambda dt: dt + rebalance if rebalancingFunc: self.SetRebalancingFunc(rebalancingFunc) def ShouldCreateTargetForInsight(self, insight): if len(PortfolioConstructionModel.FilterInvalidInsightMagnitude(self.Algorithm, [insight])) == 0: return False symbolData = self.symbolDataBySymbol.get(insight.Symbol) if insight.Magnitude is None: self.algorithm.SetRunTimeError(ArgumentNullException('MeanCVaROptimizationPortfolioConstructionModel does not accept \'None\' as Insight.Magnitude. Please checkout the selected Alpha Model specifications.')) return False if symbolData is None: # empty history will not be added so shall be dropped return False symbolData.Add(self.Algorithm.Time, insight.Magnitude) return True def DetermineTargetPercent(self, activeInsights): """ Will determine the target percent for each insight Args: Returns: """ targets = {} # If we have no insights just return an empty target list if len(activeInsights) == 0: return targets symbols = [insight.Symbol for insight in activeInsights] # Create a dictionary keyed by the symbols in the insights with an pandas.Series as value to create a data frame returns = { str(symbol) : data.Return for symbol, data in self.symbolDataBySymbol.items() if symbol in symbols } returns = pd.DataFrame(returns) # The portfolio optimizer finds the optional weights for the given data weights = self.optimizer.Optimize(returns, self.alpha, self.riskAverse) weights = pd.Series(weights, index = returns.columns) # Create portfolio targets from the specified insights for insight in activeInsights: weight = weights[str(insight.Symbol)] # don't trust the optimizer if self.portfolioBias != PortfolioBias.Long and self.sign(weight) != self.portfolioBias: weight = 0 targets[insight] = weight return targets def OnSecuritiesChanged(self, algorithm, changes): '''Event fired each time the we add/remove securities from the data feed Args: algorithm: The algorithm instance that experienced the change in securities changes: The security additions and removals from the algorithm''' # clean up data for removed securities super().OnSecuritiesChanged(algorithm, changes) for removed in changes.RemovedSecurities: if removed.Symbol in self.symbolDataBySymbol: symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None) symbolData.Reset() # initialize data for added securities symbols = [ x.Symbol for x in changes.AddedSecurities ] history = algorithm.History(symbols, self.lookback * self.period, self.resolution) if history.empty: return tickers = history.index.levels[0] for ticker in tickers: symbol = SymbolCache.GetSymbol(ticker) if symbol not in self.symbolDataBySymbol: symbolData = self.MeanCVaRSymbolData(symbol, self.lookback, self.period) symbolData.WarmUpIndicators(history.loc[ticker].iloc[:-1]) # The last one will use present value which added later to avoid same index self.symbolDataBySymbol[symbol] = symbolData class MeanCVaRSymbolData: '''Contains data specific to a symbol required by this model''' def __init__(self, symbol, lookback, period): self.symbol = symbol self.roc = RateOfChange(f'{symbol}.ROC({lookback})', lookback) self.roc.Updated += self.OnRateOfChangeUpdated self.window = RollingWindow[IndicatorDataPoint](period) def Reset(self): self.roc.Updated -= self.OnRateOfChangeUpdated self.roc.Reset() self.window.Reset() def WarmUpIndicators(self, history): for tuple in history.itertuples(): self.roc.Update(tuple.Index, tuple.close) def OnRateOfChangeUpdated(self, roc, value): if roc.IsReady: self.window.Add(value) def Add(self, time, value): item = IndicatorDataPoint(self.symbol, time, value) self.window.Add(item) @property def Return(self): return pd.Series( data = [(1 + float(x.Value))**252 - 1 for x in self.window], index = [x.EndTime for x in self.window]) @property def IsReady(self): return self.window.IsReady def __str__(self, **kwargs): return '{}: {:.2%}'.format(self.roc.Name, (1 + self.window[0])**252 - 1)
import numpy as np import pandas as pd import cvxpy as cp ### <summary> ### Provides an implementation of a portfolio optimizer that calculate the optimal weights ### with the weight range from 0 to 1 and minimize the portfolio conditional value at risk (CVaR) ### </summary> class MeanCVaRPortfolioOptimizer: '''Provides an implementation of a portfolio optimizer that calculate the optimal weights with the weight range from 0 to 1 and minimize the portfolio CVaR''' def __init__(self, minimum_weight = 0, maximum_weight = 1, target_return = None): '''Initialize the MinimumCVaRPortfolioOptimizer Args: minimum_weight(float): The lower bounds on portfolio weights, non-negative value maximum_weight(float): The upper bounds on portfolio weights target_return(float): The target portfolio return''' self.minimum_weight = max(0, minimum_weight) self.maximum_weight = maximum_weight self.target_return = target_return def Optimize(self, historicalReturns, alpha = 0.05, riskAverse = 3, expectedReturns = None): ''' Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns args: historicalReturns: Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N). alpha: percentage CVaR using, default 5% CVaR riskAverse: risk aversion level expectedReturns: Array of double with the portfolio annualized expected returns (size: K x 1). Returns: Array of double with the portfolio weights (size: K x 1) ''' if expectedReturns is None: expectedReturns = historicalReturns.mean(axis=0).values size = int(historicalReturns.shape[1]) # K x 1 number = int(historicalReturns.shape[0]) # 1 x N x0 = np.array(size * [1. / size]) # https://richtarik.org/docs/Kisiala_Dissertation.pdf, p.27, eq. 3.14 t, z, weights = cp.Variable(), cp.Variable((number, 1), nonneg=True), cp.Variable((size, 1), nonneg=True) objective = cp.Maximize(expectedReturns @ weights - riskAverse * (t + cp.sum(z)/(number * (1 - alpha)))) constrains = [z >= - historicalReturns.values @ weights - t] \ + self.get_budget_constraint(weights)\ + self.get_boundary_conditions(weights) if self.target_return is not None: constrains = constrains + self.get_target_constraint(self, weights, expectedReturns) problem = cp.Problem(objective, constrains) problem.solve(solver="SCS") weights = weights.value return weights.flatten() if weights is not None else x0 def get_boundary_conditions(self, weights): '''Creates the boundary condition for the portfolio weights''' return [weights >= self.minimum_weight, weights <= self.maximum_weight] def get_budget_constraint(self, weights): '''Defines a budget constraint: the sum of the weights equals unity''' return [cp.sum(weights) == 1.] def get_target_constraint(self, weights, expectedReturns): '''Ensure that the portfolio return target a given return''' return [expectedReturns @ weights >= self.target_return]
from MeanCVaROptimizationPortfolioConstructionModel import MeanCVaROptimizationPortfolioConstructionModel class CreativeGreenKoala(QCAlgorithm): def Initialize(self): self.SetStartDate(2021, 5, 1) self.SetCash(100000) self.AddUniverse(self.MyCoarseFilterFunction) self.UniverseSettings.Resolution = Resolution.Daily self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw)) self.SetPortfolioConstruction(MeanCVaROptimizationPortfolioConstructionModel()) self.SetExecution(ImmediateExecutionModel()) def OnData(self, data): insights = [] df = self.History(self.symbols, 252, Resolution.Daily) if df.empty: return df = df.close.unstack("symbol").pct_change()[1:].dropna(axis=1) # required to include magnitude to use (expected return) [insights.append(Insight.Price(df.columns[n], Expiry.EndOfDay, InsightDirection.Up, df[df.columns[n]].mean(axis=0), None, None, None)) for n in range(len(df.columns))] self.EmitInsights(insights) def MyCoarseFilterFunction(self, coarse): sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True) self.symbols = [ x.Symbol for x in sortedByDollarVolume if x.Price > 10 and x.DollarVolume > 10000000 ][:10] return self.symbols