The source files can be found in user_guide/.

Optimisers

This page makes one call from each family of optimisers. Every optimiser takes the same call, optimise(estimator), or optimise(estimator, rd) for the naive optimisers and the meta-optimisers. The result holds the asset weights in its field w. For the objectives, the risk measures and the variants of each family, follow the links into the optimiser examples.

We compute one empirical prior and give it to every JuMP, clustering and meta-optimiser. The naive optimisers take the returns directly.

using PortfolioOptimisers, CSV, TimeSeries, DataFrames, PrettyTables, Clarabel, StatsPlots,      GraphRecipesresfmt = (v, i, j) -> begin    return if j == 1        v    else        isa(v, AbstractFloat) ? "$(round(v*100, digits=3)) %" : v    endend;X = TimeArray(CSV.File(joinpath(@__DIR__, "../examples/SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)pr = prior(EmpiricalPrior(), rd)
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

Every JuMP optimiser below uses this solver. After each solve, the optimiser passes the solver's check_sol field to JuMP's assert_is_solved_and_feasible, which decides which solver statuses count as a solution. The default, (;), accepts OPTIMAL or LOCALLY_SOLVED at a FEASIBLE_POINT. If the solver marks its solution as approximate, the check fails, and the optimiser tries the next solver in its list.

allow_almost = true also accepts the ALMOST_* statuses. We set it, because a conic solver that stops at its tolerance on a well-posed portfolio problem still returns usable weights. allow_local = true is JuMP's default, so it changes nothing here. To accept only OPTIMAL, set allow_local = false.

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

1. Naive optimisers

A naive optimiser uses a fixed rule and no solver. InverseVolatility weights each asset by the reciprocal of its volatility. EqualWeighted gives each asset the same weight. RandomWeighted draws the weights from a Dirichlet distribution. They take the ReturnsResult directly. OnlinePortfolioSelection is also a naive optimiser. It moves its allocation after each period by a rule of its own, and it has its own page, Online portfolio selection.

res_iv = optimise(InverseVolatility(), rd)res_ew = optimise(EqualWeighted(), rd)
NaiveOptimisationResult
       pr ┼ ReturnsResult
          │     nx ┼ 20-element Vector{String}
          │      X ┼ 252×20 Matrix{Float64}
          │     nf ┼ nothing
          │      F ┼ nothing
          │     nb ┼ nothing
          │      B ┼ nothing
          │     ne ┼ nothing
          │      E ┼ nothing
          │     ts ┼ 252-element Vector{Date}
          │     iv ┼ nothing
          │   ivpa ┼ nothing
          │    pnl ┼ AssetPanel
          │        │     pf ┼ Vector{AbstractPanelField}: AbstractPanelField[]
          │        │   amsk ┼ 252×20 AllTrueMask
          │        │   emsk ┴ 252×20 AllTrueMask
       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}
     fees ┼ nothing
  retcode ┼ OptimisationSuccess
          │   res ┴ nothing
        w ┼ 20-element Vector{Float64}
     imsk ┼ nothing
       fb ┴ nothing

2. JuMP optimisers and MeanRisk

A JuMP optimiser solves a mathematical program, and it accepts more constraints and objectives than the other families. It needs a JuMPOptimiser that holds the prior and a Solver. Clarabel suits a problem with no integer variables. The most used JuMP optimiser is MeanRisk, and its default objective is MinimumRisk.

res_mr = 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

MeanRisk also takes the objectives MaximumUtility, MaximumRatio and MaximumReturn, and it computes efficient frontiers. See MeanRisk Objectives and Efficient Frontier.

The risk measure is the r field of MeanRisk and of the clustering optimisers below, and its default is Variance. The measure sets the kind of risk that the optimiser penalises. Variance penalises the spread of the returns, ConditionalValueatRisk the left tail, MaximumDrawdown the largest fall from a peak, and OrderedWeightsArray a weighted sum of the sorted returns. Risk measures lists every measure with its alias, what it penalises and the optimisers that accept it. You can also put several measures in one objective. See Multiple Risk Measures.

The return is the ret field of JuMPOptimiser, an ArithmeticReturn by default. Like r, it takes one term or a vector of terms. The optimiser multiplies each term by its scale and adds the terms into one return expression. Each term has its own JuMPReturnsSettings, which set its scale, its lower bound, and whether it enters the sum. So a term can bound the portfolio's return and add nothing to the objective. With this you can measure how much of one return you give up to keep another above a floor (ℓ1 uncertainty sets).

To measure the drawdowns of a portfolio without optimising them, call drawdowns on its returns after the optimisation. See Performance Attribution.

The other JuMP optimisers take the same opt = JuMPOptimiser(...) keyword:

We run risk budgeting with its default budget, which gives every asset the same share of the risk.

res_rb = optimise(RiskBudgeting(; opt = JuMPOptimiser(; pe = pr, slv = slv)))
RiskBudgetingResult
   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: 42
      │           │ ├ num_constraints: 25
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ AffExpr in MOI.GreaterThan{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 1
      │           │ │ └ Vector{AffExpr} in MOI.ExponentialCone: 20
      │           │ └ Names registered in the model
      │           │   └ :G, :T, :bgt, :cdev_soc_1, :clog_w, :crkb, :dev_1, :k, :log_w, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :unit_budget, :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
  prb ┼ ProcessedAssetRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
   fb ┴ nothing

Which risk measures each optimiser family accepts

The family of an optimiser sets the risk measures that it accepts. Every JuMP optimiser with an r field accepts the same RiskMeasures. RelaxedRiskBudgeting has no r field. The clustering optimisers accept those measures and also the measures that only a clustering optimiser can use. You can ask with supports_risk_measure and supported_risk_measures:

supports_risk_measure(MeanRisk, ConditionalValueatRisk)supported_risk_measures(HierarchicalRiskParity)

The first call returns true, and the second returns OptimisationRiskMeasure. For a meta-optimiser, NestedClustered, Stacking or SubsetResampling, the answer depends on the optimisers that it holds. The risk measures page shows every measure against these classes.

3. Clustering optimisers

A clustering optimiser builds the weights from a hierarchy of the assets and not from one program. It takes a HierarchicalOptimiser that holds the prior and a clustering of the assets. HierarchicalRiskParity (HRP) is the best known. HierarchicalEqualRiskContribution and SchurComplementHierarchicalRiskParity are in the same family. See Clustering Optimisers.

clr = clusterise(ClustersEstimator(), pr.X)hopt = HierarchicalOptimiser(; pe = pr, cle = clr)res_hrp = optimise(HierarchicalRiskParity(; opt = hopt, r = Variance()))
HierarchicalRiskParityResult
   hr ┼ HierarchicalResult
      │        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; -2 1; … ; 15 16; 18 17], [0.2992796916890278, 0.3905612031361093, 0.41164549474076095, 0.22529867864314262, 0.4892405516490092, 0.28518628966607745, 0.38008631653392605, 0.5842580180890103, 0.420914669890951, 0.4605479608276053, 0.491265779899127, 0.5080236207888622, 0.602839332661682, 0.24670816017739208, 0.5174631833197463, 0.6821605083740295, 0.7399456049755035, 0.9602133336085283, 1.0707389175241806], [17, 20, 5, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 12, 8, 15], :ward)
      │           │     S ┼ 20×20 Matrix{Float64}
      │           │     D ┼ 20×20 Matrix{Float64}
      │           │     P ┼ nothing
      │           │     k ┴ Int64: 2
      │        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}
      │      fees ┼ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ nothing
      │         w ┼ 20-element Vector{Float64}
      │      imsk ┴ nothing
    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
  sca ┼ SumScalariser()
   fb ┴ nothing

