Nested Clustered

PortfolioOptimisers.NestedClusteredResultType
struct NestedClusteredResult{__T_pr, __T_clr, __T_wb, __T_fees, __T_resi, __T_reso, __T_cv, __T_retcode, __T_w, __T_imsk, __T_fb} <: NonJuMPOptimisationResult

Result type for NestedClustered.

clr holds the clustering the algorithm found, and resi holds one intra-cluster optimisation per cluster, in cluster order. reso is the inter-cluster optimisation over the synthetic universe those clusters define, so reso.w has one entry per cluster rather than one per asset. w is the product of the two.

Fields

  • pr: Prior result.
  • clr: Clusters result.
  • wb: Weight bounds.
  • fees: Fees estimator or result.
  • resi: Inner optimisation results.
  • reso: Outer optimisation results.
  • cv: Cross-validation estimator.
  • retcode: Optimisation return code.
  • w: Final aggregated portfolio weights.
  • imsk: The Investable Mask the optimisation reduced on: true at every asset whose prior moments were finite. It is nothing when every asset was investable, and that sentinel is what skips both the reduction and the expansion. investable_mask derives it once from the full-universe prior result, and the result carries it, because the reduced prior can no longer yield it.
  • fb: The fallback chain that answered this result: the (estimator, result) pair of every attempt optimise made before this one, in the order they ran, or nothing when the estimator it was asked of answered (see FbChain).

Constructors

NestedClusteredResult(;    pr::Option{<:AbstractPriorResult},    clr::Option{<:AbstractClusteringResult},    wb::Option{<:WeightBounds},    fees::Option{<:Fees},    resi::AbstractVector{<:NonFiniteAllocationOptimisationResult},    reso::OptimisationResult,    cv::Option{<:OptimisationCrossValidation},    retcode::OptRetCode_VecOptRetCode,    w::VecNum_VecVecNum,    imsk::Option{<:BitVector} = nothing,    fb::Option{<:OptE_Opt_FbChain}) -> NestedClusteredResult

Keywords correspond to the struct's fields.

The keyword constructor is the one door _optimise exits through, so it is where the aggregated weights expand back onto the full asset universe, through expand_investable_weights. The positional constructor never expands: set_retcode rebuilds through it, and a second pass would expand twice. pr, clr, wb, fees and every member of resi are the objects of the reduced universe, because that is what the algorithm ran on.

Related

References

  • [142] M. López de Prado. A robust estimator of the efficient frontier. SSRN Electronic Journal (2019).
  • [9] M. M. De Prado. Machine learning for asset managers (Cambridge University Press, 2020). Chapter 7.
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.3.
source
PortfolioOptimisers.NestedClusteredType
struct NestedClustered{__T_pe, __T_cle, __T_wb, __T_fees, __T_sets, __T_opti, __T_opto, __T_cv, __T_wf, __T_ex, __T_fb, __T_brt, __T_x_src, __T_strict, __T_cache} <: ClusteringOptimisationEstimator

Nested Clustered Optimisation (NCO) portfolio optimiser.

NestedClustered implements the Nested Clustered Optimisation algorithm. It first clusters assets, then solves a within-cluster (inner) optimisation for each cluster independently, and finally solves an across-cluster (outer) optimisation to combine the cluster portfolios into a final portfolio.

Fields

  • pe: Prior estimator.
  • cle: Clusters estimator.
  • wb: Weight bounds estimator or weight bounds.
  • fees: Fees estimator.
  • sets: Sets used to map estimator values to assets.
  • opti: Inner optimiser.
  • opto: Outer optimiser.
  • cv: Cross-validation estimator.
  • wf: Weight finaliser.
  • ex: Parallel execution strategy.
  • fb: Fallback result or estimator.
  • brt: Whether to use bootstrap returns.
  • x_src: Which returns matrix the clustering, phylogeny and centrality estimators read: :prior takes the prior result's X, :data takes the raw returns result's X. Ignored when no returns result is available, in which case the prior result's X is used.
  • strict: Whether to strictly enforce weight bounds.
  • cache: Optional ReturnsBufferState, the fold context of the online step. It is nothing until partial_fit! writes one, and optimise(opt) with no returns reads it. The returns themselves are carried by the prior, which owns the rows once; this holds every other column of the carrier and the context pinned at the first step. factory carries it unchanged and port_opt_view slices it to the selected assets.

