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.
  • Stacking runs several inner optimisers over every asset, then combines the portfolios they give with one outer optimiser.
  • SubsetResampling optimises 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.

When to reach for this

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

This page was generated using Literate.jl.