Skip to content
13

The source files 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.

julia
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.

julia
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
    nb ┼ nothing
     B ┼ nothing
    ts ┼ 252-element Vector{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.

julia
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.

julia
r = LowOrderMoment(; alg = SecondMoment(; alg1 = Semi(), 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 ┼ Semi()
           │   alg2 ┴ SOCRiskExpr()

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

julia
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.

julia
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
     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
          │   ucs ┼ nothing
          │    lb ┼ nothing
          │    mu ┴ nothing
      sca ┼ SumScalariser()
     ccnt ┼ nothing
     cobj ┼ nothing
       sc ┼ Int64: 1
       so ┼ Int64: 1
       ss ┼ nothing
     card ┼ nothing
    scard ┼ nothing
      nea ┼ nothing
       l1 ┼ nothing
       l2 ┼ nothing
     linf ┼ nothing
       lp ┼ nothing
      brt ┼ Bool: false
   cle_pr ┼ Bool: true
   strict ┴ Bool: false

Here we define the estimators for different objective functions.

julia
# 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
      │      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
      │           │   ucs ┼ nothing
      │           │    lb ┼ nothing
      │           │    mu ┴ nothing
      │       sca ┼ SumScalariser()
      │      ccnt ┼ nothing
      │      cobj ┼ nothing
      │        sc ┼ Int64: 1
      │        so ┼ Int64: 1
      │        ss ┼ nothing
      │      card ┼ nothing
      │     scard ┼ nothing
      │       nea ┼ nothing
      │        l1 ┼ nothing
      │        l2 ┼ nothing
      │      linf ┼ nothing
      │        lp ┼ nothing
      │       brt ┼ Bool: false
      │    cle_pr ┼ Bool: true
      │    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 ┼ Semi()
      │            │   alg2 ┴ SOCRiskExpr()
  obj ┼ MaximumReturn()
   wi ┼ nothing
   fb ┴ nothing

Let's 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.

julia
res1 = optimise(mr1)
res2 = optimise(mr2)
res3 = optimise(mr3)
res4 = optimise(mr4)
res0 = optimise(InverseVolatility(; pe = pr))
NaiveOptimisationResult
       oe ┼ DataType: DataType
       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
       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}
  retcode ┼ OptimisationSuccess
          │   res ┴ nothing
        w ┼ 20-element Vector{Float64}
       fb ┴ nothing

Let's view the results as pretty tables.

julia
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  MaximumRe
 String    Float64      Float64         Float64       Float64        Flo
├────────┼───────────┼─────────────┼────────────────┼──────────────┼────────────
│   AAPL │   4.004 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    AMD │   2.332 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    BAC │    4.39 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    BBY │   3.143 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    CVX │   4.326 % │     8.817 % │        6.884 % │        0.0 % │         0 ⋯
│     GE │   4.087 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│     HD │    4.55 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    JNJ │   8.175 % │    49.192 % │       39.727 % │        0.0 % │         0 ⋯
│    JPM │   4.771 % │     3.414 % │        0.689 % │        0.0 % │         0 ⋯
│     KO │   7.239 % │     9.206 % │       11.461 % │        0.0 % │         0 ⋯
│    LLY │   5.224 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    MRK │   7.143 % │    16.429 % │        26.96 % │     69.998 % │         0 ⋯
│   MSFT │   4.046 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    PEP │    7.32 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│    PFE │   5.274 % │       0.0 % │          0.0 % │        0.0 % │         0 ⋯
│      ⋮ │         ⋮ │           ⋮ │              ⋮ │            ⋮ │           ⋱
└────────┴───────────┴─────────────┴────────────────┴──────────────┴────────────
                                                     1 column and 5 rows omitted

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.

julia
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);

Let's make sure the results are what we expect.

julia
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 see 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.