3.1 Clustering on something other than the returns

clusterise(ClustersEstimator(), pr.X) computes its distance from the correlation of the returns, so the hierarchy shows only the structure that the returns contain. A FeatureDistance in the estimator's de field clusters the assets on a matrix of features instead. A feature can be a sector or a country, a profile of factor loadings, or any other quantity you have for each asset. That matrix comes from an AssetPanel. panel_input turns keys of a UniverseSets into the fields of the panel, and asset_panel builds the panel. The panel goes on the returns, next to X, and not on the estimator.

We label each asset with its sector and with where most of its revenue comes from, and we cluster the assets on those two labels.

sector = Dict("AAPL" => "Tech", "AMD" => "Tech", "MSFT" => "Tech", "BAC" => "Financials",              "JPM" => "Financials", "CVX" => "Energy", "XOM" => "Energy",              "RRC" => "Energy", "GE" => "Industrials", "BBY" => "Discretionary",              "HD" => "Discretionary", "KO" => "Staples", "PEP" => "Staples",              "PG" => "Staples", "WMT" => "Staples", "JNJ" => "Health", "LLY" => "Health",              "MRK" => "Health", "PFE" => "Health", "UNH" => "Health")revenue = Dict("AAPL" => "Global", "AMD" => "Global", "MSFT" => "Global",               "BAC" => "Domestic", "JPM" => "Global", "CVX" => "Global", "XOM" => "Global",               "RRC" => "Domestic", "GE" => "Global", "BBY" => "Domestic",               "HD" => "Domestic", "KO" => "Global", "PEP" => "Global", "PG" => "Global",               "WMT" => "Domestic", "JNJ" => "Global", "LLY" => "Global", "MRK" => "Global",               "PFE" => "Global", "UNH" => "Domestic")sets_z = UniverseSets(; xkey = "nx",                      dict = Dict("nx" => rd.nx, "nx_sector" => [sector[a] for a in rd.nx],                                  "nx_revenue" => [revenue[a] for a in rd.nx]))vals_z = ["nx_sector", "nx_revenue"]rd_z = ReturnsResult(; nx = rd.nx, X = rd.X, ts = rd.ts,                     pnl = asset_panel(panel_input(sets_z, vals_z)))res_hrp_z = optimise(HierarchicalRiskParity(;                                            opt = HierarchicalOptimiser(; pe = pr,                                                                        cle = ClustersEstimator(;                                                                                                de = FeatureDistance())),                                            r = Variance()), rd_z)
HierarchicalRiskParityResult
   hr ┼ HierarchicalResult
      │        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 -2; 1 -13; … ; 15 5; 17 18], [6.707879276254074e-9, 6.707879276254074e-9, 6.707879276254074e-9, 6.707879276254074e-9, 6.707879276254074e-9, 0.33333333333333337, 6.707879276254074e-9, 0.33333333333333337, 6.707879276254074e-9, 6.707879276254074e-9, 6.707879276254074e-9, 0.33333333333333337, 0.408248290463863, 0.511766315719159, 0.33333333333333337, 0.5520524474738834, 0.6274159654095468, 0.43033148291193524, 1.1075737342134728], [1, 2, 13, 6, 9, 5, 20, 10, 14, 16, 12, 8, 11, 15, 18, 3, 17, 19, 7, 4], :ward)
      │           │     S ┼ 20×20 Matrix{Float64}
      │           │     D ┼ 20×20 Matrix{Float64}
      │           │     P ┼ nothing
      │           │     k ┴ Int64: 3
      │        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}
      │      fees ┼ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ nothing
      │         w ┼ 20-element Vector{Float64}
      │      imsk ┴ nothing
    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
  sca ┼ SumScalariser()
   fb ┴ nothing

