| Overall Statistics |
|
Total Orders 644 Average Win 0.23% Average Loss -0.45% Compounding Annual Return 0.915% Drawdown 27.800% Expectancy -0.026 Start Equity 10000000 End Equity 10403455.47 Net Profit 4.035% Sharpe Ratio -0.358 Sortino Ratio -0.364 Probabilistic Sharpe Ratio 1.331% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 0.51 Alpha -0.057 Beta 0.495 Annual Standard Deviation 0.097 Annual Variance 0.009 Information Ratio -0.81 Tracking Error 0.098 Treynor Ratio -0.07 Total Fees $20288.29 Estimated Strategy Capacity $17000000.00 Lowest Capacity Asset EFAV V0WRDXSSH205 Portfolio Turnover 1.87% Drawdown Recovery 1555 |
#region imports
from AlgorithmImports import *
#endregion
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm.Framework")
from QuantConnect import Resolution, Extensions
from QuantConnect.Algorithm.Framework.Alphas import *
from QuantConnect.Algorithm.Framework.Portfolio import *
from itertools import groupby
from datetime import datetime, timedelta
class EqualWeightingPortfolioConstructionModel(PortfolioConstructionModel):
'''Provides an implementation of IPortfolioConstructionModel that gives equal weighting to all securities.
The target percent holdings of each security is 1/N where N is the number of securities.
For insights of direction InsightDirection.Up, long targets are returned and
for insights of direction InsightDirection.Down, short targets are returned.'''
def __init__(self, rebalance = Resolution.Daily, portfolioBias = PortfolioBias.LongShort):
'''Initialize a new instance of EqualWeightingPortfolioConstructionModel
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 (Short, Long/Short, Long)'''
self.portfolioBias = portfolioBias
# 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 DetermineTargetPercent(self, activeInsights):
'''Will determine the target percent for each insight
Args:
activeInsights: The active insights to generate a target for'''
result = {}
# give equal weighting to each security
count = sum(x.Direction != InsightDirection.Flat and self.RespectPortfolioBias(x) for x in activeInsights)
percent = 0 if count == 0 else 1.0 / count
for insight in activeInsights:
result[insight] = (insight.Direction if self.RespectPortfolioBias(insight) else InsightDirection.Flat) * percent
return result
def RespectPortfolioBias(self, insight):
'''Method that will determine if a given insight respects the portfolio bias
Args:
insight: The insight to create a target for
'''
return self.portfolioBias == PortfolioBias.LongShort or insight.Direction == self.portfolioBias
# Your New Python File#region imports
from AlgorithmImports import *
#endregion
from clr import AddReference
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")
from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm.Framework.Alphas import *
from datetime import timedelta
class HistoricalReturnsAlphaModel(AlphaModel):
'''Uses Historical returns to create insights.'''
def __init__(self, *args, **kwargs):
'''Initializes a new default instance of the HistoricalReturnsAlphaModel class.
Args:
lookback(int): Historical return lookback period
resolution: The resolution of historical data'''
self.lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.Daily
self.predictionInterval = Time.Multiply(Extensions.ToTimeSpan(self.resolution), self.lookback)
self.symbolDataBySymbol = {}
def Update(self, algorithm, data):
'''Updates this alpha model with the latest data from the algorithm.
This is called each time the algorithm receives data for subscribed securities
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbolData.CanEmit:
direction = InsightDirection.Flat
magnitude = symbolData.Return
if magnitude > 0: direction = InsightDirection.Up
if magnitude < 0: direction = InsightDirection.Down
insights.append(Insight.Price(symbol, self.predictionInterval, direction, magnitude, None))
return insights
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
for removed in changes.RemovedSecurities:
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
if symbolData is not None:
symbolData.RemoveConsolidators(algorithm)
# initialize data for added securities
symbols = [ x.Symbol for x in changes.AddedSecurities ]
history = algorithm.History(symbols, self.lookback, 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 = SymbolData(symbol, self.lookback)
self.symbolDataBySymbol[symbol] = symbolData
symbolData.RegisterIndicators(algorithm, self.resolution)
symbolData.WarmUpIndicators(history.loc[ticker])
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, lookback):
self.Symbol = symbol
self.ROC = RateOfChange('{}.ROC({})'.format(symbol, lookback), lookback)
self.Consolidator = None
self.previous = 0
def RegisterIndicators(self, algorithm, resolution):
self.Consolidator = algorithm.ResolveConsolidator(self.Symbol, resolution)
algorithm.RegisterIndicator(self.Symbol, self.ROC, self.Consolidator)
def RemoveConsolidators(self, algorithm):
if self.Consolidator is not None:
algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.Consolidator)
def WarmUpIndicators(self, history):
for tuple in history.itertuples():
self.ROC.Update(tuple.Index, tuple.close)
@property
def Return(self):
return float(self.ROC.Current.Value)
@property
def CanEmit(self):
if self.previous == self.ROC.Samples:
return False
self.previous = self.ROC.Samples
return self.ROC.IsReady
def __str__(self, **kwargs):
return '{}: {:.2%}'.format(self.ROC.Name, (1 + self.Return)**252 - 1)#region imports
from AlgorithmImports import *
#endregion
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 Portfolio.MinimumVariancePortfolioOptimizer import MinimumVariancePortfolioOptimizer
from datetime import timedelta
import numpy as np
import pandas as pd
### <summary>
### Provides an implementation of Mean-Variance portfolio optimization based on modern portfolio theory.
### The default model uses the MinimumVariancePortfolioOptimizer that accepts a 63-row matrix of 1-day returns.
### </summary>
class MeanVarianceOptimizationPortfolioConstructionModel(PortfolioConstructionModel):
def __init__(self,
rebalance = Resolution.Daily,
portfolioBias = PortfolioBias.LongShort,
lookback = 1,
period = 63,
resolution = Resolution.Daily,
targetReturn = 0.02,
optimizer = None):
"""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 (Short, Long/Short, Long)
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"""
self.lookback = lookback
self.period = period
self.resolution = resolution
self.portfolioBias = portfolioBias
self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0)
lower = 0 if portfolioBias == PortfolioBias.Long else -1
upper = 0 if portfolioBias == PortfolioBias.Short else 1
self.optimizer = MinimumVariancePortfolioOptimizer(lower, upper, targetReturn) if optimizer is None else optimizer
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('MeanVarianceOptimizationPortfolioConstructionModel does not accept \'None\' as Insight.Magnitude. Please checkout the selected Alpha Model specifications.'))
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 = {}
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)
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.LongShort 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:
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.MeanVarianceSymbolData(symbol, self.lookback, self.period)
symbolData.WarmUpIndicators(history.loc[ticker])
self.symbolDataBySymbol[symbol] = symbolData
class MeanVarianceSymbolData:
'''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)# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
class ConstantAlphaModel(AlphaModel):
''' Provides an implementation of IAlphaModel that always returns the same insight for each security'''
def __init__(self, type, direction, period, magnitude = None, confidence = None, weight = None):
'''Initializes a new instance of the ConstantAlphaModel class
Args:
type: The type of insight
direction: The direction of the insight
period: The period over which the insight with come to fruition
magnitude: The predicted change in magnitude as a +- percentage
confidence: The confidence in the insight
weight: The portfolio weight of the insights'''
self.type = type
self.direction = direction
self.period = period
self.magnitude = magnitude
self.confidence = confidence
self.weight = weight
self.securities = []
self.insightsTimeBySymbol = {}
typeString = Extensions.GetEnumString(type, InsightType)
directionString = Extensions.GetEnumString(direction, InsightDirection)
self.Name = '{}({},{},{}'.format(self.__class__.__name__, typeString, directionString, strfdelta(period))
if magnitude is not None:
self.Name += ',{}'.format(magnitude)
if confidence is not None:
self.Name += ',{}'.format(confidence)
self.Name += ')'
def Update(self, algorithm, data):
''' Creates a constant insight for each security as specified via the constructor
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for security in self.securities:
# security price could be zero until we get the first data point. e.g. this could happen
# when adding both forex and equities, we will first get a forex data point
if security.Price != 0 and self.ShouldEmitInsight(algorithm.UtcTime, security.Symbol):
insights.append(Insight(security.Symbol, self.period, self.type, self.direction, self.magnitude, self.confidence, weight = self.weight))
return insights
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'''
for added in changes.AddedSecurities:
self.securities.append(added)
# this will allow the insight to be re-sent when the security re-joins the universe
for removed in changes.RemovedSecurities:
if removed in self.securities:
self.securities.remove(removed)
if removed.Symbol in self.insightsTimeBySymbol:
self.insightsTimeBySymbol.pop(removed.Symbol)
def ShouldEmitInsight(self, utcTime, symbol):
generatedTimeUtc = self.insightsTimeBySymbol.get(symbol)
if generatedTimeUtc is not None:
# we previously emitted a insight for this symbol, check it's period to see
# if we should emit another insight
if utcTime - generatedTimeUtc < self.period:
return False
# we either haven't emitted a insight for this symbol or the previous
# insight's period has expired, so emit a new insight now for this symbol
self.insightsTimeBySymbol[symbol] = utcTime
return True
def strfdelta(tdelta):
d = tdelta.days
h, rem = divmod(tdelta.seconds, 3600)
m, s = divmod(rem, 60)
return "{}.{:02d}:{:02d}:{:02d}".format(d,h,m,s)#region imports
from AlgorithmImports import *
#endregion
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")
from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import *
class EmaCrossAlphaModel(AlphaModel):
'''Alpha model that uses an EMA cross to create insights'''
def __init__(self,
fastPeriod = 12,
slowPeriod = 26,
resolution = Resolution.Daily):
'''Initializes a new instance of the EmaCrossAlphaModel class
Args:
fastPeriod: The fast EMA period
slowPeriod: The slow EMA period'''
self.fastPeriod = fastPeriod
self.slowPeriod = slowPeriod
self.resolution = resolution
self.predictionInterval = Time.Multiply(Extensions.ToTimeSpan(resolution), fastPeriod)
self.symbolDataBySymbol = {}
resolutionString = Extensions.GetEnumString(resolution, Resolution)
self.Name = '{}({},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, resolutionString)
def Update(self, algorithm, data):
'''Updates this alpha model with the latest data from the algorithm.
This is called each time the algorithm receives data for subscribed securities
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbolData.Fast.IsReady and symbolData.Slow.IsReady:
if symbolData.FastIsOverSlow:
if symbolData.Slow > symbolData.Fast:
insights.append(Insight.Price(symbolData.Symbol, self.predictionInterval, InsightDirection.Down))
elif symbolData.SlowIsOverFast:
if symbolData.Fast > symbolData.Slow:
insights.append(Insight.Price(symbolData.Symbol, self.predictionInterval, InsightDirection.Up))
symbolData.FastIsOverSlow = symbolData.Fast > symbolData.Slow
return insights
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'''
for added in changes.AddedSecurities:
symbolData = self.symbolDataBySymbol.get(added.Symbol)
if symbolData is None:
# create fast/slow EMAs
symbolData = SymbolData(added)
symbolData.Fast = algorithm.EMA(added.Symbol, self.fastPeriod, self.resolution)
symbolData.Slow = algorithm.EMA(added.Symbol, self.slowPeriod, self.resolution)
self.symbolDataBySymbol[added.Symbol] = symbolData
else:
# a security that was already initialized was re-added, reset the indicators
symbolData.Fast.Reset()
symbolData.Slow.Reset()
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, security):
self.Security = security
self.Symbol = security.Symbol
self.Fast = None
self.Slow = None
# True if the fast is above the slow, otherwise false.
# This is used to prevent emitting the same signal repeatedly
self.FastIsOverSlow = False
@property
def SlowIsOverFast(self):
return not self.FastIsOverSlow
# Your New Python File#region imports
from AlgorithmImports import *
#endregion
"""
INTRODUCTION TO RISK MANAGEMENT IN THE QC FRAMEWORK
This algorithm demonstrates how risk management fits into the QuantConnect
Algorithm Framework.
The framework structure is:
1. Universe Selection:
The strategy uses a manual ETF universe focused on equity factor ETFs and SPY.
The universe includes minimum volatility, dividend growth, quality, dividends,
momentum, value, developed ex-U.S. minimum volatility, emerging-market minimum
volatility, international dividend, international quality, and broad U.S.
equity exposure through SPY.
2. Alpha Model:
The alpha model is intentionally simple and pedagogical. It emits long-only
Up insights once per month for every ETF in the universe. This means the alpha
layer is not trying to forecast relative performance. It simply says that each
ETF is eligible for investment during the next monthly period.
3. Portfolio Construction:
The EqualWeightingPortfolioConstructionModel allocates equally across ETFs that
have active Up insights. This keeps portfolio construction easy to understand.
4. Execution:
The ImmediateExecutionModel submits orders as soon as portfolio targets are
created.
5. Risk Management:
The MaximumDrawdownPercentPortfolio model monitors the total portfolio drawdown.
If the portfolio falls more than the configured threshold from its trailing
peak, the risk model reduces exposure. This is a portfolio-level risk rule.
It does not select securities; it acts after portfolio targets have been created.
This version uses a 5% trailing portfolio drawdown limit. The purpose is not to
optimize performance, but to show how a risk model can sit inside the framework
and override the portfolio when losses exceed a predefined threshold.
The benchmark is SPY buy-and-hold. Since this is an equity-factor ETF universe,
SPY is a simple and relevant market reference.
"""
class MonthlyConstantAlphaModel(AlphaModel):
def __init__(self, insight_duration_days=35, magnitude=0.025):
self.insight_duration = timedelta(days=insight_duration_days)
self.magnitude = magnitude
self.symbols = []
self.last_emit_month = None
def Update(self, algorithm, data):
insights = []
if algorithm.IsWarmingUp:
return insights
current_month = (algorithm.Time.year, algorithm.Time.month)
# Emit insights only once per month.
# This avoids creating repeated daily insights and keeps the example clear.
if self.last_emit_month == current_month:
return insights
for symbol in self.symbols:
if not algorithm.Securities.ContainsKey(symbol):
continue
security = algorithm.Securities[symbol]
if not security.HasData:
continue
insights.append(
Insight.Price(
symbol,
self.insight_duration,
InsightDirection.Up,
self.magnitude,
1.0
)
)
self.last_emit_month = current_month
algorithm.Debug(
"Monthly alpha emitted "
+ str(len(insights))
+ " insights on "
+ str(algorithm.Time.date())
)
return insights
def OnSecuritiesChanged(self, algorithm, changes):
for security in changes.AddedSecurities:
if security.Symbol not in self.symbols:
self.symbols.append(security.Symbol)
for security in changes.RemovedSecurities:
if security.Symbol in self.symbols:
self.symbols.remove(security.Symbol)
class MeanVarianceOptimizationAlgorithm(QCAlgorithm):
def Initialize(self):
# ------------------------------------------------------------
# 1. BACKTEST SETTINGS
# ------------------------------------------------------------
self.SetStartDate(2022, 1, 1)
self.SetEndDate(2026, 5, 5)
self.initial_cash = 10000000
self.SetCash(self.initial_cash)
# ------------------------------------------------------------
# 2. UNIVERSE SELECTION
# ------------------------------------------------------------
self.UniverseSettings.Resolution = Resolution.Daily
tickers = [
"USMV",
"DGRO",
"QUAL",
"DVY",
"MTUM",
"VLUE",
"EFAV",
"EEMV",
"IDV",
"IQLT",
"SPY"
]
symbols = [
Symbol.Create(ticker, SecurityType.Equity, Market.USA)
for ticker in tickers
]
self.SetUniverseSelection(
ManualUniverseSelectionModel(symbols)
)
# ------------------------------------------------------------
# 3. ALPHA MODEL
# ------------------------------------------------------------
# Monthly long-only constant alpha.
# This is cleaner than daily one-day insights for a risk-management demo.
self.AddAlpha(
MonthlyConstantAlphaModel(
insight_duration_days=35,
magnitude=0.025
)
)
self.SetWarmUp(30, Resolution.Daily)
# ------------------------------------------------------------
# 4. PORTFOLIO CONSTRUCTION MODEL
# ------------------------------------------------------------
self.SetPortfolioConstruction(
EqualWeightingPortfolioConstructionModel()
)
# ------------------------------------------------------------
# 5. EXECUTION MODEL
# ------------------------------------------------------------
self.SetExecution(
ImmediateExecutionModel()
)
# ------------------------------------------------------------
# 6. RISK MANAGEMENT MODEL
# ------------------------------------------------------------
# Portfolio-level trailing drawdown limit.
# 0.05 means 5%.
self.risk_drawdown_limit = 0.05
self.SetRiskManagement(
MaximumDrawdownPercentPortfolio(
self.risk_drawdown_limit,
isTrailing=True
)
)
# ------------------------------------------------------------
# 7. BENCHMARK
# ------------------------------------------------------------
self._benchmark = self.AddEquity(
"SPY",
Resolution.Daily,
Market.USA
).Symbol
self.SetBenchmark(self._benchmark)
self.initial_benchmark_price = None
# ------------------------------------------------------------
# 8. DIAGNOSTIC STATE VARIABLES
# ------------------------------------------------------------
self.strategy_peak = self.initial_cash
self.benchmark_peak = self.initial_cash
def OnData(self, data):
# ------------------------------------------------------------
# 1. CHECK BENCHMARK DATA
# ------------------------------------------------------------
if self._benchmark not in data or data[self._benchmark] is None:
return
benchmark_price = self.Securities[self._benchmark].Price
if benchmark_price <= 0:
return
if self.initial_benchmark_price is None:
self.initial_benchmark_price = benchmark_price
# ------------------------------------------------------------
# 2. BENCHMARK VALUE
# ------------------------------------------------------------
benchmark_value = (
self.initial_cash
* benchmark_price
/ self.initial_benchmark_price
)
# ------------------------------------------------------------
# 3. STRATEGY EQUITY VS BENCHMARK
# ------------------------------------------------------------
self.Plot(
"Strategy Equity",
"Portfolio Value",
self.Portfolio.TotalPortfolioValue
)
self.Plot(
"Strategy Equity",
"Buy Hold SPY",
benchmark_value
)
# ------------------------------------------------------------
# 4. PORTFOLIO STATE
# ------------------------------------------------------------
invested_value = 0
active_holdings = 0
for holding in self.Portfolio.Values:
if holding.Invested:
invested_value += abs(holding.HoldingsValue)
active_holdings += 1
if self.Portfolio.TotalPortfolioValue > 0:
invested_weight = (
invested_value
/ self.Portfolio.TotalPortfolioValue
)
cash_weight = 1 - invested_weight
self.Plot(
"Portfolio State",
"Invested Weight",
invested_weight
)
self.Plot(
"Portfolio State",
"Cash Weight",
cash_weight
)
self.Plot(
"Portfolio Diagnostics",
"Active Holdings",
active_holdings
)
# ------------------------------------------------------------
# 5. DRAWDOWN DIAGNOSTICS
# ------------------------------------------------------------
self.strategy_peak = max(
self.strategy_peak,
self.Portfolio.TotalPortfolioValue
)
self.benchmark_peak = max(
self.benchmark_peak,
benchmark_value
)
strategy_drawdown = (
self.Portfolio.TotalPortfolioValue
/ self.strategy_peak
- 1
)
benchmark_drawdown = (
benchmark_value
/ self.benchmark_peak
- 1
)
self.Plot(
"Drawdown",
"Strategy Drawdown",
strategy_drawdown
)
self.Plot(
"Drawdown",
"Benchmark Drawdown",
benchmark_drawdown
)
# ------------------------------------------------------------
# 6. RISK LIMIT VISUALIZATION
# ------------------------------------------------------------
self.Plot(
"Risk Management",
"Drawdown Limit",
-self.risk_drawdown_limit
)
risk_triggered_marker = 0
if strategy_drawdown <= -self.risk_drawdown_limit:
risk_triggered_marker = 1
self.Plot(
"Risk Management",
"Risk Triggered",
risk_triggered_marker
)