Near optimal centering

PortfolioOptimisers.ConstrainedNearOptimalCenteringType
struct ConstrainedNearOptimalCentering <: NearOptimalCenteringAlgorithm

Constrained Near Optimal Centering algorithm.

Centres inside the feasible region the portfolio constraints define. Its middle is the shared assemble_jump_model!, so every JuMPOptimiser setting reaches the centring model. Use this variant when a setting must bind on the centring solve itself. See UnconstrainedNearOptimalCentering for what the default variant does not read.

Related

source
PortfolioOptimisers.UnconstrainedNearOptimalCenteringType
struct UnconstrainedNearOptimalCentering <: NearOptimalCenteringAlgorithm

Unconstrained Near Optimal Centering algorithm. This is the default alg of NearOptimalCentering.

Centres inside the near-optimal region of the unconstrained problem. The centring model carries the weight bounds and the budgets its head applies, the risk expression, the return expression and the non-fixed fees, and nothing else — the constraint and penalty builders of the shared middle do not run. "Unconstrained" names that omission.

The omitted settings are carried and validated, not rejected. They are not inert: the three anchor portfolios are solved as MeanRisk sub-problems that do run the whole middle, so an omitted setting still shapes the anchors and, through them, the centring target. The one configuration in which such a setting reaches no model at all is w_min, w_opt and w_max all supplied, because then no sub-problem is solved.

Settings the centring model reads

pe, slv, wb, bgt, sbgt, gbgt, sc, so, sca, ret — the bound-free copy, see no_bounds_optimiser — and the non-fixed part of fees.

Settings the centring model does not read

lcse, cte, card, gcarde, scard, sgcarde, smtx, sgmtx, slt, sst, sglt, sgst, lt, st, xbgt, ss, tn, tr, ple, l2c, lpc, linfc, l1, l2, linf, lp, ccnt, cobj, and the fixed part of fees — a fixed fee is charged per position held, so it needs the cardinality binaries set_mip_constraints! produces.

cobj is the one omission a user can observe on the objective rather than on the feasible set: set_near_optimal_objective_function! folds no Objective Penalty into the barrier, so a Custom Objective Term prices the anchor sub-problems, not the centring solve.

Related

source
PortfolioOptimisers.NearOptimalCenteringResultType
struct NearOptimalCenteringResult{__T_jr, __T_r, __T_w_min_retcode, __T_w_opt_retcode, __T_w_max_retcode, __T_noc_retcode, __T_fb} <: RiskJuMPOptimisationResult

Result type for Near Optimal Centering portfolio optimisation.

Fields

  • r: The risk measure the optimisation ran under, or a vector of them, stored resolved — a Deferred Quantity has already been fitted and an unstated slot has already taken the prior's field. A resolved measure is fitted state, not configuration, so it belongs on the Result. Pass it back as expected_risk(res.r, res.w, res.pr; sca = res.sca).
  • w_min_retcode: Return code for the minimum-risk sub-problem.
  • w_opt_retcode: Return code for the optimal-objective sub-problem.
  • w_max_retcode: Return code for the maximum-risk sub-problem.
  • noc_retcode: Return code for the near-optimal centering sub-problem.
  • 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).

Property access delegates to the embedded JuMPOptimisationResult: the unique retcodes resolve directly, the virtual :w and unknown properties resolve through jr.

Related

source
PortfolioOptimisers.NearOptimalCenteringType
struct NearOptimalCentering{__T_opt, __T_r, __T_obj, __T_bins, __T_w_min, __T_w_min_ini, __T_w_opt, __T_w_opt_ini, __T_w_max, __T_w_max_ini, __T_ucs_flag, __T_alg, __T_fb} <: RiskJuMPOptimisationEstimator

Near Optimal Centering (NOC) portfolio optimiser.

NearOptimalCentering first solves three anchor sub-problems – minimum risk, maximum return, and the user's own objective – and reads a near optimal region off them. It then finds the analytic centre of that region, which is the point that maximises the product of the margins of its inequalities. The answer is robust to small perturbations in risk-return space.

Fields

  • opt: JuMP optimiser configuration.
  • r: Risk measure or vector of risk measures.
  • obj: Portfolio objective function.
  • bins: Number of equally-spaced risk bins for the frontier approximation.
  • w_min: Minimum risk portfolio weights.
  • w_min_ini: Initial weights for the minimum-risk sub-problem.
  • w_opt: Optimal portfolio weights.
  • w_opt_ini: Initial weights for the optimal sub-problem.
  • w_max: Maximum return portfolio weights.
  • w_max_ini: Initial weights for the maximum-risk sub-problem.
  • ucs_flag: Whether to use the uncertainty set.
  • fb: Fallback result or estimator.