The ape field of FeatureDistance sets where the panel comes from. With nothing, it reads the panel on the ReturnsResult. With RegressionPanel or PhylogenyPanel, it builds a panel when it runs, from the prior and the returns that it receives.

The two choices give different panels only when the optimiser runs on part of the data, inside a cross-validation fold or inside a subproblem of a meta-optimiser. There the optimiser takes the part of a stored panel that the fold uses. A panel estimator instead fits a new panel on the returns of the fold. For a fixed label, such as a sector, the two give the same panel. For a panel estimated from the returns, they give different panels. A panel estimator is also the only way to use a feature that changes with time on every fold, because it fits the feature on the rows of the fold.

See Feature Matrices as a Distance Source for the panel estimators, the features that change with time, and a comparison over a walk-forward.

4. Meta-optimisers

A meta-optimiser combines other optimisers. NestedClustered (NCO) runs an inner optimiser inside each cluster, and an outer optimiser across the portfolios of the clusters. Stacking runs several inner optimisers and combines their weights with an outer optimiser. SubsetResampling runs one optimiser on random subsets of the assets and combines the weights. See Meta Optimisers.

In the call below, the inner optimiser holds the computed prior pr, and the outer one must not. The outer problem has one asset for each cluster, so the outer optimiser computes its own prior.

