The source files can be found in user_guide/.

Optimisers

This is the breadth tour of the optimiser families. Every optimiser shares the same call — optimise(estimator) (or optimise(estimator, rd) for the naive and meta ones) — and returns a result whose w field holds the asset weights. The point of this page is to show the shape of each family with one minimal call; for objectives, risk measures, variants, and trade-offs, follow the cross-links into the optimiser examples.

We fix one empirical prior and reuse it everywhere so the families are comparable.

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
      Z ┴ nothing

Every JuMP optimiser below shares this one solver. Its check_sol field is splatted into JuMP's assert_is_solved_and_feasible after each solve, and decides which solver statuses count as a solved model. The default (;) is strict on purpose — it accepts only OPTIMAL or LOCALLY_SOLVED at a FEASIBLE_POINT, so a solution the solver itself flags as approximate is rejected rather than silently used. Passing allow_almost = true widens it to the ALMOST_* statuses, which is what we want here: a first-order conic solver that reaches its tolerance on a well-posed portfolio problem gives a usable answer, and rejecting it would only make the optimiser fall through to the next solver. Tighten it the other way with allow_local = false when nothing short of a certified global optimum will do.

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

Naive optimisers use simple, solver-free rules that buy robustness through unsophistication. InverseVolatility weights by the reciprocal of each asset's volatility; EqualWeighted splits capital evenly; RandomWeighted samples a Dirichlet allocation. They take the ReturnsResult directly.

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
          │     ts ┼ 252-element Vector{Date}
          │     iv ┼ nothing
          │   ivpa ┼ nothing
          │     nz ┼ nothing
          │      Z ┴ nothing
       wb ┼ WeightBounds
          │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
          │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
  retcode ┼ OptimisationSuccess
          │   res ┴ nothing
        w ┼ 20-element Vector{Float64}
       fb ┴ nothing

2. JuMP optimisers — MeanRisk

JuMP optimisers solve a mathematical program and are the most flexible on constraints, objectives, and risk measures. They need a JuMPOptimiser carrying the prior and a Solver (we recommend Clarabel for non-MIP problems). The workhorse is MeanRisk; 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
     │           │           │       Z ┴ 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}
     │           │       sca ┴ SumScalariser()
     │   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, :bgt, :cdev_soc_1, :dev_1, :k, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :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()
  fb ┴ nothing

MeanRisk also offers MaximumUtility, MaximumRatio and MaximumReturn objectives and efficient frontiers — see MeanRisk Objectives and Efficient Frontier.

The risk measure is the r field (of MeanRisk and of the clustering optimisers below); the default is Variance. Which one you pick encodes what kind of risk you penalise — overall dispersion (Variance), the left tail (ConditionalValueatRisk), peak-to-trough paths (MaximumDrawdown), or the whole ordered loss curve (OrderedWeightsArray). The full menu — every measure with its alias, its meaning, and which optimisers accept it — is the risk measures page; you can also mix several in one objective (Multiple Risk Measures).

The return side is the ret field of JuMPOptimiser, an ArithmeticReturn by default. Like r, it takes one term or a vector of them, summed with weights into the model's single return expression. Each term carries its own JuMPReturnsSettings — its weight in that sum, its own lower bound, and whether it enters the sum at all — so a term can bound the portfolio without being rewarded, which is how you price what a floor on one quantity costs in another (ℓ1 uncertainty sets).

The drawdown notion is also useful purely as a post-optimisation diagnostic — via drawdowns on a realised book — when you want to measure rather than optimise it (Performance Attribution).

The other JuMP families follow the same opt = JuMPOptimiser(...) pattern:

Here is the minimal risk-budgeting call (equal risk contribution by default):

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
      │           │           │       Z ┴ 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}
      │           │       sca ┴ SumScalariser()
      │   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, :bgt, :cdev_soc_1, :clog_w, :crkb, :dev_1, :k, :log_w, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_vec, :sc, :so, :unit_budget, :variance_flag, :variance_risk_1, :w, :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()
  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

Compatibility is a property of the optimiser family, not the individual optimiser: every JuMP optimiser accepts the same RiskMeasures, and clustering optimisers additionally accept the hierarchical-only measures. You can ask programmatically with supports_risk_measure / supported_risk_measures:

supports_risk_measure(MeanRisk, ConditionalValueatRisk)   # truesupported_risk_measures(HierarchicalRiskParity)           # OptimisationRiskMeasure

