The source files can be found in examples/.
Meta-optimisers
Every optimiser so far gives weights by solving one problem. A meta-optimiser runs other optimisers instead. It splits the problem, solves each piece with the estimator you name, and puts the pieces back together. One fit over every asset depends on one set of estimates, and a meta-optimiser splits that dependence across several fits. It also lets you apply a different rule to a different part of the universe.
PortfolioOptimisers has three.
NestedClustered, or NCO, clusters the assets, runs an inner optimiser inside each cluster, and runs an outer optimiser over the clusters.Stackingruns several inner optimisers over every asset, then combines the portfolios they give with one outer optimiser.SubsetResamplingoptimises over random subsets of the assets many times and averages the weights, which is bagging applied to a portfolio.
NestedClustered takes one inner optimiser in opti and an outer one in opto. Stacking takes a list of inner optimisers in opti and an outer one in opto. SubsetResampling takes one optimiser in opt. Each of these keywords takes any optimiser that gives continuous weights, including another meta-optimiser, so you can nest them as deep as you like.
Reach for a meta-optimiser when one fit over every asset depends on more estimates than you trust. Use NCO when the cluster structure is sound and you want a different rule inside a cluster and across clusters. Use Stacking when you want several rules to share the decision rather than one. Use SubsetResampling when you want the weights to depend less on which assets are in the universe. If one optimiser already does the job, use it, because a meta-optimiser costs more time and more settings.
using PortfolioOptimisers, PrettyTables, StableRNGsresfmt = (v, i, j) -> begin if j == 1 return v else return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v endend;1. Data and shared ingredients
We use one year of daily prices for twenty S&P 500 stocks. We build a prior, a clustering and a solver once, and every meta-optimiser below uses them.
using CSV, TimeSeries, DataFrames, ClarabelX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)slv = Solver(; name = :clarabel1, solver = Clarabel.Optimizer, settings = Dict("verbose" => false), check_sol = (; allow_local = true, allow_almost = true))pr = prior(EmpiricalPrior(), rd)clr = clusterise(ClustersEstimator(; alg = DBHT()), pr.X)Clusters
res ┼ Clustering.Hclust{Float64}([-1 -13; -7 -4; … ; 12 17; 10 18], [0.1, 0.1111111111111111, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 2.0], [17, 20, 5, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 8, 12, 15], :DBHT)
S ┼ 20×20 Matrix{Float64}
D ┼ 20×20 Matrix{Float64}
P ┼ nothing
k ┴ Int64: 4
The inner and the outer optimiser take their statistics from different places, and the cell below shows how. The MeanRisk objectives page covers the wider point, that a keyword takes a computed result or an estimator that computes one.
The inner optimiser gets the prior we computed above, through pe = pr on its JuMPOptimiser. It solves over the returns of the real assets, and that is what the prior describes.
The outer optimiser gets no prior. It solves over the returns the meta-optimiser builds, one series per cluster for NCO and one per inner portfolio for Stacking. A prior over the assets does not describe those series. The outer optimiser computes what it needs from them when it solves, so it needs only a solver here. The constructor refuses a computed prior on the outer optimiser.
jopti = JuMPOptimiser(; pe = pr, slv = slv)jopto = JuMPOptimiser(; slv = slv)JuMPOptimiser
pe ┼ EmpiricalPrior
│ ce ┼ PortfolioOptimisersCovariance
│ │ ce ┼ Covariance
│ │ │ me ┼ SimpleExpectedReturns
│ │ │ │ w ┴ nothing
│ │ │ ce ┼ GeneralCovariance
│ │ │ │ ce ┼ SimpleCovariance: SimpleCovariance(true)
│ │ │ │ w ┴ nothing
│ │ │ alg ┼ FullMoment()
│ │ │ w ┴ nothing
│ │ mp ┼ MatrixProcessing
│ │ │ pdm ┼ Posdef
│ │ │ │ alg ┼ UnionAll: NearestCorrelationMatrix.Newton
│ │ │ │ kwargs ┴ @NamedTuple{}: NamedTuple()
│ │ │ dn ┼ nothing
│ │ │ dt ┼ nothing
│ │ │ alg ┼ nothing
│ │ │ order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
│ me ┼ SimpleExpectedReturns
│ │ w ┴ nothing
│ horizon ┼ nothing
│ fill_limit ┼ nothing
│ x_src ┴ 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
We also solve a plain minimum-variance MeanRisk over every asset, to compare against.
res_bench = optimise(MeanRisk(; obj = MinimumRisk(), opt = JuMPOptimiser(; pe = pr, slv = slv)))MeanRiskResult
jr ┼ JuMPOptimisationResult
│ pa ┼ ProcessedJuMPOptimiserAttributes
│ │ 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}
│ │ lt ┼ nothing
│ │ st ┼ nothing
│ │ lcsr ┼ nothing
│ │ ctr ┼ nothing
│ │ gcardr ┼ nothing
│ │ sgcardr ┼ nothing
│ │ smtx ┼ nothing
│ │ sgmtx ┼ nothing
│ │ slt ┼ nothing
│ │ sst ┼ nothing
│ │ sglt ┼ nothing
│ │ sgst ┼ nothing
│ │ tn ┼ nothing
│ │ fees ┼ nothing
│ │ plr ┼ nothing
│ │ ret ┼ ArithmeticReturn
│ │ │ settings ┼ JuMPReturnsSettings
│ │ │ │ scale ┼ Float64: 1.0
│ │ │ │ lb ┼ nothing
│ │ │ │ rte ┼ Bool: true
│ │ │ │ fee ┼ Bool: true
│ │ │ │ mic ┴ Bool: true
│ │ │ ucs ┼ nothing
│ │ │ mu ┼ 20-element Vector{Float64}
│ │ │ mtx_sqrt ┴ EigenFallbackSquareRoot()
│ │ sca ┼ SumScalariser()
│ │ imsk ┴ nothing
│ retcode ┼ OptimisationSuccess
│ │ res ┴ Dict{Any, Any}: Dict{Any, Any}()
│ sol ┼ JuMPOptimisationSolution
│ │ w ┴ 20-element Vector{Float64}
│ model ┼ A JuMP Model
│ │ ├ solver: Clarabel
│ │ ├ objective_sense: MIN_SENSE
│ │ │ └ objective_function_type: QuadExpr
│ │ ├ num_variables: 21
│ │ ├ num_constraints: 4
│ │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
│ │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
│ │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
│ │ │ └ Vector{AffExpr} in MOI.SecondOrderCone: 1
│ │ └ Names registered in the model
│ │ └ :G, :T, :bgt, :cdev_soc_1, :dev_1, :k, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :w_gross_ub, :w_lb, :w_ub
r ┼ Variance
│ settings ┼ RiskMeasureSettings
│ │ scale ┼ Float64: 1.0
│ │ ub ┼ nothing
│ │ rke ┴ Bool: true
│ sigma ┼ 20×20 Matrix{Float64}
│ chol ┼ nothing
│ rc ┼ nothing
│ alg ┼ SquaredSOCRiskExpr()
│ mtx_sqrt ┼ EigenFallbackSquareRoot()
│ ratio_alg ┼ SquareRootRatio()
│ corner ┴ nothing
fb ┴ nothing
2. Nested clustered optimisation (NCO)
NCO solves a minimum-variance problem inside each cluster. It then turns each cluster into one series of returns and solves a second minimum-variance problem over those series. The inner and the outer optimiser are independent. Both hold a MeanRisk here, and either one takes a risk-budgeting, hierarchical or naive estimator instead.
res_nco = optimise(NestedClustered(; pe = pr, cle = clr, opti = MeanRisk(; obj = MinimumRisk(), opt = jopti), opto = MeanRisk(; obj = MinimumRisk(), opt = jopto)), rd)NestedClusteredResult
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
clr ┼ Clusters
│ res ┼ Clustering.Hclust{Float64}([-1 -13; -7 -4; … ; 12 17; 10 18], [0.1, 0.1111111111111111, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 2.0], [17, 20, 5, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 8, 12, 15], :DBHT)
│ S ┼ 20×20 Matrix{Float64}
│ D ┼ 20×20 Matrix{Float64}
│ P ┼ nothing
│ k ┴ Int64: 4
wb ┼ WeightBounds
│ lb ┼ 20-element Vector{Float64}
│ ub ┴ 20-element Vector{Float64}
fees ┼ nothing
resi ┼ 4-element Vector{MeanRiskResult}
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
reso ┼ MeanRiskResult
│ jr ┼ JuMPOptimisationResult
│ │ pa ┼ ProcessedJuMPOptimiserAttributes
│ │ │ pr ┼ LowOrderPrior
│ │ │ │ X ┼ 252×4 Matrix{Float64}
│ │ │ │ o_X ┼ nothing
│ │ │ │ mu ┼ Vector{Float64}: [-0.0005703954772857657, 0.002111454811962959, 0.0005954569768867551, 0.00038875642679476487]
│ │ │ │ sigma ┼ 4×4 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 ┼ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(0.0, 0.0, 4)
│ │ │ │ ub ┴ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(1.0, 0.0, 4)
│ │ │ lt ┼ nothing
│ │ │ st ┼ nothing
│ │ │ lcsr ┼ nothing
│ │ │ ctr ┼ nothing
│ │ │ gcardr ┼ nothing
│ │ │ sgcardr ┼ nothing
│ │ │ smtx ┼ nothing
│ │ │ sgmtx ┼ nothing
│ │ │ slt ┼ nothing
│ │ │ sst ┼ nothing
│ │ │ sglt ┼ nothing
│ │ │ sgst ┼ nothing
│ │ │ tn ┼ nothing
│ │ │ fees ┼ nothing
│ │ │ plr ┼ nothing
│ │ │ ret ┼ ArithmeticReturn
│ │ │ │ settings ┼ JuMPReturnsSettings
│ │ │ │ │ scale ┼ Float64: 1.0
│ │ │ │ │ lb ┼ nothing
│ │ │ │ │ rte ┼ Bool: true
│ │ │ │ │ fee ┼ Bool: true
│ │ │ │ │ mic ┴ Bool: true
│ │ │ │ ucs ┼ nothing
│ │ │ │ mu ┼ Vector{Float64}: [-0.0005703954772857657, 0.002111454811962959, 0.0005954569768867551, 0.00038875642679476487]
│ │ │ │ mtx_sqrt ┴ EigenFallbackSquareRoot()
│ │ │ sca ┼ SumScalariser()
│ │ │ imsk ┴ nothing
│ │ retcode ┼ OptimisationSuccess
│ │ │ res ┴ Dict{Any, Any}: Dict{Any, Any}()
│ │ sol ┼ JuMPOptimisationSolution
│ │ │ w ┴ Vector{Float64}: [7.224724072738346e-7, 0.1366078644652331, 0.6133269234160051, 0.2500644896463545]
│ │ model ┼ A JuMP Model
│ │ │ ├ solver: Clarabel
│ │ │ ├ objective_sense: MIN_SENSE
│ │ │ │ └ objective_function_type: QuadExpr
│ │ │ ├ num_variables: 5
│ │ │ ├ num_constraints: 4
│ │ │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
│ │ │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
│ │ │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
│ │ │ │ └ Vector{AffExpr} in MOI.SecondOrderCone: 1
│ │ │ └ Names registered in the model
│ │ │ └ :G, :T, :bgt, :cdev_soc_1, :dev_1, :k, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :w_gross_ub, :w_lb, :w_ub
│ r ┼ Variance
│ │ settings ┼ RiskMeasureSettings
│ │ │ scale ┼ Float64: 1.0
│ │ │ ub ┼ nothing
│ │ │ rke ┴ Bool: true
│ │ sigma ┼ 4×4 Matrix{Float64}
│ │ chol ┼ nothing
│ │ rc ┼ nothing
│ │ alg ┼ SquaredSOCRiskExpr()
│ │ mtx_sqrt ┼ EigenFallbackSquareRoot()
│ │ ratio_alg ┼ SquareRootRatio()
│ │ corner ┴ nothing
│ fb ┴ nothing
cv ┼ nothing
retcode ┼ OptimisationSuccess
│ res ┴ nothing
w ┼ 20-element Vector{Float64}
imsk ┼ nothing
fb ┴ nothing
3. Stacking
Stacking runs a list of inner optimisers over every asset. The list below holds a minimum-variance MeanRisk, a HierarchicalRiskParity and a naive InverseVolatility. The outer optimiser then combines the three portfolios they give. No single rule sets the result on its own.
res_stk = optimise(Stacking(; pe = pr, opti = [MeanRisk(; opt = jopti), HierarchicalRiskParity(; opt = HierarchicalOptimiser(; pe = pr)), InverseVolatility(; pe = pr)], opto = MeanRisk(; obj = MinimumRisk(), opt = jopto)), rd)StackingResult
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 Vector{Float64}
│ ub ┴ 20-element Vector{Float64}
fees ┼ nothing
resi ┼ 3-element Vector{NonFiniteAllocationOptimisationResult}
│ MeanRiskResult ⋯
│ HierarchicalRiskParityResult ⋯
│ NaiveOptimisationResult ⋯
reso ┼ MeanRiskResult
│ jr ┼ JuMPOptimisationResult
│ │ pa ┼ ProcessedJuMPOptimiserAttributes
│ │ │ pr ┼ LowOrderPrior
│ │ │ │ X ┼ 252×3 Matrix{Float64}
│ │ │ │ o_X ┼ nothing
│ │ │ │ mu ┼ Vector{Float64}: [0.000772070998643933, 0.00043701664570415066, 0.00024546841241466914]
│ │ │ │ sigma ┼ 3×3 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 ┼ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(0.0, 0.0, 3)
│ │ │ │ ub ┴ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(1.0, 0.0, 3)
│ │ │ lt ┼ nothing
│ │ │ st ┼ nothing
│ │ │ lcsr ┼ nothing
│ │ │ ctr ┼ nothing
│ │ │ gcardr ┼ nothing
│ │ │ sgcardr ┼ nothing
│ │ │ smtx ┼ nothing
│ │ │ sgmtx ┼ nothing
│ │ │ slt ┼ nothing
│ │ │ sst ┼ nothing
│ │ │ sglt ┼ nothing
│ │ │ sgst ┼ nothing
│ │ │ tn ┼ nothing
│ │ │ fees ┼ nothing
│ │ │ plr ┼ nothing
│ │ │ ret ┼ ArithmeticReturn
│ │ │ │ settings ┼ JuMPReturnsSettings
│ │ │ │ │ scale ┼ Float64: 1.0
│ │ │ │ │ lb ┼ nothing
│ │ │ │ │ rte ┼ Bool: true
│ │ │ │ │ fee ┼ Bool: true
│ │ │ │ │ mic ┴ Bool: true
│ │ │ │ ucs ┼ nothing
│ │ │ │ mu ┼ Vector{Float64}: [0.000772070998643933, 0.00043701664570415066, 0.00024546841241466914]
│ │ │ │ mtx_sqrt ┴ EigenFallbackSquareRoot()
│ │ │ sca ┼ SumScalariser()
│ │ │ imsk ┴ nothing
│ │ retcode ┼ OptimisationSuccess
│ │ │ res ┴ Dict{Any, Any}: Dict{Any, Any}()
│ │ sol ┼ JuMPOptimisationSolution
│ │ │ w ┴ Vector{Float64}: [0.9999999289628075, 4.112082139056714e-8, 2.9916371244836604e-8]
│ │ model ┼ A JuMP Model
│ │ │ ├ solver: Clarabel
│ │ │ ├ objective_sense: MIN_SENSE
│ │ │ │ └ objective_function_type: QuadExpr
│ │ │ ├ num_variables: 4
│ │ │ ├ num_constraints: 4
│ │ │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
│ │ │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
│ │ │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
│ │ │ │ └ Vector{AffExpr} in MOI.SecondOrderCone: 1
│ │ │ └ Names registered in the model
│ │ │ └ :G, :T, :bgt, :cdev_soc_1, :dev_1, :k, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :w_gross_ub, :w_lb, :w_ub
│ r ┼ Variance
│ │ settings ┼ RiskMeasureSettings
│ │ │ scale ┼ Float64: 1.0
│ │ │ ub ┼ nothing
│ │ │ rke ┴ Bool: true
│ │ sigma ┼ 3×3 Matrix{Float64}
│ │ chol ┼ nothing
│ │ rc ┼ nothing
│ │ alg ┼ SquaredSOCRiskExpr()
│ │ mtx_sqrt ┼ EigenFallbackSquareRoot()
│ │ ratio_alg ┼ SquareRootRatio()
│ │ corner ┴ nothing
│ fb ┴ nothing
cv ┼ nothing
retcode ┼ OptimisationSuccess
│ res ┴ nothing
w ┼ 20-element Vector{Float64}
imsk ┼ nothing
fb ┴ nothing
4. Subset resampling
SubsetResampling draws a random subset of the assets, optimises over it, and repeats. It then averages the weights it collected. We draw 10 subsets, each holding 70% of the assets. The random number generator and the seed are fixed, so the cell gives the same weights on every run.
res_ssr = optimise(SubsetResampling(; pe = pr, opt = MeanRisk(; obj = MinimumRisk(), opt = JuMPOptimiser(; slv = slv)), subset_size = 0.7, n_subsets = 10, rng = StableRNG(123), seed = 42), rd)SubsetResamplingResult
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 Vector{Float64}
│ ub ┴ 20-element Vector{Float64}
fees ┼ nothing
ress ┼ 10-element Vector{MeanRiskResult}
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
│ MeanRiskResult ⋯
idx ┼ 14×10 Matrix{Int64}
retcode ┼ OptimisationSuccess
│ res ┴ nothing
w ┼ 20-element Vector{Float64}
imsk ┼ nothing
fb ┴ nothing
5. Comparing the allocations
The plain fit, NCO and subset resampling target minimum variance. The stacked portfolio combines three inner portfolios by minimum variance, and only one of them, the MeanRisk fit, targets minimum variance itself. The outer fit measures the variance of each mix on the same rows and with the same covariance estimator as that inner fit, and every long-only mix of the three members is a portfolio the inner fit could have chosen. No mix has a lower variance, so on any returns the outer fit puts all its weight on the minimum-variance member, and the Stacking column is the MinVar column. Read the other two meta-optimiser columns against the plain fit, and read how far each one spreads the weight over the assets.
pretty_table(DataFrame(; :assets => rd.nx, :MinVar => res_bench.w, :NCO => res_nco.w, :Stacking => res_stk.w, :SubsetResampling => res_ssr.w); formatters = [resfmt])┌────────┬──────────┬──────────┬──────────┬──────────────────┐
│ assets │ MinVar │ NCO │ Stacking │ SubsetResampling │
│ String │ Float64 │ Float64 │ Float64 │ Float64 │
├────────┼──────────┼──────────┼──────────┼──────────────────┤
│ AAPL │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ AMD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BAC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BBY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ CVX │ 7.432 % │ 10.376 % │ 7.432 % │ 6.786 % │
│ GE │ 0.806 % │ 0.0 % │ 0.806 % │ 0.856 % │
│ HD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.677 % │
│ JNJ │ 36.974 % │ 31.364 % │ 36.974 % │ 20.456 % │
│ JPM │ 0.749 % │ 0.0 % │ 0.749 % │ 1.915 % │
│ KO │ 11.161 % │ 10.028 % │ 11.161 % │ 13.75 % │
│ LLY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.735 % │
│ MRK │ 17.467 % │ 15.876 % │ 17.467 % │ 19.192 % │
│ MSFT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PEP │ 8.978 % │ 10.115 % │ 8.978 % │ 10.81 % │
│ PFE │ 0.0 % │ 0.0 % │ 0.0 % │ 2.41 % │
│ PG │ 2.353 % │ 10.609 % │ 2.353 % │ 7.461 % │
│ RRC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ UNH │ 0.0 % │ 3.483 % │ 0.0 % │ 1.144 % │
│ WMT │ 9.355 % │ 4.863 % │ 9.355 % │ 7.507 % │
│ XOM │ 4.725 % │ 3.285 % │ 4.725 % │ 6.301 % │
└────────┴──────────┴──────────┴──────────┴──────────────────┘6. Visualising the compositions
The plot stacks the same four allocations, with the plain minimum-variance fit first.
using StatsPlots, GraphRecipesplot_stacked_bar_composition([res_bench, res_nco, res_stk, res_ssr], rd)This page was generated using Literate.jl.