Base JuMP Optimisation: private API

PortfolioOptimisers.ObjectiveFunctionType
abstract type ObjectiveFunction <: AbstractEstimator

Abstract supertype for portfolio objective functions.

Subtype ObjectiveFunction to implement portfolio optimisation objectives such as minimum risk, maximum return, or maximum Sharpe ratio.

The four concrete children are the source's four classic objective functions, one per subsection.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 8.2.
source
PortfolioOptimisers.JuMPReturnsEstimatorType
abstract type JuMPReturnsEstimator <: AbstractEstimator

Abstract supertype for JuMP-based returns estimators used in optimisation models.

JuMPReturnsEstimator types define how expected returns are incorporated into JuMP models.

The two children are the source's two return definitions: the arithmetic return of Section 8.1.1 and the geometric return of Section 8.1.2.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 8.1.
source
PortfolioOptimisers.JuMPConstraintEstimatorType
abstract type JuMPConstraintEstimator <: AbstractConstraintEstimator

Abstract supertype for JuMP constraint estimators.

The extension point for user-defined constraints and objectives. Rather than subtyping this directly, subtype one of the two purpose-built children and implement its one contract method:

(The objective child subtypes AbstractEstimator directly — it is grouped here as the sibling extension point, not by type hierarchy.)

Related

source
PortfolioOptimisers.RiskJuMPOptimisationResultType
abstract type RiskJuMPOptimisationResult <: NonFiniteAllocationOptimisationResult

Abstract supertype for JuMP-based continuous optimisation results that carry a risk measure.

One of the two JuMP result halves; mirrors RiskJuMPOptimisationEstimator. The sibling half is NonRiskJuMPOptimisationResult, for the JuMP results that carry no risk measure at all. Concrete subtypes embed a JuMPOptimisationResult as their first field (jr) and add only their unique fields plus the trailing fb. Every subtype carries a resolved r; a JuMP result with no r belongs on the sibling branch. The default getproperty resolves unique fields directly and delegates everything else (including :w and the pa fall-through) to jr; types with composed sub-result fields override it to forward into those first.

Related

source
PortfolioOptimisers.NonRiskJuMPOptimisationResultType
abstract type NonRiskJuMPOptimisationResult <: NonFiniteAllocationOptimisationResult

Abstract supertype for JuMP-based continuous optimisation results that carry no risk measure.

The sibling half of RiskJuMPOptimisationResult. A relaxed risk budgeting run builds its constraints straight from pr.sigma and never resolves a measure, so its result has no r to carry. Splitting the branch keeps r mandatory on the risk half instead of optional on a shared type. Concrete subtypes follow the same shape: an embedded JuMPOptimisationResult jr first, their unique fields, then the trailing fb.

Related

source
PortfolioOptimisers.RJR_NRJRType
const RJR_NRJR = Union{<:RiskJuMPOptimisationResult, <:NonRiskJuMPOptimisationResult}

Union of both JuMP result halves.

The default getproperty and propertynames are bound here, not on either half alone. MeanRiskResult and NearOptimalCenteringResult declare no @forward_properties rule and depend on that default for res.w, so a half without it would silently cost the next measure-less leaf its property forwarding.

Related

source
PortfolioOptimisers.AbstractDecompositionContractType
abstract type AbstractDecompositionContract

Abstract supertype for the head's decomposition contract: how model[:w] relates to the long/short parts model[:lw] and model[:sw].

Heads build that relationship in one of two incompatible ways, and a builder that pins the decomposition needs to know which, because the two need different constraints to become exact. The head declares its own with set_decomposition_contract!; builders read it back with decomposition_contract and dispatch.

Related

source
PortfolioOptimisers.SHARED_STATEConstant
SHARED_STATE

The Model State entries deliberately shared bare across a nested risk build.

The complement of Per-Build Risk State: an entry belongs here iff it is not a function of the weights being optimised and not a build-scoped presence flag, so the inner and outer builds want the same object and prefixing it would break sharing rather than protect it. shared_get and friends validate against this set, so the classification is enforced at run time rather than only by the seam-lock test.

Each grouping records why those entries are shared. Adding a name here is a claim that a nested build may safely see the enclosing build's copy — check that claim before adding.