Constructors

NestedClustered(;    pe::Onl{<:TD{<:PrE_Pr}} = EmpiricalPrior(),    cle::TD{<:ClE_Cl} = ClustersEstimator(),    wb::TD_Option{<:WbE_Wb} = nothing,    fees::TD_Option{<:FeesE_Fees} = nothing,    sets::TD_Option{<:UniverseSets} = nothing,    opti::OptE_TD,    opto::OptE_TD,    cv::Option{<:OptimisationCrossValidation} = nothing,    wf::TD{<:WeightFinaliser} = IterativeWeightFinaliser(),    ex::FLoops.Transducers.Executor = FLoops.ThreadedEx(),    fb::TDO_Option{<:OptE_Opt} = nothing,    brt::Bool = false,    x_src::Symbol = :prior,    strict::Bool = false,    cache::Option{<:ReturnsBufferState} = nothing) -> NestedClustered

Keywords correspond to the struct's fields.

Time-dependent fields

pe, cle, wb, fees, sets, wf, opti, opto and fb may hold a TimeDependent per-fold schedule. opto and fb are bind = :outermost only — no inner fold loop consumes them. opti additionally admits bind = :nearest: the inner cross-validation is entered per cluster (cross_val_predict(opti, …; cols = cl)), so the field itself is the inner fold loop's entry point and a :nearest schedule is consumed there, per cluster. A :nearest opti schedule requires an explicit default and cv !== nothing at construction (see assert_nearest_optimiser_schedule) because the per-cluster optimise leg always resolves it fold-lessly to its default. cv itself stays static: it is the inner fold loop, not part of the per-fold problem definition, and the :nearest construction checks must be able to inspect it.

Schedule entries for opti/opto must be estimators, like the static fields: a vector schedule holding a precomputed result is rejected at construction by the entry substitution pass.

Validation

  • x_src in (:prior, :data).
  • opto must pass assert_external_optimiser and assert_special_nco_requirements (schedules delegate to their entries and default).
  • If opti !== opto: opti must pass assert_internal_optimiser and assert_special_nco_requirements.
  • If cv is provided: opti must also pass assert_external_optimiser and assert_special_nco_requirements.
  • opto and fb schedules: bind !== :nearest. A bind = :nearest opti schedule: explicit default and cv !== nothing.

Mathematical definition

Let clusters $C_1, \ldots, C_K$ partition the $N$ assets. The NCO algorithm:

  1. Inner: for each cluster $k$, solve $\boldsymbol{w}_{C_k} = \mathrm{opti}(\mathbf{X}_{C_k})$ (sub-portfolio weights within $C_k$).
  2. Outer: form a $T \times K$ synthetic returns matrix from cluster portfolios and solve $\boldsymbol{a} = \mathrm{opto}(\mathbf{X}_{\mathrm{cluster}})$ (allocation across clusters).
  3. Combine: $w_i = a_k \cdot w_{C_k, i}$ for $i \in C_k$.

Step 1 is the book's intra-cluster asset allocation and step 2 its inter-cluster asset allocation. The combined weights then pass through wf and wb (see finalise_weight_bounds), which is what the result's w and retcode carry.

Propagated parameters

When factory is called on this type, the following @fprop-tagged fields are automatically propagated:

  • fees: Recursively updated via factory.
  • opti: Recursively updated via factory.
  • opto: Recursively updated via factory.
  • fb: Recursively updated via factory.