res_nco = optimise(NestedClustered(; pe = pr, cle = clr,                                   opti = MeanRisk(; obj = MinimumRisk(),                                                   opt = JuMPOptimiser(; pe = pr,                                                                       slv = slv)),                                   opto = MeanRisk(; obj = MinimumRisk(),                                                   opt = JuMPOptimiser(; slv = slv))), 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; -2 1; … ; 15 16; 18 17], [0.2992796916890278, 0.3905612031361093, 0.41164549474076095, 0.22529867864314262, 0.4892405516490092, 0.28518628966607745, 0.38008631653392605, 0.5842580180890103, 0.420914669890951, 0.4605479608276053, 0.491265779899127, 0.5080236207888622, 0.602839332661682, 0.24670816017739208, 0.5174631833197463, 0.6821605083740295, 0.7399456049755035, 0.9602133336085283, 1.0707389175241806], [17, 20, 5, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 12, 8, 15], :ward)
          │     S ┼ 20×20 Matrix{Float64}
          │     D ┼ 20×20 Matrix{Float64}
          │     P ┼ nothing
          │     k ┴ Int64: 2
       wb ┼ WeightBounds
          │   lb ┼ 20-element Vector{Float64}
          │   ub ┴ 20-element Vector{Float64}
     fees ┼ nothing
     resi ┼ 2-element Vector{MeanRiskResult}
          │ MeanRiskResult ⋯
          │ MeanRiskResult ⋯
     reso ┼ MeanRiskResult
          │   jr ┼ JuMPOptimisationResult
          │      │        pa ┼ ProcessedJuMPOptimiserAttributes
          │      │           │        pr ┼ LowOrderPrior
          │      │           │           │         X ┼ 252×2 Matrix{Float64}
          │      │           │           │       o_X ┼ nothing
          │      │           │           │        mu ┼ Vector{Float64}: [0.00036354628076021224, 0.000652797420571905]
          │      │           │           │     sigma ┼ 2×2 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, 2)
          │      │           │           │   ub ┴ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(1.0, 0.0, 2)
          │      │           │        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.00036354628076021224, 0.000652797420571905]
          │      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
          │      │           │       sca ┼ SumScalariser()
          │      │           │      imsk ┴ nothing
          │      │   retcode ┼ OptimisationSuccess
          │      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
          │      │       sol ┼ JuMPOptimisationSolution
          │      │           │   w ┴ Vector{Float64}: [0.13998673390957875, 0.8600132660904212]
          │      │     model ┼ A JuMP Model
          │      │           │ ├ solver: Clarabel
          │      │           │ ├ objective_sense: MIN_SENSE
          │      │           │ │ └ objective_function_type: QuadExpr
          │      │           │ ├ num_variables: 3
          │      │           │ ├ 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 ┼ 2×2 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

5. Comparing the families

We print the weights of the six optimisers in one table and plot them. The naive rules, risk budgeting and HRP give some weight to every asset. MeanRisk with MinimumRisk and NCO put most of the weight in a few assets with a low variance. The six optimisers use the same returns, so the differences come from the optimisers alone.

results = [res_iv, res_ew, res_mr, res_rb, res_hrp, res_nco]labels = ["InvVol", "EqualW", "MinRisk", "RiskBudget", "HRP", "NCO"]pretty_table(DataFrame(["Asset" => rd.nx,                        [labels[i] => results[i].w for i in eachindex(results)]...]);             formatters = [resfmt], title = "Weights by optimiser family")plot_stacked_bar_composition(results, rd; xticks = (1:length(labels), labels))
Example block output

This page was generated using Literate.jl.