source
PortfolioOptimisers.WeightsFromPartsType
struct WeightsFromParts <: AbstractDecompositionContract

The head defines the weights from the parts: w = lw - sw is an identity, lw and sw being the primitive variables. Declared by set_rb_mip_w!.

Because the identity always holds, forcing the long-xor-short sign pattern is enough to pin the decomposition: with sw = 0 the identity leaves lw == w, and lw >= 0 makes that max(w, 0). No slack remains to close.

Related

source
PortfolioOptimisers.PartsBoundWeightsType
struct PartsBoundWeights <: AbstractDecompositionContract

The head defines the parts as bounds on the weights: lw >= w, sw >= -w, lw, sw >= 0, w being the primitive variable. Declared by set_weight_constraints!.

The parts are only upper bounds on the true long/short exposures, so every budget built on them (bgt, sbgt, gbgt) bounds the realised exposure rather than pinning it. Forcing the sign pattern does not change that — the slack survives it — so pinning the decomposition under this contract needs two further constraints to close it.

Related

source
PortfolioOptimisers.optimise_JuMP_model!Function
optimise_JuMP_model!(model::JuMP.Model, slv::Slv_VecSlv)

Attempt to optimise a JuMP model using one or more configured solvers.

Tries each solver in order, applying settings and checking for solution feasibility. Returns a JuMPResult with trial errors and success status.

Arguments

  • model: JuMP model to optimise.
  • slv: Single Solver or vector of Solver objects.

Returns

  • res::JuMPResult: Result object containing trial errors and success flag.

Details

  • For each solver, sets the optimizer and attributes, runs JuMP.optimize!, and checks solution feasibility.
  • If a solver fails at one of the three guarded stages, records the error under the solver's name and tries the next.
  • Stops at the first successful solution, and leaves no trials entry for it.

Three stages are guarded: JuMP.set_optimizer, JuMP.optimize! and JuMP.assert_is_solved_and_feasible. set_solver_attributes is not. A solver attribute the backend refuses throws straight out of this function, so no trial is recorded and no later solver is tried. This is deliberate: a misspelled attribute is a configuration error, not a solver failure, and swallowing it would silently drop a setting the caller asked for.

Give each solver of a vector its own name. The name is the trials key, the default is "" for every solver, and a later failure overwrites an earlier one under the same key. Measured on two solvers that both fail at set_optimizer: trials holds one entry with the default names and two with distinct names.

Related

source
optimise_JuMP_model!(
    model::Model,
    opt::JuMPOptimisationEstimator
) -> Tuple{Union{OptimisationFailure{Dict{Any, Any}}, OptimisationSuccess{Dict{Any, Any}}}, JuMPOptimisationSolution{<:AbstractArray{var"#s137", N}} where {var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), N}}
optimise_JuMP_model!(
    model::Model,
    opt::JuMPOptimisationEstimator,
    datatype::DataType
) -> Tuple{Union{OptimisationFailure{Dict{Any, Any}}, OptimisationSuccess{Dict{Any, Any}}}, JuMPOptimisationSolution{<:AbstractArray{var"#s137", N}} where {var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), N}}

Attempt to solve the JuMP model using each solver in opt.opt.slv in order.

Tries each solver sequentially, checking feasibility and finite non-zero weights. Returns a (retcode, solution) tuple where retcode is OptimisationSuccess or OptimisationFailure and solution is a JuMPOptimisationSolution.

Related

source
PortfolioOptimisers.set_model_scales!Function
set_model_scales!(model::JuMP.Model, sc::Number, so::Number)

Register constraint scale sc and objective scale so as named expressions in the JuMP model.

The positional order is sc first, matching every head, which passes opt.sc, opt.so straight out of its JuMPOptimiser.

Arguments

Returns

  • nothing.

Related

source
PortfolioOptimisers.set_model_observations!Function
set_model_observations!(model::JuMP.Model, T::Integer)

Register the observation count of the fit as the named entry model[:T].