Constructors

NearOptimalCentering(;    opt::JuMPOptimiser,    r::TD{<:RM_VecRM} = StandardDeviation(),    obj::TD_Option{<:ObjectiveFunction} = MinimumRisk(),    bins::Option{<:Number} = nothing,    w_min::TD_Option{<:VecNum} = nothing,    w_min_ini::TD_Option{<:VecNum} = nothing,    w_opt::TD_Option{<:VecNum_VecVecNum} = nothing,    w_opt_ini::TD_Option{<:VecNum_VecVecNum} = nothing,    w_max::TD_Option{<:VecNum} = nothing,    w_max_ini::TD_Option{<:VecNum} = nothing,    ucs_flag::Bool = true,    alg::NearOptimalCenteringAlgorithm = UnconstrainedNearOptimalCentering(),    fb::TDO_Option{<:OptE_Opt} = nothing) -> NearOptimalCentering

Keywords correspond to the struct's fields. Fields typed TD, TD_Option or TDO_Option may hold a TimeDependent per-fold schedule instead of a static value: the risk measure, objective, anchor/warm-start weights and fallback are problem definition, so a cross-validation fold loop resolves them per fold, and a fold-less optimise runs with each at its static default. bins, ucs_flag and the alg formulation variant are execution control and stay static.

Validation

  • If r is a vector: !isempty(r).
  • If w_min is a vector: !isempty(w_min).
  • If w_min_ini is a vector: !isempty(w_min_ini).
  • If w_opt is a vector: !isempty(w_opt).
  • If w_opt_ini is a vector: !isempty(w_opt_ini).
  • If w_max is a vector: !isempty(w_max).
  • If w_max_ini is a vector: !isempty(w_max_ini).
  • If bins is a number: isfinite(bins) && bins > 0.
  • fb schedules: bind !== :nearest.

Mathematical definition

Three anchor portfolios bound the region: $\boldsymbol{w}_{\min}$ of minimum risk, $\boldsymbol{w}_{\max}$ of maximum return, and $\boldsymbol{w}_{\mathrm{opt}}$, which solves the user's own objective. The near optimal region is the set of portfolios whose return is no worse than $\epsilon_{1}$ and whose risk is no worse than $\epsilon_{2}$:

\[\begin{align} \hat{\boldsymbol{\mu}}^\intercal \boldsymbol{w} &\geq \epsilon_{1}\,, \\ \rho(\boldsymbol{w}) &\leq \epsilon_{2}\,, \\ \boldsymbol{w} &\in \mathcal{W}\,. \end{align}\]

The two margins are a fraction of the span the anchors describe, and bins is the number of parts that span is divided into:

\[\begin{align} \epsilon_{1} &= \hat{\boldsymbol{\mu}}^\intercal \boldsymbol{w}_{\mathrm{opt}} - \frac{\hat{\boldsymbol{\mu}}^\intercal \boldsymbol{w}_{\max} - \hat{\boldsymbol{\mu}}^\intercal \boldsymbol{w}_{\min}}{m}\,, \\ \epsilon_{2} &= \rho(\boldsymbol{w}_{\mathrm{opt}}) + \frac{\rho(\boldsymbol{w}_{\max}) - \rho(\boldsymbol{w}_{\min})}{m}\,. \end{align}\]

The NOC portfolio is the analytic centre of that region, which is the point that maximises the product of the margins of its inequalities. Equivalently, it minimises the logarithmic barrier:

\[\begin{align} \underset{\boldsymbol{w}}{\min} \; &- \ln\left(\hat{\boldsymbol{\mu}}^\intercal \boldsymbol{w} - \epsilon_{1}\right) - \ln\left(\epsilon_{2} - \rho(\boldsymbol{w})\right) \\ &- \sum_{i=1}^{N} \ln\left(w_{i}\right) - \sum_{i=1}^{N} \ln\left(w_{u,i} - w_{i}\right) \\ \text{s.t.} \quad &\boldsymbol{w} \in \mathcal{W}\,. \end{align}\]

Where:

  • $\boldsymbol{w}$: Portfolio weight vector.
  • $\mathcal{W}$: Feasible weight set defined by portfolio constraints.
  • $\boldsymbol{w}_{\min}$: Minimum-risk anchor weights, w_min.
  • $\boldsymbol{w}_{\mathrm{opt}}$: Anchor weights of the user's objective, w_opt.
  • $\boldsymbol{w}_{\max}$: Maximum-return anchor weights, w_max.
  • $\boldsymbol{w}_{u}$: Upper weight bound, opt.wb.ub.
  • $\hat{\boldsymbol{\mu}}$: Estimated expected return vector.
  • $\rho(\boldsymbol{w})$: Portfolio risk measure.
  • $m$: Number of bins, bins. It defaults to $T / N$, so a wider universe or a shorter history gives a wider region.
  • $N$: Number of assets.

