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.