The sibling of set_model_scales!, and every head calls it in the same place, before any builder runs. The count is a model-wide singleton: one fit produces one returns matrix, and its row count is the holding period every builder measures against. Registering it here rather than inside a builder means a reader may rely on it whatever the model carries, and get_T reads it back.

The row count of a fold, of a benchmark, or of a stacked meta-optimisation panel is a different number. A site that means one of those keeps its own size(..., 1).

Arguments

  • model::JuMP.Model: JuMP optimisation model.
  • T::Integer: Observation count of the fit, read back by get_T.

Returns

  • nothing.

Related

source
PortfolioOptimisers.set_initial_w!Function
set_initial_w!(args...)
set_initial_w!(w::VecNum, wi::VecNum)

Set initial (warm-start) values for portfolio weight variables in the JuMP model.

The no-op fallback does nothing when wi is not provided. The two-argument method sets JuMP start values for each weight variable.

Arguments

  • w::VecNum: Vector of JuMP weight variables.
  • wi::VecNum: Vector of initial weight values.

Returns

  • nothing.

Related

source
PortfolioOptimisers.set_w!Function
set_w!(model::JuMP.Model, X::MatNum, wi::Option{<:VecNum_VecVecNum})

Create portfolio weight variables in the JuMP model and optionally set initial values.

Registers a vector of weight variables w of length size(X, 2) in the model. If wi is provided, sets the initial values via set_initial_w!.

Arguments

  • model::JuMP.Model: JuMP optimisation model.
  • X::MatNum: Asset returns matrix (shape: observations × assets).
  • wi: Optional initial weight values.

Returns

  • nothing.

Related

source
PortfolioOptimisers.set_net_portfolio_returns!Function
set_net_portfolio_returns!(model::JuMP.Model, X::MatNum)

Compute and register net portfolio returns (after fees) in the JuMP model.

Calls set_portfolio_returns! and subtracts the fees if any are registered. The model's :fees expression holds the per period terms l, s and tn, which are rates per period, so it is subtracted from every observation. The model's :one_time_fees expression holds the two fixed terms, which are charged one time for the whole holding period, and :fee_fa names the clock they fall on: a nothing or FirstObservationFees clock subtracts them from the first observation alone, and an AmortisedFees spreads them over the observation count of the fit. That is the rule charge_fees states at the value level, and charge_one_time_fees applies it here.

Arguments

  • model::JuMP.Model: JuMP optimisation model.
  • X::MatNum: Asset returns matrix.

Returns

  • The net portfolio returns expression.

Related

source
PortfolioOptimisers.get_retFunction
get_ret(model::JuMP.Model)

Return the portfolio expected-return expression model[:ret].

Asserts the return expression has been registered; errors otherwise.

Related

source
PortfolioOptimisers.set_unit_budget!Function
set_unit_budget!(model::JuMP.Model)

Record that the head normalised the model's budget scale to unit.

A head declares this when its own constraints make the formulation scale-invariant, so downstream builders may substitute the literal 1 for the homogenisation variable k. RiskBudgeting is the only such head: its log-barrier normalisation pins the scale, and the weights are renormalised after the solve. Note that k remains a free variable under this declaration — it is the budget scale that is unit, not k that is constant.

Related

source
PortfolioOptimisers.effective_kFunction
effective_k(model::JuMP.Model)

Return the budget scale a builder should use: 1 under a unit budget, else model[:k].

Builders that multiply a bound by the budget want this rather than get_k, so a scale-invariant head is honoured without each builder re-deriving that fact for itself.

Related

source
PortfolioOptimisers.set_decomposition_contract!Function
set_decomposition_contract!(model::JuMP.Model, dc::AbstractDecompositionContract)

Record how the head related model[:w] to model[:lw]/model[:sw].

The first declaration wins: a head may run both builders (the mixed-integer RiskBudgeting head calls set_rb_mip_w!, then hands the same lw/sw to set_weight_constraints!, which re-states them as bounds). The identity is the stronger statement and still holds, so the bounds must not overwrite it.

Related

source
PortfolioOptimisers.assert_shared_stateFunction
assert_shared_state(name::Symbol)

Assert name is a sanctioned bare Model State entry.

Guards the shared_get family so that reaching for a per-build entry without a prefix fails loudly at the call site, rather than silently aliasing the enclosing build's copy — the regression class that broke IndependentVariableTracking.