View parameters

NestedClustered defines its own port_opt_view method rather than deriving one from field tags.

  • The method reads the returns matrix X as its third argument. When pe already holds a prior result, the method replaces X with pe.X, so the children are viewed against the prior's own observations rather than the caller's matrix.
  • pe, wb, fees and sets recurse through port_opt_view with the index alone.
  • opti and opto recurse with that matrix.
  • cle and the remaining fields are carried through unchanged.

Related

References

  • [142] M. López de Prado. A robust estimator of the efficient frontier. SSRN Electronic Journal (2019).
  • [9] M. M. De Prado. Machine learning for asset managers (Cambridge University Press, 2020). Chapter 7.
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.3.
source
PortfolioOptimisers.factoryMethod
factory(a::Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                 <:AbstractResult}, args...; kwargs...) -> a
factory(a::AbstractVector{<:Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                                  <:AbstractResult}}, args...; kwargs...) -> Vector

No-op factory function for constructing objects with a uniform interface.

Defining methods which dispatch on the first argument allows for a consistent factory interface across different types.

factory and port_opt_view are the two propagation mechanisms in this library. They are duals: factory threads runtime values (prior moments, observation weights, previous portfolio weights) down through a composed struct tree; port_opt_view threads an index selection (a subset of assets or observations) down through the same tree.

The vector method is the one forwarding contract for every vector-valued propagation field: it applies factory to each element and forwards args... and kwargs... unchanged, so a family that admits a vector of estimators, algorithms, or results needs no method of its own. A family that needs more than the forward, such as a concrete element type (concrete_typed_array_if_abstract), defines its own more specific method.

Algorithm

The scalar method:

  1. Return a unchanged, and drop args... and kwargs.... This method is the leaf of the recursion, and it is what makes an untagged type safe to call the verb on.

The vector method:

  1. For each element ai of a, call factory on ai, and forward args... and kwargs... unchanged.
  2. Collect the results into a new vector, in the order of a, and return it.

A @propagatable struct with at least one @fprop- or @wprop-tagged field carries a generated method that dominates the scalar method. That method rebuilds the struct with its keyword constructor, sending each @fprop field through factory_child and each @wprop field through _wprop.

Arguments

  • a: Indicates no object should be constructed, or a vector whose elements are rebuilt one by one.
  • args...: Arbitrary positional arguments (ignored by the scalar method, forwarded by the vector method).
  • kwargs...: Arbitrary keyword arguments (ignored by the scalar method, forwarded by the vector method).

Returns

  • a: The input unchanged.
  • v::Vector: The element-wise rebuilds, for the vector method.

Examples

julia> factory(nothing, 1, 2; x = 3)julia> factory(MeanValue())MeanValue  w ┴ nothing

Related

source
factory(res::NonFiniteAllocationOptimisationResult, fb::Option{<:OptE_Opt_FbChain})

Rebuild a continuous optimisation result with an updated fallback record fb.

Every optimisation result carries fb as its last field, so the generic rebuild copies all fields unchanged except the trailing fb. Concrete result types may override this method when rebuilding requires more than swapping fb. optimise is the one caller, and it hands in the FbChain it walked.

Related

source
factory(
    opt::Union{NonFiniteAllocationOptimisationEstimator, NonFiniteAllocationOptimisationResult},
    _
) -> RandomWeighted{_A, var"#s185", _B, _C, _D, _E, _F, Bool} where {_A, var"#s185"<:AbstractRNG, _B, _C, _D, _E, _F}

Return opt unchanged.

Default pass-through factory for optimisation estimators and results. Overridden for estimators that carry parameters requiring update at each optimisation step.

Related

source
PortfolioOptimisers.factoryMethod
factory(a::Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                 <:AbstractResult}, args...; kwargs...) -> a
factory(a::AbstractVector{<:Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                                  <:AbstractResult}}, args...; kwargs...) -> Vector

