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.

  • MinimumRisk ignores return and takes the portfolio with the least risk.
  • MaximumReturn ignores risk and takes the portfolio with the highest return, which is a corner solution.
  • MaximumRatio maximises the ratio of return, net of the risk-free rate, to risk. This is the tangency portfolio.
  • MaximumUtility maximises return − l · risk. The risk aversion l moves 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.

When to reach for this

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
Precomputed result vs estimator

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 Stacking and NestedClustered give 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), ", "))
Example block output

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)
Example block output

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)
Example block output

The drawdown plot shows the loss of the minimum-risk portfolio from its last peak on each day.

plot_drawdowns(res1, rd)
Example block output

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)
Example block output

This page was generated using Literate.jl.