source
PortfolioOptimisers.frontier_point_countFunction
frontier_point_count(front::Frontier)
frontier_point_count(front::VecNum)

Number of sweep points one frontier bound asks for.

A Frontier states its count in N; a stated vector of bounds states it in its length. Both shapes are admissible in :ret_frontier and :risk_frontier (see Front_NumVec), and at Model Assembly time a Frontier has not yet been resolved into its range, so the count is read from the shape rather than from a materialised vector.

Related

source
PortfolioOptimisers.frontier_sweep_pointsFunction
frontier_sweep_points(model::JuMP.Model)

Count the solves the model's frontier sweep runs, and the factors that make up the count.

The sweep is a product: every swept return term and every swept risk measure joins the same Iterators.product, so k bounds of N points each cost N^k full solves rather than k * N. This reads both frontier registries — :ret_frontier and :risk_frontier — and multiplies their per-entry counts together.

The product is accumulated as a BigInt, so it is exact and cannot overflow into a value that would pass a cap it should fail.

Returns

  • (total, factors): the total number of sweep points, and a bound_key => count pair per swept entry, in registration order (return terms first).

Related

source
PortfolioOptimisers.assert_frontier_sweep_capFunction
assert_frontier_sweep_cap(model::JuMP.Model)

Assert the total frontier sweep does not exceed the active max_frontier ceiling.

Frontier's constructor caps the N of one bound; nothing there sees the product, so k bounds at the ceiling cost max_frontier^k solves and no guard fires. This is the guard, and it runs at Model Assembly — the point at which both frontier registries are complete and no sweep solve has started yet.

Every sweep point runs a full inner optimise_JuMP_model! solve, so the product is the compute-exhaustion sink max_frontier exists to bound (see RESOURCE_LIMITS). The cap applies to the risk side and the return side alike.

Returns

  • nothing.

Throws

  • DomainError if the product exceeds RESOURCE_LIMITS[].max_frontier. The message names the product, the factors that made it, and the knob that raises the ceiling.

Related

source
PortfolioOptimisers.frontier_axisFunction
frontier_axis(frontier::VecPair)

Turn one resolved frontier registry into the sweep axis it stands for.

Both registries — :ret_frontier and :risk_frontier — hold (bound_var_key, bound_key) => (expr, points, …) entries, and both are swept as a product across their own entries: two swept risk measures of N points each cost N^2 solves on the risk axis alone. This is that product, in two halves — the keys of the bound parameters, and the values to write into them — so set_frontier_point! can zip one against the other.

Arguments

  • frontier::VecPair: A resolved frontier registry. Every entry's bound is already a vector of sweep points.

Returns

  • (keys, points): Two product iterators of equal length.

Related

source
PortfolioOptimisers.set_ret_frontier_parameters!Function
set_ret_frontier_parameters!(model::JuMP.Model, ret_frontier::VecPair)

Register one parameter and one lower-bound constraint per swept return term.

Each term's bound binds on that term's own expression, so the return side is a product across terms rather than a single ladder. The bound is homogenised by k, exactly as the scalar bound in set_return_bounds! is.

Arguments

  • model::JuMP.Model: The JuMP optimisation model.
  • ret_frontier::VecPair: The resolved :ret_frontier registry.

Returns

Related

source
PortfolioOptimisers.set_risk_frontier_parameters!Function
set_risk_frontier_parameters!(model::JuMP.Model, risk_frontier::VecPair)

Register one parameter and one bound constraint per swept risk measure.

The twin of set_ret_frontier_parameters!, and the one place the risk side's two extra pieces are stated: the polarity d, which flips the inequality for a measure whose bigger value is better, and the homogenisation k, which the scalar bound in set_risk_upper_bound! also applies. k is the literal 1 under every head whose objective is fixed — NearOptimalCentering minimises a barrier, so its head registers k = 1 and the factor is a no-op there — and the ratio variable under MaximumRatio. Reading it here rather than at each call site is what keeps the two heads from drifting apart.

Arguments

  • model::JuMP.Model: The JuMP optimisation model.
  • risk_frontier::VecPair: The resolved :risk_frontier registry.

