The source files can be found in examples/.
Drawdown risk measures
A drawdown is how far the running sum of the portfolio returns is below its previous high at each point in time. The variance and the conditional value at risk (CVaR) use the distribution of the returns of single periods, and the order of those returns does not count. A drawdown measure uses the path of the cumulative returns, which gives the sum of the returns from any peak to any later trough.
This page uses four of the library's drawdown measures.
| Measure | What it penalises |
|---|---|
MaximumDrawdown | The worst decline from a peak to a trough over the whole period |
AverageDrawdown | The mean depth of the drawdown curve over time |
UlcerIndex | The root mean square of the drawdown curve, which penalises deep drawdowns more than AverageDrawdown does |
ConditionalDrawdownatRisk | The conditional drawdown at risk (CDaR), the drawdown form of CVaR. It is the mean drawdown over the worst fraction α of days |
Reach for a drawdown measure when the path to recovery matters. Examples are a trend-following strategy, a strategy for retail investors who can sell at the worst moment, and any portfolio whose report states the depth and the length of its drawdowns. Minimising the variance ignores the order of the returns, and these measures depend on it.
using PortfolioOptimisers, PrettyTables, DataFramesresfmt = (v, i, j) -> begin if j == 1 return v else return isa(v, Number) ? "$(round(v * 100, digits = 3)) %" : v endend;1. Data and shared setup
using CSV, TimeSeries, ClarabelX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)pr = prior(EmpiricalPrior(), rd)slv = [Solver(; name = :clarabel1, solver = Clarabel.Optimizer, settings = Dict("verbose" => false), check_sol = (; allow_local = true, allow_almost = true)), Solver(; name = :clarabel2, solver = Clarabel.Optimizer, settings = Dict("verbose" => false, "max_step_fraction" => 0.95), check_sol = (; allow_local = true, allow_almost = true)), Solver(; name = :clarabel3, solver = Clarabel.Optimizer, settings = Dict("verbose" => false, "max_step_fraction" => 0.9), check_sol = (; allow_local = true, allow_almost = true))]opt = JuMPOptimiser(; pe = pr, slv = slv)JuMPOptimiser
pe ┼ LowOrderPrior
│ X ┼ 252×20 Matrix{Float64}
│ o_X ┼ nothing
│ mu ┼ 20-element Vector{Float64}
│ sigma ┼ 20×20 Matrix{Float64}
│ chol ┼ nothing
│ w ┼ nothing
│ ens ┼ nothing
│ kld ┼ nothing
│ ow ┼ nothing
│ rr ┼ nothing
│ fpr ┼ nothing
│ x_src ┼ nothing
│ ss_gaps ┼ nothing
│ pc ┴ nothing
slv ┼ 3-element Vector{Solver}
│ Solver ⋯
│ Solver ⋯
│ Solver ⋯
wb ┼ WeightBounds
│ lb ┼ Float64: 0.0
│ ub ┴ Float64: 1.0
bgt ┼ Float64: 1.0
sbgt ┼ nothing
gbgt ┼ nothing
lt ┼ nothing
st ┼ nothing
lcse ┼ nothing
cte ┼ nothing
gcarde ┼ nothing
sgcarde ┼ nothing
smtx ┼ nothing
sgmtx ┼ nothing
slt ┼ nothing
sst ┼ nothing
sglt ┼ nothing
sgst ┼ nothing
tn ┼ nothing
fees ┼ nothing
sets ┼ nothing
tr ┼ nothing
ple ┼ nothing
ret ┼ ArithmeticReturn
│ settings ┼ JuMPReturnsSettings
│ │ scale ┼ Float64: 1.0
│ │ lb ┼ nothing
│ │ rte ┼ Bool: true
│ │ fee ┼ Bool: true
│ │ mic ┴ Bool: true
│ ucs ┼ nothing
│ mu ┼ nothing
│ mtx_sqrt ┴ EigenFallbackSquareRoot()
sca ┼ SumScalariser()
ccnt ┼ nothing
cobj ┼ nothing
sc ┼ Int64: 1
so ┼ Int64: 1
ss ┼ nothing
card ┼ nothing
scard ┼ nothing
l2c ┼ nothing
lpc ┼ nothing
linfc ┼ nothing
l1 ┼ nothing
l2 ┼ nothing
lp ┼ nothing
linf ┼ nothing
xbgt ┼ Bool: false
strict ┴ Bool: false
2. Minimising each drawdown measure
MeanRisk accepts these measures with their defaults. Each constructor takes an optional settings::RiskMeasureSettings. AverageDrawdown and ConditionalDrawdownatRisk also take observation weights w, and ConditionalDrawdownatRisk takes alpha, the tail probability, which is 0.05 by default. We minimise each measure and print the weights.
r_mdd = MaximumDrawdown()r_add = AverageDrawdown()r_uci = UlcerIndex()r_cdar = ConditionalDrawdownatRisk()results = map([r_mdd, r_add, r_uci, r_cdar]) do r return optimise(MeanRisk(; r = r, opt = opt))endlabels = ["MDD", "ADD", "Ulcer", "CDaR 5%"]pretty_table(DataFrame(hcat(rd.nx, [r.w for r in results]...), [:assets; Symbol.(labels)...]); formatters = [resfmt])┌────────┬──────────┬──────────┬──────────┬──────────┐
│ assets │ MDD │ ADD │ Ulcer │ CDaR 5% │
│ Any │ Any │ Any │ Any │ Any │
├────────┼──────────┼──────────┼──────────┼──────────┤
│ AAPL │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ AMD │ -0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │
│ BAC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BBY │ -0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │
│ CVX │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ GE │ -0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │
│ HD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ JNJ │ 67.181 % │ 19.758 % │ 29.674 % │ 38.725 % │
│ JPM │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ KO │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ LLY │ 6.369 % │ 0.0 % │ 4.612 % │ 5.567 % │
│ MRK │ 5.662 % │ 38.898 % │ 25.462 % │ 7.176 % │
│ MSFT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PEP │ 10.952 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PFE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PG │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ RRC │ -0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │
│ UNH │ 0.0 % │ 18.475 % │ 16.226 % │ 9.485 % │
│ WMT │ 4.773 % │ 6.206 % │ 6.432 % │ 12.791 % │
│ XOM │ 5.064 % │ 16.663 % │ 17.594 % │ 26.254 % │
└────────┴──────────┴──────────┴──────────┴──────────┘MaximumDrawdown depends only on the worst decline, so it puts its weight in the assets that reduce that one loss. AverageDrawdown and UlcerIndex use the whole drawdown curve, and CDaR uses the worst 5 % of it.
using StatsPlots, GraphRecipesplot_stacked_bar_composition(results, rd)Four drawdown curves on one line chart are hard to tell apart, so we compare them with a heatmap. Each row is one portfolio and each column one day. The colour of a cell shows the depth of the drawdown, and the colour bar gives the scale.
drawdown_grid = hcat([(-drawdowns(rd.X * res.w)) for res in results]...)heatmap(eachindex(rd.ts), labels, drawdown_grid'; xlabel = "Day", ylabel = "Minimised measure", colorbar_title = "Drawdown", title = "Daily drawdown depth of each portfolio")3. The tail level alpha of CDaR
alpha sets the fraction of the worst days that ConditionalDrawdownatRisk averages. As alpha → 0 it uses only the deepest drawdowns. With a larger alpha the measure averages more days, and it moves toward the average drawdown. We minimise CDaR for four values of alpha and print the weights.
alphas = [0.01, 0.05, 0.1, 0.25]cdar_results = [optimise(MeanRisk(; r = ConditionalDrawdownatRisk(; alpha = a), opt = opt)) for a in alphas]pretty_table(DataFrame(hcat(rd.nx, [r.w for r in cdar_results]...), [:assets; Symbol.("CDaR_" .* string.(alphas))...]); formatters = [resfmt])┌────────┬───────────┬───────────┬──────────┬───────────┐
│ assets │ CDaR_0.01 │ CDaR_0.05 │ CDaR_0.1 │ CDaR_0.25 │
│ Any │ Any │ Any │ Any │ Any │
├────────┼───────────┼───────────┼──────────┼───────────┤
│ AAPL │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ AMD │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │
│ BAC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BBY │ 0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ CVX │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ GE │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │
│ HD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ JNJ │ 63.749 % │ 38.725 % │ 34.907 % │ 31.571 % │
│ JPM │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ KO │ 1.179 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ LLY │ 8.586 % │ 5.567 % │ 3.354 % │ 5.208 % │
│ MRK │ 0.0 % │ 7.176 % │ 15.667 % │ 22.41 % │
│ MSFT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PEP │ 0.0 % │ 0.0 % │ 6.188 % │ 0.0 % │
│ PFE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PG │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ RRC │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │
│ UNH │ 0.0 % │ 9.485 % │ 12.444 % │ 14.243 % │
│ WMT │ 11.425 % │ 12.791 % │ 7.425 % │ 10.14 % │
│ XOM │ 15.061 % │ 26.254 % │ 20.015 % │ 16.428 % │
└────────┴───────────┴───────────┴──────────┴───────────┘As alpha rises, the weight of JNJ falls and the weights of MRK and UNH grow. All three move toward their weights in the ADD column of section 2.
4. Bounding the drawdown instead of minimising it
A drawdown measure need not be the objective. You can set an upper bound on it and optimise the return instead. We maximise the risk-adjusted return with CDaR at most 0.08, and set that bound as the ub of RiskMeasureSettings.
rf = 4.2 / 100 / 252r_cdar_ub = ConditionalDrawdownatRisk(; settings = RiskMeasureSettings(; ub = 0.08))res_cdar_max_ratio = optimise(MeanRisk(; r = r_cdar_ub, obj = MaximumRatio(; rf = rf), opt = opt))println("CDaR-constrained max-ratio retcode: $(res_cdar_max_ratio.retcode)")CDaR-constrained max-ratio retcode: OptimisationSuccess
res ┴ Dict{Any, Any}: Dict{Any, Any}()5. Drawdown statistics after the optimisation
When you have a portfolio, drawdowns() and cumulative_returns() return its drawdown series and its cumulative return series over the sample. By default both add the returns up without compounding. They are not objectives, so use them to study a portfolio after the optimiser has run. expected_risk with the measures of section 2 gives the statistics. We compare the minimum-variance portfolio with the minimum-CDaR portfolio of section 2.
w_var = optimise(MeanRisk(; r = Variance(), opt = opt)).ww_cdar = results[4].wret_var = rd.X * w_varret_cdar = rd.X * w_cdarcr_var = cumulative_returns(ret_var)cr_cdar = cumulative_returns(ret_cdar)dd_var = drawdowns(ret_var)dd_cdar = drawdowns(ret_cdar)rs_dd = [r_mdd, r_add, r_uci, r_cdar]pretty_table(DataFrame(; :Metric => ["Maximum drawdown", "Average drawdown", "Ulcer index", "CDaR 5%"], :MinVariance => [expected_risk(r, w_var, rd.X) for r in rs_dd], :MinCDaR => [expected_risk(r, w_cdar, rd.X) for r in rs_dd]); formatters = [resfmt])┌──────────────────┬─────────────┬─────────┐
│ Metric │ MinVariance │ MinCDaR │
│ String │ Float64 │ Float64 │
├──────────────────┼─────────────┼─────────┤
│ Maximum drawdown │ 9.19 % │ 8.965 % │
│ Average drawdown │ 2.622 % │ 1.902 % │
│ Ulcer index │ 3.472 % │ 2.535 % │
│ CDaR 5% │ 7.866 % │ 5.988 % │
└──────────────────┴─────────────┴─────────┘Compare the two columns row by row. The minimum-variance portfolio has the lower variance, but the variance ignores the order of the returns, and the four rows compare the depth of the drawdowns of the two portfolios. We plot the cumulative returns of both portfolios.
plot(cr_var; label = "Minimum variance", xlabel = "Day", ylabel = "Cumulative return", title = "Cumulative return paths")plot!(cr_cdar; label = "Minimum CDaR")We plot the drawdowns of the same two portfolios.
plot(dd_var; label = "Minimum variance", xlabel = "Day", ylabel = "Drawdown", title = "Drawdown paths")plot!(dd_cdar; label = "Minimum CDaR")Summary
Drawdown measures use the path of the cumulative returns.
MaximumDrawdowndepends on the worst decline alone, and its portfolio can put most of its weight in a few assets.AverageDrawdownandUlcerIndexpenalise the whole drawdown curve.ConditionalDrawdownatRiskis the drawdown form of CVaR, andalphasets its tail as it does for CVaR.drawdowns()andcumulative_returns()return the drawdown series and the cumulative return series of any portfolio,rd.X * w, andexpected_riskcomputes the statistics, without a new optimisation.
This page was generated using Literate.jl.