| 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