Returns

Related

source
PortfolioOptimisers.frontier_sweep_axesFunction
frontier_sweep_axes(ret_axis, risk_axis)

Join the two sweep axes into the flat sequence of sweep points.

The risk axis varies fastest, so the flat order is return-outer and risk-inner. That order is load-bearing rather than cosmetic: NearOptimalCentering solves its anchor portfolios as one MeanRisk sweep over the same two frontiers, and pairs anchor i with sweep point i. Stating the order once here is what keeps the two sweeps aligned. Either axis may be nothing, which means that side is not swept. If both axes are nothing, the sweep is one point that writes nothing.

Arguments

Returns

  • An iterator of sweep points. Each point is a tuple of (keys, bounds) pairs, one per swept axis, and its length is the number of solves the sweep runs.

Related

source
PortfolioOptimisers.frontier_sweep!Function
frontier_sweep!(point!, model, opt, ::Type{T}, points)
frontier_sweep!(point!, model, opt, ::Type{T}, n::Integer)
frontier_sweep!(model, opt, ::Type{T}, points)

Solve one model per sweep point and collect the outcomes.

The collect tail every frontier sweep shares. The model is assembled once and its objective is set once; a sweep point changes only parameter values, so no constraint is rebuilt between solves. point! is the per-optimiser hook, called with the flat 1-based index of the point after its bounds are written — NearOptimalCentering uses it to move noc_rk and noc_rt onto that point's anchor, and MeanRisk needs no hook at all.

The n::Integer method sweeps n points with no frontier bound to write, which is the unconstrained NearOptimalCentering sweep over a vector of anchors.

Arguments

  • point!: Hook of one argument, the flat index of the sweep point. Defaults to a no-op.
  • model::JuMP.Model: The JuMP optimisation model.
  • opt::JuMPOptimisationEstimator: The optimiser, for optimise_JuMP_model!.
  • ::Type{T}: Element type of the returns matrix.
  • points: A frontier_sweep_axes iterator, or the point count n.

Returns

  • (retcodes, sols): One entry per sweep point, in flat sweep order.

Related

source
PortfolioOptimisers.state_keyFunction
state_key(prefix::Symbol, name::Symbol)
state_key(prefix::Symbol, name::Symbol, i)

Resolve the Model State key for entry name under prefix, optionally at measure index i.

Internal to the Model State interface: the single place the two namespacing conventions are spelled. A Model State key is disambiguated on two axes, and both are resolved here:

  • prefix separates one build from another, so a nested risk build cannot collide with the build that encloses it.
  • i separates one measure instance from another inside a single build, so two ConditionalValueatRisk measures in the same vector get their own scratch entries.

Keeping both here is what lets the seam-lock test assert that no emitter builds a key by hand — emitters reach Model State through state_get, state_has, state_set! and state_build!.

Neither axis carries a delimiter, so composition is not injective: (:te_dr_, 11) and (:te_dr_1, 1) both give :te_dr_11. The spelling is kept — a delimiter would move every top-level key a caller reads — and the collision is caught where it does harm, by assert_state_key_free at registration.

Related

source
PortfolioOptimisers.assert_state_key_freeFunction
assert_state_key_free(model::JuMP.Model, key::Symbol)

Assert Model State key key is not registered yet, so a write cannot replace an entry.

Neither axis of state_key is separated by a delimiter, so key composition is not injective: a name that ends in a digit and a low index compose the same Symbol as a shorter name and a higher index — state_key(p, :te_dr_, 11) == state_key(p, :te_dr_1, 1). Without this guard the second write wins, the model carries one entry where the build expected two, and a constraint binds the wrong variable. That is a wrong answer, not a crash, so the registration verb fails closed instead.

A delimiter was rejected as the fix: it would move every top-level key spelling (state_key(Symbol(""), :ret_, 1) is :ret_1, a key callers read), and it would still let one emitter overwrite another's entry under a key both spell correctly. The guard closes both. Re-registration under one key has no legitimate reading either: the build-once case is state_build!, which returns the existing entry untouched, and the flag case is mark_state!, which is idempotent.

