The source files for all examples can be found in /examples.

Example 2: MeanRisk objectives

In this example we will show the different objective functions available in MeanRisk, and compare them to a benchmark.

using PortfolioOptimisers, PrettyTables
# Format for pretty tables.
tsfmt = (v, i, j) -> begin
    if j == 1
        return Date(v)
    else
        return v
    end
end;
resfmt = (v, i, j) -> begin
    if j == 1
        return v
    else
        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v
    end
end;

1. ReturnsResult data

We will use the same data as the previous example.

using CSV, TimeSeries, DataFrames

X = TimeArray(CSV.File(joinpath(@__DIR__, "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]
pretty_table(X[(end - 5):end]; formatters = tsfmt)

# Compute the returns
rd = prices_to_returns(X)
ReturnsResult
    nx | 20-element Vector{String}
     X | 252×20 Matrix{Float64}
    nf | nothing
     F | nothing
    ts | 252-element Vector{Dates.Date}
    iv | nothing
  ivpa | nothing

2. MeanRisk objectives

Here we will show the different objective functions available in MeanRisk. We will also use the semi-standard deviation risk measure.

using Clarabel
slv = Solver(; name = :clarabel1, solver = Clarabel.Optimizer,
             settings = Dict("verbose" => false),
             check_sol = (; allow_local = true, allow_almost = true))
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

Here we encounter another consequence of the design philosophy of PortfolioOptimisers. An entire class of risk measures can be categorised and consistently implemented as LowOrderMoment risk measures with different internal algorithms. This corresponds to the semi-standard deviation.

r = LowOrderMoment(;
                   alg = LowOrderDeviation(;
                                           alg = SecondLowerMoment(; alg = SqrtRiskExpr())))
LowOrderMoment
  settings | RiskMeasureSettings
           |   scale | Float64: 1.0
           |      ub | nothing
           |     rke | Bool: true
         w | nothing
        mu | nothing
       alg | LowOrderDeviation
           |    ve | SimpleVariance
           |       |          me | nothing
           |       |           w | nothing
           |       |   corrected | Bool: true
           |   alg | SecondLowerMoment
           |       |   alg | SqrtRiskExpr()

Since we will perform various optimisations on the same data, there's no need to redo work. Lets precompute the prior statistics using the EmpiricalPrior to avoid recomputing them every time we call the optimisation.

pr = prior(EmpiricalPrior(), rd)
LowOrderPrior
        X | 252×20 Matrix{Float64}
       mu | 20-element Vector{Float64}
    sigma | 20×20 Matrix{Float64}
     chol | nothing
        w | nothing
      ens | nothing
      kld | nothing
       ow | nothing
       rr | nothing
     f_mu | nothing
  f_sigma | nothing
      f_w | nothing

We can provide the prior result to JuMPOptimiser.

opt = JuMPOptimiser(; pe = pr, slv = slv)
JuMPOptimiser
      pe | LowOrderPrior
         |         X | 252×20 Matrix{Float64}
         |        mu | 20-element Vector{Float64}
         |     sigma | 20×20 Matrix{Float64}
         |      chol | nothing
         |         w | nothing
         |       ens | nothing
         |       kld | nothing
         |        ow | nothing
         |        rr | nothing
         |      f_mu | nothing
         |   f_sigma | nothing
         |       f_w | 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
      lt | nothing
      st | nothing
     lcs | nothing
     lcm | nothing
    cent | nothing
   gcard | nothing
  sgcard | nothing
    smtx | nothing
   sgmtx | nothing
     slt | nothing
     sst | nothing
    sglt | nothing
    sgst | nothing
    sets | nothing
    nplg | nothing
    cplg | nothing
      tn | nothing
      te | nothing
    fees | nothing
     ret | ArithmeticReturn
         |   ucs | nothing
         |    lb | nothing
     sce | SumScalariser: SumScalariser()
    ccnt | nothing
    cobj | nothing
      sc | Int64: 1
      so | Int64: 1
    card | nothing
   scard | nothing
     nea | nothing
      l1 | nothing
      l2 | nothing
      ss | nothing
  strict | Bool: false

Here we define the estimators for different objective functions.

# Minimum risk
mr1 = MeanRisk(; r = r, obj = MinimumRisk(), opt = opt)
# Maximum utility with risk aversion parameter 2
mr2 = MeanRisk(; r = r, obj = MaximumUtility(), opt = opt)
# Risk-free rate of 4.2/100/252
rf = 4.2 / 100 / 252
mr3 = MeanRisk(; r = r, obj = MaximumRatio(; rf = rf), opt = opt)
# Maximum return
mr4 = MeanRisk(; r = r, obj = MaximumReturn(), opt = opt)
MeanRisk
  opt | JuMPOptimiser
      |       pe | LowOrderPrior
      |          |         X | 252×20 Matrix{Float64}
      |          |        mu | 20-element Vector{Float64}
      |          |     sigma | 20×20 Matrix{Float64}
      |          |      chol | nothing
      |          |         w | nothing
      |          |       ens | nothing
      |          |       kld | nothing
      |          |        ow | nothing
      |          |        rr | nothing
      |          |      f_mu | nothing
      |          |   f_sigma | nothing
      |          |       f_w | 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
      |       lt | nothing
      |       st | nothing
      |      lcs | nothing
      |      lcm | nothing
      |     cent | nothing
      |    gcard | nothing
      |   sgcard | nothing
      |     smtx | nothing
      |    sgmtx | nothing
      |      slt | nothing
      |      sst | nothing
      |     sglt | nothing
      |     sgst | nothing
      |     sets | nothing
      |     nplg | nothing
      |     cplg | nothing
      |       tn | nothing
      |       te | nothing
      |     fees | nothing
      |      ret | ArithmeticReturn
      |          |   ucs | nothing
      |          |    lb | nothing
      |      sce | SumScalariser: SumScalariser()
      |     ccnt | nothing
      |     cobj | nothing
      |       sc | Int64: 1
      |       so | Int64: 1
      |     card | nothing
      |    scard | nothing
      |      nea | nothing
      |       l1 | nothing
      |       l2 | nothing
      |       ss | nothing
      |   strict | Bool: false
    r | LowOrderMoment
      |   settings | RiskMeasureSettings
      |            |   scale | Float64: 1.0
      |            |      ub | nothing
      |            |     rke | Bool: true
      |          w | nothing
      |         mu | nothing
      |        alg | LowOrderDeviation
      |            |    ve | SimpleVariance
      |            |       |          me | nothing
      |            |       |           w | nothing
      |            |       |   corrected | Bool: true
      |            |   alg | SecondLowerMoment
      |            |       |   alg | SqrtRiskExpr()
  obj | MaximumReturn()
   wi | nothing

Lets perform the optimisations, but since we've precomputed the prior statistics, we do not need to provide the returns data. We will also produce a benchmark using the InverseVolatility estimator.

res1 = optimise!(mr1)
res2 = optimise!(mr2)
res3 = optimise!(mr3)
res4 = optimise!(mr4)
res0 = optimise!(InverseVolatility(; pe = pr))
NaiveOptimisation
       oe | DataType: InverseVolatility
       pr | LowOrderPrior
          |         X | 252×20 Matrix{Float64}
          |        mu | 20-element Vector{Float64}
          |     sigma | 20×20 Matrix{Float64}
          |      chol | nothing
          |         w | nothing
          |       ens | nothing
          |       kld | nothing
          |        ow | nothing
          |        rr | nothing
          |      f_mu | nothing
          |   f_sigma | nothing
          |       f_w | nothing
        w | 20-element Vector{Float64}
  retcode | OptimisationSuccess
          |   res | nothing

Lets view the results as pretty tables.

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 % │
└────────┴───────────┴─────────────┴────────────────┴──────────────┴───────────────┘

In order to confirm that the objective functions do what they say on the tin, we can compute the risk, return and risk return ration. There are individual functions for each expected_risk, expected_return, expected_ratio, but we also have expected_risk_ret_ratio that returns all three at once (risk, return, risk-return ratio) which is what we will use here.

Due to the fact that we provide different expected portfolio return measures, any function that computes the expected portfolio return also needs to know which return type to compute. We will be consistent with the returns we used in the optimisation.

rk1, rt1, rr1 = expected_risk_ret_ratio(r, res1.ret, res1.w, res1.pr; rf = rf);
rk2, rt2, rr2 = expected_risk_ret_ratio(r, res2.ret, res2.w, res2.pr; rf = rf);
rk3, rt3, rr3 = expected_risk_ret_ratio(r, res3.ret, res3.w, res3.pr; rf = rf);
rk4, rt4, rr4 = expected_risk_ret_ratio(r, res4.ret, res4.w, res4.pr; rf = rf);
rk0, rt0, rr0 = expected_risk_ret_ratio(r, ArithmeticReturn(), res0.w, res0.pr; rf = rf);

Lets make sure the results are what we expect.

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 % │
└────────────────┴─────────┴─────────┴──────────┘

We can seee that indeed, the minimum risk produces the portfolio with minimum risk, the maximum ratio produces the portfolio with the maximum risk-return ratio, and the maximum return portfolio produces the portfolio with the maximum return.


This page was generated using Literate.jl.