Meta-optimisers (NestedClustered, Stacking, SubsetResampling) are the exception: their acceptance is instance-specific because they delegate, accepting a measure only when every constituent optimiser does (the intersection of their children's categories).

The risk measures page tabulates every measure against these classes — the tables there are generated from the same predicate, so they cannot drift from what the optimisers actually dispatch on.

3. Clustering optimisers

Clustering optimisers build the allocation from the asset correlation hierarchy instead of a single program. They take a HierarchicalOptimiser carrying the prior and a clustering estimate. HierarchicalRiskParity (HRP) is the canonical one; HierarchicalEqualRiskContribution and SchurComplementHierarchicalRiskParity are its siblings — 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
      │           │       Z ┴ nothing
      │       clr ┼ Clusters
      │           │   res ┼ Clustering.Hclust{Float64}([-1 -13; -2 1; … ; 15 16; 18 17], [0.2992796916890263, 0.39056120313611004, 0.4116454947407609, 0.22529867864314176, 0.48924055164900887, 0.28518628966607656, 0.3800863165339249, 0.5842580180890099, 0.4209146698909511, 0.4605479608276049, 0.491265779899127, 0.5080236207888611, 0.6028393326616824, 0.24670816017739164, 0.5174631833197462, 0.6821605083740293, 0.7399456049755042, 0.9602133336085281, 1.07073891752418], [5, 20, 17, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 8, 12, 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}
    r ┼ Variance
      │   settings ┼ RiskMeasureSettings
      │            │   scale ┼ Float64: 1.0
      │            │      ub ┼ nothing
      │            │     rke ┴ Bool: true
      │      sigma ┼ 20×20 Matrix{Float64}
      │       chol ┼ nothing
      │         rc ┼ nothing
      │        alg ┴ SquaredSOCRiskExpr()
  sca ┼ SumScalariser()
   fb ┴ nothing

3.1 Clustering on something other than the returns

clusterise(ClustersEstimator(), pr.X) derives its distance from the correlation, so the hierarchy can only ever see structure the price history contains. Swapping the estimator's distance slot for a FeatureDistance clusters an assets × features matrix instead — a sector or country classification, a factor loading profile, any per-asset quantity you can name. asset_sets_features builds one from a UniverseSets taxonomy, and it travels beside the returns as data rather than on the estimator.

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,                     nz = asset_sets_feature_names(vals_z, sets_z),                     Z = asset_sets_features(vals_z, sets_z))res_hrp_z = optimise(HierarchicalRiskParity(;                                            opt = HierarchicalOptimiser(; pe = pr,                                                                        cle = ClustersEstimator(;                                                                                                de = FeatureDistance()),                                                                        z_src = :data),                                            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
      │           │       Z ┴ nothing
      │       clr ┼ Clusters
      │           │   res ┼ Clustering.Hclust{Float64}([-1 -2; 1 -13; … ; 5 15; 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, 20, 5, 6, 9, 16, 14, 10, 15, 8, 11, 12, 4, 7, 3, 18, 17, 19], :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}
    r ┼ Variance
      │   settings ┼ RiskMeasureSettings
      │            │   scale ┼ Float64: 1.0
      │            │      ub ┼ nothing
      │            │     rke ┴ Bool: true
      │      sigma ┼ 20×20 Matrix{Float64}
      │       chol ┼ nothing
      │         rc ┼ nothing
      │        alg ┴ SquaredSOCRiskExpr()
  sca ┼ SumScalariser()
   fb ┴ nothing

z_src picks which of the two carriers supplies the matrix: :data reads the one you supplied on the ReturnsResult, :prior reads one a producer derived onto the prior result. It defaults to :data — the opposite of x_src — because an explicitly supplied matrix outranks a derived one.

Two consequences only appear once cross-validation is switched on, and neither can be inferred from the API:

  • :data slices, :prior refits. Inside a fold or a meta-optimiser's subproblem, a carried matrix is subselected while a derived one is recomputed on the subproblem's own returns. For a fixed classification the two coincide; for a returns-derived producer they are two different questions, so the selector chooses between two semantics rather than two copies.
  • A time-varying literal matrix cannot survive an observation fold. A three-dimensional observations × assets × features matrix handed straight to FeaturePrior has no way to be resliced down its observation axis, so the fit throws a DimensionMismatch as soon as the observation count changes. Features that must vary with time and survive folds have to come from a producer, which refits on whatever rows the fold hands it.

See Feature Matrices as a Distance Source for the four producers, the time-varying shapes, and a walk-forward comparison.

4. Meta-optimisers

Meta-optimisers compose other optimisers. NestedClustered (NCO) runs an inner optimiser within each cluster and an outer optimiser across the cluster representatives; Stacking and SubsetResampling blend several fits — see Meta Optimisers. The inner optimiser carries the prior; the outer one does not.

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
          │       Z ┴ nothing
      clr ┼ Clusters
          │   res ┼ Clustering.Hclust{Float64}([-1 -13; -2 1; … ; 15 16; 18 17], [0.2992796916890263, 0.39056120313611004, 0.4116454947407609, 0.22529867864314176, 0.48924055164900887, 0.28518628966607656, 0.3800863165339249, 0.5842580180890099, 0.4209146698909511, 0.4605479608276049, 0.491265779899127, 0.5080236207888611, 0.6028393326616824, 0.24670816017739164, 0.5174631833197462, 0.6821605083740293, 0.7399456049755042, 0.9602133336085281, 1.07073891752418], [5, 20, 17, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 8, 12, 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.00036354628076020715, 0.000652797420571548]
          │      │           │           │   sigma ┼ 2×2 Matrix{Float64}
          │      │           │           │    chol ┼ nothing
          │      │           │           │       w ┼ nothing
          │      │           │           │     ens ┼ nothing
          │      │           │           │     kld ┼ nothing
          │      │           │           │      ow ┼ nothing
          │      │           │           │      rr ┼ nothing
          │      │           │           │     fpr ┼ nothing
          │      │           │           │       Z ┴ 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.00036354628076020715, 0.000652797420571548]
          │      │           │       sca ┴ SumScalariser()
          │      │   retcode ┼ OptimisationSuccess
          │      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
          │      │       sol ┼ JuMPOptimisationSolution
          │      │           │   w ┴ Vector{Float64}: [0.13998673390958633, 0.8600132660904137]
          │      │     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, :bgt, :cdev_soc_1, :dev_1, :k, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :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()
          │   fb ┴ nothing
       cv ┼ nothing
  retcode ┼ OptimisationSuccess
          │   res ┴ nothing
        w ┼ 20-element Vector{Float64}
       fb ┴ nothing

5. Comparing the families

One prior, six optimisers, six allocations. The naive rules, risk budgeting, and the clustering hierarchy spread weight broadly (max weight in single digits to low teens); MeanRisk(MinimumRisk) and NCO concentrate into a few low-variance names (max weight ≈ a third). Same data, very different portfolios — which is the point of having a menu.

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.