Returns

  • nothing.

Throws

  • ArgumentError if key is already registered. The message names the key and the two verbs that do accept a repeat.

Related

source
PortfolioOptimisers.state_set!Function
state_set!(model::JuMP.Model, prefix::Symbol, name::Symbol, val)
state_set!(model::JuMP.Model, prefix::Symbol, name::Symbol, i, val)

Register val in the model under the prefixed Model State key and return it.

A nested risk build (e.g. risk tracking) passes a non-empty prefix so the shared infrastructure entries it creates (:X, :net_X, :W, :dd, …) do not collide with the outer model's; the default empty prefix reproduces the bare key.

The indexed method registers per-measure scratch (:cvar_risk_, :z_cvar_, …) at measure index i, so two instances of the same measure in one build get their own entries. Both disambiguators are resolved by state_key.

Registration is fresh: the composed key must be free, because key composition is not injective and a replaced entry is a wrong answer rather than an error (assert_state_key_free). Reuse is the other two verbs' job.

Throws

  • ArgumentError if the composed key is already registered.

Related

source
PortfolioOptimisers.state_hasFunction
state_has(model::JuMP.Model, prefix::Symbol, name::Symbol)
state_has(model::JuMP.Model, prefix::Symbol, name::Symbol, i)

Return true if Model State entry name is registered under prefix, at index i if given.

Related

source
PortfolioOptimisers.state_getFunction
state_get(model::JuMP.Model, prefix::Symbol, name::Symbol)
state_get(model::JuMP.Model, prefix::Symbol, name::Symbol, i)

Return Model State entry name under prefix, asserting it has been registered.

Prefer a named accessor (get_X, get_net_X, get_dd, …) where one exists: those name the builder that produces the entry, so an out-of-order read reports which builder to call instead of a generic missing-entry error.

The indexed method reads per-measure scratch registered at measure index i.

Related

source
PortfolioOptimisers.state_build!Function
state_build!(f, model::JuMP.Model, prefix::Symbol, name::Symbol)
state_build!(f, model::JuMP.Model, prefix::Symbol, name::Symbol, i)

Return Model State entry name under prefix, building it with f() exactly once.

The memoise-on-prefixed-key idiom shared by every risk and constraint emitter: if the entry is already registered — an earlier measure in the same build produced it, or an outer build already did — it is returned untouched; otherwise f() runs and its value is registered under the prefixed key. Companion entries created inside f register with state_set!.

Because the key is resolved here rather than at the call site, a Model State entry added in future participates in the prefix discipline with no further work. That is what closes a residual hole an earlier, more permissive design left open.

Related

source
PortfolioOptimisers.mark_state!Function
mark_state!(model::JuMP.Model, prefix::Symbol, name::Symbol)

Record that this build has name present, idempotently.

A build-scoped presence flag: name carries no value beyond its own existence, and readers test it with state_has rather than reading it. Marking under prefix is what keeps a nested build's flags out of the enclosing build — the second half of Per-Build Risk State, the half that is not weight-dependent.

Related

source
PortfolioOptimisers.nested_prefixFunction
nested_prefix(prefix::Symbol, tag::Symbol)
nested_prefix(prefix::Symbol, tag::Symbol, i)

Compose the Model State namespace a nested build threads down its own spine.

Distinct from a Model State key: this produces a prefix, not an entry name, so a nested build's entries cannot alias the enclosing build's. tag names the nesting kind (:tr_iv_, :tr_dv_, :te_ir_, :te_dr_, :gain_) and the optional i disambiguates the measure index, which is what makes tracking-nested-in-tracking collision-free.

Related

source
PortfolioOptimisers.nested_indexFunction
nested_index(tag::Symbol, i)

Compose the Model State measure index a sub-measure build threads down.

The twin of nested_prefix on the other disambiguating axis. A composite measure that builds its parts in the same buildGenericValueatRiskRange over its loss and gain sides — separates the parts by index rather than by namespace, because they share the build's infrastructure entries and must not each rebuild them. tag names the part (:loss_, :gain_), and the composition nests, so a range inside a range stays collision-free.

Distinct from a Model State key: this produces an index, not an entry name.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).