No-op factory function for constructing objects with a uniform interface.

Defining methods which dispatch on the first argument allows for a consistent factory interface across different types.

factory and port_opt_view are the two propagation mechanisms in this library. They are duals: factory threads runtime values (prior moments, observation weights, previous portfolio weights) down through a composed struct tree; port_opt_view threads an index selection (a subset of assets or observations) down through the same tree.

The vector method is the one forwarding contract for every vector-valued propagation field: it applies factory to each element and forwards args... and kwargs... unchanged, so a family that admits a vector of estimators, algorithms, or results needs no method of its own. A family that needs more than the forward, such as a concrete element type (concrete_typed_array_if_abstract), defines its own more specific method.

Algorithm

The scalar method:

  1. Return a unchanged, and drop args... and kwargs.... This method is the leaf of the recursion, and it is what makes an untagged type safe to call the verb on.

The vector method:

  1. For each element ai of a, call factory on ai, and forward args... and kwargs... unchanged.
  2. Collect the results into a new vector, in the order of a, and return it.

A @propagatable struct with at least one @fprop- or @wprop-tagged field carries a generated method that dominates the scalar method. That method rebuilds the struct with its keyword constructor, sending each @fprop field through factory_child and each @wprop field through _wprop.

Arguments

  • a: Indicates no object should be constructed, or a vector whose elements are rebuilt one by one.
  • args...: Arbitrary positional arguments (ignored by the scalar method, forwarded by the vector method).
  • kwargs...: Arbitrary keyword arguments (ignored by the scalar method, forwarded by the vector method).

Returns

  • a: The input unchanged.
  • v::Vector: The element-wise rebuilds, for the vector method.

Examples

julia> factory(nothing, 1, 2; x = 3)julia> factory(MeanValue())MeanValue  w ┴ nothing

Related

source
factory(
    opt::Union{NonFiniteAllocationOptimisationEstimator, NonFiniteAllocationOptimisationResult},
    _
) -> RandomWeighted{_A, var"#s185", _B, _C, _D, _E, _F, Bool} where {_A, var"#s185"<:AbstractRNG, _B, _C, _D, _E, _F}

Return opt unchanged.

Default pass-through factory for optimisation estimators and results. Overridden for estimators that carry parameters requiring update at each optimisation step.

Related

source
PortfolioOptimisers.optimiseMethod
optimise(nco::NestedClustered{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any,
                  <:Any, <:Any, <:Any, Nothing
              }, rd::ReturnsResult;
         dims::Int = 1, branchorder::Symbol = :optimal, str_names::Bool = false,
         save::Bool = true, kwargs...) -> NestedClusteredResult

Run the Nested Clustered Optimisation portfolio optimisation.

Arguments

  • nco: The nested clustered optimiser to use.
  • rd: The returns result to use.
  • dims: The dimension along which observations advance in time.
  • branchorder: Passed to the inner and outer optimisers. If this optimiser uses hierarchical clustering, this applies to the clusterisation. The branch order to use for the clusterisation.
  • str_names: Passed to the inner and outer optimisers. Whether to use string names for the assets in the optimisation.
  • save: Passed to the inner and outer optimisers. Whether to save the JuMP model in the optimisation result.
  • kwargs: Additional keyword arguments passed to the optimisation function.

Validation

  • No field in the tree of nco holds an Online. An ArgumentError naming the field is thrown otherwise, through assert_batch_entry: a plain optimise is a batch fit, and a wrapper resolves only at the warm-up of the fold loop's online arm.

Returns

  • res::NestedClusteredResult: The combined portfolio. retcode is an OptimisationFailure when any intra-cluster optimisation, the inter-cluster optimisation, or the weight finalisation failed.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[9]
M. M. De Prado. Machine learning for asset managers (Cambridge University Press, 2020).
[142]
M. López de Prado. A robust estimator of the efficient frontier. SSRN Electronic Journal (2019).