Details

  • Every logarithm is modelled as an exponential cone, so the barrier is a conic problem rather than a nonlinear one.
  • The last barrier term is the upper weight bound. It has no counterpart in the published model, which states the region with a lower bound alone.
  • The three anchors are solved as MeanRisk sub-problems, unless the caller supplies them.
  • The solution is centrally located within the near optimal region, so it is robust to small perturbations of the objective.

Propagated parameters

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

  • opt: Recursively updated via factory.
  • r: Recursively updated via factory.
  • fb: Recursively updated via factory.

View parameters

NearOptimalCentering 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 opt.pe already holds a prior result, the method replaces X with opt.pe.X, so the children are viewed against the prior's own observations rather than the caller's matrix.
  • opt and r recurse through port_opt_view with that matrix.
  • The six weight vectors w_min, w_min_ini, w_opt, w_opt_ini, w_max and w_max_ini are sliced to the selected assets.
  • obj, bins, ucs_flag, alg and fb are carried through unchanged.

Related

References

  • [134] D. Cajas. Robust portfolio selection with near optimal centering. Available at SSRN 3572435 (2019).
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 11.4.
  • [135] T. de Graaf. Robust Mean-Variance Optimization. Master's Thesis, Leiden University (2016).
  • [95] S. Boyd and L. Vandenberghe. Convex Optimization (Cambridge University Press, Cambridge, UK, 2004). Section 8.5.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.port_opt_viewMethod
port_opt_view(
    noc::NearOptimalCentering,
    i,
    X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
    args...
) -> NearOptimalCentering{JuMPOptimiser{__T_pe, __T_slv, __T_wb, __T_bgt, __T_sbgt, __T_gbgt, __T_xbgt, __T_lt, __T_st, __T_lcse, __T_cte, __T_gcarde, __T_sgcarde, __T_smtx, __T_sgmtx, __T_slt, __T_sst, __T_sglt, __T_sgst, __T_tn, __T_fees, __T_sets, __T_tr, __T_ple, __T_ret, __T_sca, __T_ccnt, __T_cobj, __T_sc, __T_so, __T_ss, __T_card, __T_scard, __T_l2c, __T_lpc, __T_linfc, __T_l1, __T_l2, __T_lp, __T_linf, __T_brt, __T_x_src, __T_strict, __T_cache}, _A, _B, _C, _D, _E, _F, _G, _H, _I, Bool, <:NearOptimalCenteringAlgorithm} where {__T_pe, __T_slv, __T_wb, __T_bgt, __T_sbgt, __T_gbgt, __T_xbgt, __T_lt, __T_st, __T_lcse, __T_cte, __T_gcarde, __T_sgcarde, __T_smtx, __T_sgmtx, __T_slt, __T_sst, __T_sglt, __T_sgst, __T_tn, __T_fees, __T_sets, __T_tr, __T_ple, __T_ret, __T_sca, __T_ccnt, __T_cobj, __T_sc, __T_so, __T_ss, __T_card, __T_scard, __T_l2c, __T_lpc, __T_linfc, __T_l1, __T_l2, __T_lp, __T_linf, __T_brt, __T_x_src, __T_strict, __T_cache, _A, _B, _C, _D, _E, _F, _G, _H, _I}

Return a cluster-sliced copy of NearOptimalCentering for asset index set i and returns matrix X.

source
PortfolioOptimisers.optimiseMethod
optimise(noc::NearOptimalCentering{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any,
                  <:Any, <:Any, <:Any, <:Any, <:Any, Nothing
              },
         rd::ReturnsResult; dims::Int = 1,
         str_names::Bool = false, save::Bool = true, kwargs...) -> NearOptimalCenteringResult

Run the Near Optimal Centering portfolio optimisation.

Arguments

  • noc: The near optimal centering optimiser to use.
  • rd: The returns result to use. If isa(noc.opt.pe, AbstractPriorResult), rd is not necessary if doing a standalone optimisation, but may be required/desired by fallbacks and/or clusterisation.
  • dims: The dimension along which observations advance in time.
  • str_names: Whether to use string names for the assets in the optimisation.
  • save: 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 noc 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.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[95]
S. Boyd and L. Vandenberghe. Convex Optimization (Cambridge University Press, Cambridge, UK, 2004).
[134]
D. Cajas. Robust Portfolio Selection with Near Optimal Centering. Available at SSRN 3572435 (2019).
[135]
T. de Graaf. Robust Mean-Variance Optimization. Master's thesis, Leiden University (2016).