The source files can be found in examples/.
MeanRisk objectives
MeanRisk trades expected return against risk, and its objective, the obj field, picks the point on that trade-off that you get. With one prior and one risk measure, the four objectives give four different portfolios.
MinimumRiskignores return and takes the portfolio with the least risk.MaximumReturnignores risk and takes the portfolio with the highest return, which is a corner solution.MaximumRatiomaximises the ratio of return, net of the risk-free rate, to risk. This is the tangency portfolio.MaximumUtilitymaximisesreturn − l · risk. The risk aversionlmoves the portfolio between the high-return end and the low-risk end.
This page runs all four next to a benchmark. It shows how the risk aversion of MaximumUtility changes the portfolio, and then prints the risk, return and ratio of each portfolio.
Reach for MeanRisk when you want a portfolio that trades expected return against risk, with one objective to pick the point. The objective can minimise risk, maximise return, maximise the risk-adjusted ratio, or maximise a utility that penalises risk. If you want to allocate the risk itself instead of trading it against return, see RiskBudgeting. If you want the whole curve of the trade-off instead of one point, see the efficient-frontier example.
using PortfolioOptimisers, PrettyTablestsfmt = (v, i, j) -> begin if j == 1 return Date(v) else return v endend;resfmt = (v, i, j) -> begin if j == 1 return v else return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v endend;1. Data
We load the last 253 days of the S&P 500 prices that the other optimiser examples use, and compute their returns.
using CSV, TimeSeries, DataFramesX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]pretty_table(X[(end - 5):end]; formatters = [tsfmt])rd = prices_to_returns(X)ReturnsResult
nx ┼ 20-element Vector{String}
X ┼ 252×20 Matrix{Float64}
nf ┼ nothing
F ┼ nothing
nb ┼ nothing
B ┼ nothing
ne ┼ nothing
E ┼ nothing
ts ┼ 252-element Vector{Date}
iv ┼ nothing
ivpa ┼ nothing
pnl ┼ AssetPanel
│ pf ┼ Vector{AbstractPanelField}: AbstractPanelField[]
│ amsk ┼ 252×20 AllTrueMask
│ emsk ┴ 252×20 AllTrueMask
2. The four objectives
We hold the risk measure fixed and change only the objective. The risk measure is the semi-standard deviation, and the library has no type of that name. We build it from LowOrderMoment, which covers a whole class of risk measures. SecondMoment selects the second moment, SemiMoment() keeps only the returns below the mean, and SOCRiskExpr takes the square root as a second-order cone expression.
using Clarabelslv = Solver(; name = :clarabel1, solver = Clarabel.Optimizer, settings = Dict("verbose" => false), check_sol = (; allow_local = true, allow_almost = true))r = LowOrderMoment(; alg = SecondMoment(; alg1 = SemiMoment(), alg2 = SOCRiskExpr()))LowOrderMoment
settings ┼ RiskMeasureSettings
│ scale ┼ Float64: 1.0
│ ub ┼ nothing
│ rke ┴ Bool: true
w ┼ nothing
mu ┼ nothing
alg ┼ SecondMoment
│ ve ┼ SimpleVariance
│ │ me ┼ nothing
│ │ w ┼ nothing
│ │ corrected ┴ Bool: true
│ alg1 ┼ SemiMoment()
│ alg2 ┴ SOCRiskExpr()
Every optimisation below runs on the same data, so we compute the prior once with EmpiricalPrior and pass the result to JuMPOptimiser. No call computes it again.
pr = prior(EmpiricalPrior(), rd)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 ┼ Solver
│ name ┼ Symbol: :clarabel1
│ solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
│ settings ┼ Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
│ check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
│ add_bridges ┴ Bool: true
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
pe = pr passes the result of prior(...). The optimiser reuses those statistics on every optimise call, so we do not pass the returns data again. You can instead pass the estimator, pe = EmpiricalPrior(), and call optimise(model, rd). The optimiser then computes the prior from the data you give it. Most examples here precompute the prior because they solve many times on one fixed window. Use the estimator form when the data changes between calls.
- Cross-validation fits the optimiser again on each training fold, so it needs the estimator form. A result fitted on the whole sample has already seen the test data, and the library does not accept one.
- The meta-optimisers
StackingandNestedClusteredgive their outer optimiser synthetic returns that they build from the inner solves. The assets of the outer optimiser are the inner portfolios, so a prior computed on the original assets does not apply, and that field takes an estimator.
The meta-optimisers and subset resampling and cross-validation examples show each case.
We build one MeanRisk per objective. Only the obj field differs between them.
mr1 = MeanRisk(; r = r, obj = MinimumRisk(), opt = opt)mr2 = MeanRisk(; r = r, obj = MaximumUtility(), opt = opt)rf = 4.2 / 100 / 252mr3 = MeanRisk(; r = r, obj = MaximumRatio(; rf = rf), opt = opt)mr4 = MeanRisk(; r = r, obj = MaximumReturn(), opt = opt)MeanRisk
opt ┼ 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 ┼ Solver
│ │ name ┼ Symbol: :clarabel1
│ │ solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
│ │ settings ┼ Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
│ │ check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
│ │ add_bridges ┴ Bool: true
│ 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
r ┼ LowOrderMoment
│ settings ┼ RiskMeasureSettings
│ │ scale ┼ Float64: 1.0
│ │ ub ┼ nothing
│ │ rke ┴ Bool: true
│ w ┼ nothing
│ mu ┼ nothing
│ alg ┼ SecondMoment
│ │ ve ┼ SimpleVariance
│ │ │ me ┼ nothing
│ │ │ w ┼ nothing
│ │ │ corrected ┴ Bool: true
│ │ alg1 ┼ SemiMoment()
│ │ alg2 ┴ SOCRiskExpr()
obj ┼ MaximumReturn()
wi ┼ nothing
fb ┴ nothing
We optimise each one. As a reference we also compute an InverseVolatility benchmark. It needs no solver, and it uses neither an objective nor the expected returns.
res1 = optimise(mr1)res2 = optimise(mr2)res3 = optimise(mr3)res4 = optimise(mr4)res0 = optimise(InverseVolatility(; pe = pr))NaiveOptimisationResult
pr ┼ 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
wb ┼ WeightBounds
│ lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
│ ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
fees ┼ nothing
retcode ┼ OptimisationSuccess
│ res ┴ nothing
w ┼ 20-element Vector{Float64}
imsk ┼ nothing
fb ┴ nothing
The table prints the weights side by side. The benchmark gives each asset a weight in proportion to the inverse of its volatility, so every asset gets one. The minimum-risk portfolio holds mostly assets with low volatility, and the maximum-return portfolio puts all its weight in the asset with the highest expected return.
pretty_table(DataFrame(; :assets => rd.nx, :benchmark => res0.w, :MinimumRisk => res1.w, :MaximumUtility => res2.w, :MaximumRatio => res3.w, :MaximumReturn => res4.w); formatters = [resfmt])┌────────┬───────────┬─────────────┬────────────────┬──────────────┬───────────────┐
│ assets │ benchmark │ MinimumRisk │ MaximumUtility │ MaximumRatio │ MaximumReturn │
│ String │ Float64 │ Float64 │ Float64 │ Float64 │ Float64 │
├────────┼───────────┼─────────────┼────────────────┼──────────────┼───────────────┤
│ AAPL │ 4.004 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ AMD │ 2.332 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BAC │ 4.39 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BBY │ 3.143 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ CVX │ 4.326 % │ 8.817 % │ 6.884 % │ 0.0 % │ 0.0 % │
│ GE │ 4.087 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ HD │ 4.55 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ JNJ │ 8.175 % │ 49.192 % │ 39.727 % │ 0.0 % │ 0.0 % │
│ JPM │ 4.771 % │ 3.414 % │ 0.689 % │ 0.0 % │ 0.0 % │
│ KO │ 7.239 % │ 9.206 % │ 11.461 % │ 0.0 % │ 0.0 % │
│ LLY │ 5.224 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ MRK │ 7.143 % │ 16.429 % │ 26.96 % │ 69.998 % │ 0.0 % │
│ MSFT │ 4.046 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PEP │ 7.32 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PFE │ 5.274 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PG │ 6.482 % │ 1.722 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ RRC │ 2.263 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ UNH │ 5.843 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ WMT │ 5.329 % │ 7.142 % │ 6.463 % │ 0.0 % │ 0.0 % │
│ XOM │ 4.058 % │ 4.078 % │ 7.816 % │ 30.002 % │ 100.0 % │
└────────┴───────────┴─────────────┴────────────────┴──────────────┴───────────────┘3. Risk aversion: tuning MaximumUtility
MinimumRisk and MaximumReturn are the two ends of the trade-off, and MaximumUtility moves between them. It maximises return − l · risk, so the risk aversion l sets the point. As l → 0 the utility favours return, and the portfolio moves toward the maximum-return corner. As l grows, the risk term dominates, and the portfolio moves toward the minimum-risk portfolio. The default is l = 2.
We optimise for five values of l and print the risk, return and ratio of each portfolio. Section 4 explains the call that computes them.
lambdas = [1, 2, 8, 32, 128]util = [optimise(MeanRisk(; r = r, obj = MaximumUtility(; l = l), opt = opt)) for l in lambdas]sweep = map(zip(lambdas, util)) do (l, res) rk, rt, rr = expected_risk_ret_ratio(res.r, res.ret, res.w, res.pr; sca = res.sca, rf = rf) return (l, rk, rt, rr)endpretty_table(DataFrame(; Symbol("risk aversion l") => [s[1] for s in sweep], :risk => [s[2] for s in sweep], :return => [s[3] for s in sweep], :ratio => [s[4] for s in sweep]); formatters = [resfmt], title = "MaximumUtility portfolios by risk aversion l") MaximumUtility portfolios by risk aversion l
┌─────────────────┬─────────┬─────────┬──────────┐
│ risk aversion l │ risk │ return │ ratio │
│ Int64 │ Float64 │ Float64 │ Float64 │
├─────────────────┼─────────┼─────────┼──────────┤
│ 1 │ 0.673 % │ 0.119 % │ 15.256 % │
│ 2 │ 0.657 % │ 0.098 % │ 12.333 % │
│ 8 │ 0.651 % │ 0.081 % │ 9.826 % │
│ 32 │ 0.651 % │ 0.076 % │ 9.135 % │
│ 128 │ 0.651 % │ 0.075 % │ 8.958 % │
└─────────────────┴─────────┴─────────┴──────────┘Risk and return both fall as l rises, so the portfolio moves down the efficient frontier from the high-return end toward the minimum-risk end. We plot the risk and return of each portfolio to show that path.
using StatsPlots, GraphRecipesplot([s[2] for s in sweep], [s[3] for s in sweep]; seriestype = :path, marker = (:circle, 5), xlabel = "Semi-standard deviation", ylabel = "Arithmetic return", title = "MaximumUtility risk-aversion path", label = "l = " * join(string.(lambdas), ", "))4. The risk, return and ratio of each portfolio
We compute the risk, the return and the risk-return ratio of every portfolio. expected_risk, expected_return and expected_ratio compute one each, and expected_risk_ret_ratio returns all three at once, so we use it here.
A function that computes the expected return of a portfolio needs to know which return measure to use. We use the one that each optimisation used.
The result of an optimisation stores what the call needs. res.r is the risk measure of the optimisation and res.ret its return measure, both as the optimisation used them. If you gave an estimator, the result contains the fitted measure. If you left a field unset, it contains the value that the optimiser took from the prior. res.sca is the scalariser, the rule that combines several risk values into one number. The multiple risk measures page uses it. The call takes all its arguments from the result:
expected_risk_ret_ratio(res.r, res.ret, res.w, res.pr; sca = res.sca, rf = rf)Prefer this form. You can name the measure by hand, but the number then matches the optimisation only if you name the same measure and the same scalariser, and pass the same fees and rf. The benchmark is the exception. InverseVolatility uses no risk measure and no return measure, so its result has no r or ret, and we name both ourselves.
rk1, rt1, rr1 = expected_risk_ret_ratio(res1.r, res1.ret, res1.w, res1.pr; sca = res1.sca, rf = rf);rk2, rt2, rr2 = expected_risk_ret_ratio(res2.r, res2.ret, res2.w, res2.pr; sca = res2.sca, rf = rf);rk3, rt3, rr3 = expected_risk_ret_ratio(res3.r, res3.ret, res3.w, res3.pr; sca = res3.sca, rf = rf);rk4, rt4, rr4 = expected_risk_ret_ratio(res4.r, res4.ret, res4.w, res4.pr; sca = res4.sca, rf = rf);rk0, rt0, rr0 = expected_risk_ret_ratio(r, ArithmeticReturn(), res0.w, res0.pr; rf = rf);In the table, rk is the risk, rt the return and rr the risk-return ratio. Read each objective off the column it optimises. MinimumRisk has the lowest rk, MaximumRatio the highest rr, and MaximumReturn the highest rt.
pretty_table(DataFrame(; :obj => [:MinimumRisk, :MaximumUtility, :MaximumRatio, :MaximumReturn, :Benchmark], :rk => [rk1, rk2, rk3, rk4, rk0], :rt => [rt1, rt2, rt3, rt4, rt0], :rr => [rr1, rr2, rr3, rr4, rr0]); formatters = [resfmt])┌────────────────┬─────────┬─────────┬──────────┐
│ obj │ rk │ rt │ rr │
│ Symbol │ Float64 │ Float64 │ Float64 │
├────────────────┼─────────┼─────────┼──────────┤
│ MinimumRisk │ 0.651 % │ 0.075 % │ 8.899 % │
│ MaximumUtility │ 0.657 % │ 0.098 % │ 12.333 % │
│ MaximumRatio │ 0.829 % │ 0.196 % │ 21.611 % │
│ MaximumReturn │ 1.621 % │ 0.264 % │ 15.236 % │
│ Benchmark │ 0.813 % │ 0.025 % │ 0.97 % │
└────────────────┴─────────┴─────────┴──────────┘5. Visualising the objectives
The stacked bars show the weights of the five portfolios. The objectives that maximise return or ratio put their weight in few assets, and the benchmark and the minimum-risk portfolio spread it over many.
plot_stacked_bar_composition([res0, res1, res2, res3, res4], rd)The histogram shows the daily returns of the minimum-risk portfolio, with lines at its mean, its value at risk (VaR), its conditional value at risk (CVaR) and the other levels named in the legend.
plot_histogram(res1, rd)The drawdown plot shows the loss of the minimum-risk portfolio from its last peak on each day.
plot_drawdowns(res1, rd)The last plot shows how much each asset adds to the semi-standard deviation of the minimum-risk portfolio.
plot_risk_contribution(r, res1, rd)This page was generated using Literate.jl.