Linear Constraints
PortfolioOptimisers.PartialLinearConstraint — Type
struct PartialLinearConstraint{__T_A, __T_B} <: AbstractConstraintResultHolds the coefficient matrix A and the right-hand side vector B of one half of a linear constraint block.
The half is an inequality or an equality according to the field of LinearConstraint that carries it, ineq or eq. The constructor checks that neither A nor B is empty, and that size(A, 1) == length(B). The form is $\mathbf{A} \boldsymbol{x} \leq \boldsymbol{B}$, so a pair with more bounds than rows, or more rows than bounds, is satisfied by no x.
Fields
A: Linear constraint coefficient matrix.
B: Linear constraint response vector.
Constructors
PartialLinearConstraint(; A::MatNum, B::VecNum) -> PartialLinearConstraintKeywords correspond to the struct's fields.
Validation
!isempty(A).!isempty(B).size(A, 1) == length(B), one row ofAper entry ofB.
Examples
julia> PartialLinearConstraint(; A = [1.0 2.0; 3.0 4.0], B = [5.0, 6.0])PartialLinearConstraint A ┼ 2×2 Matrix{Float64} B ┴ Vector{Float64}: [5.0, 6.0]Related
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equation 9.1.
PortfolioOptimisers.LinearConstraint — Type
struct LinearConstraint{__T_ineq, __T_eq} <: AbstractConstraintResultHolds the inequality half and the equality half of a linear constraint block.
Each half is a PartialLinearConstraint, and either one may be absent.
Mathematical definition
\[\begin{align} \mathbf{A}_\text{ineq} \boldsymbol{x} &\leq \boldsymbol{B}_\text{ineq} \\ \mathbf{A}_\text{eq} \boldsymbol{x} &= \boldsymbol{B}_\text{eq}\,. \end{align}\]
Where:
- $\mathbf{A}$: Constraint coefficient matrix.
- $\boldsymbol{B}$: Constraint response vector.
- $\text{ineq}$: Subscript for inequality constraints.
- $\text{eq}$: Subscript for equality constraints.
- $\boldsymbol{x}$: Constrained variable.
The model asserts sc * (A * w - k * B) <= 0 for the inequality half and == 0 for the equality half, where sc is the constraint scale and k is the homogenisation scalar of a ratio objective. The solution is de-homogenised before it is returned, so the returned weights satisfy the form above whatever the objective is.
Fields
ineq: Optional inequality constraints.
eq: Optional equality constraints.
Constructors
LinearConstraint(; ineq::Option{<:PartialLinearConstraint} = nothing, eq::Option{<:PartialLinearConstraint} = nothing) -> LinearConstraintKeywords correspond to the struct's fields.
Validation
- Both
eqandineqcannot benothingat the same time,!(isnothing(ineq) && isnothing(eq)).
Examples
julia> ineq = PartialLinearConstraint(; A = [1.0 2.0; 3.0 4.0], B = [5.0, 6.0]);julia> eq = PartialLinearConstraint(; A = [7.0 8.0; 9.0 10.0], B = [11.0, 12.0]);julia> LinearConstraint(; ineq = ineq, eq = eq)LinearConstraint ineq ┼ PartialLinearConstraint │ A ┼ 2×2 Matrix{Float64} │ B ┴ Vector{Float64}: [5.0, 6.0] eq ┼ PartialLinearConstraint │ A ┼ 2×2 Matrix{Float64} │ B ┴ Vector{Float64}: [11.0, 12.0]Related
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equation 9.1.
PortfolioOptimisers.merge_partial_linear_constraints — Function
merge_partial_linear_constraints(
ps
) -> Union{Nothing, PartialLinearConstraint{var"#s179", <:AbstractVector{var"#s90"}} where {var"#s90"<:(Union{var"#s89", var"#s88"} where {var"#s89"<:Number, var"#s88"<:AbstractJuMPScalar}), var"#s179"<:AbstractMatrix{var"#s90"}, var"#s90"<:(Union{var"#s89", var"#s88"} where {var"#s89"<:Number, var"#s88"<:AbstractJuMPScalar})}}
Concatenate the rows of the same half of several PartialLinearConstraints, skipping the absent ones.
Arguments
ps: The halves to concatenate, each aPartialLinearConstraintornothing.
Returns
- A
PartialLinearConstraint, ornothingwhen every input was absent.
Related
PortfolioOptimisers.merge_linear_constraints — Function
merge_linear_constraints(
lcs::AbstractVector{<:LinearConstraint}
) -> LinearConstraint
Combine several LinearConstraints into the single one that holds all their rows.
A LinearConstraint is a block of rows, and applying two blocks is the same as applying the block that stacks them — the inequality halves concatenate, the equality halves concatenate, and an absent half contributes nothing. This is exactly what generation already does when it is handed several estimators at once: centrality_constraints over a vector of CentralityConstraints appends every row into one result rather than returning one result per estimator.
That equivalence is what this function exists to preserve. A caller that computes its constraints separately — a Pipeline running one step per estimator — can merge them here and reach the optimiser with the value it would have had from the vector form.
Arguments
lcs: The constraints to merge.
Validation
lcsis non-empty.- Every merged half is written over the same number of variables.
Returns
lc::LinearConstraint: One constraint carrying every row, in input order.
Examples
julia> lc1 = LinearConstraint(; ineq = PartialLinearConstraint(; A = [1.0 0.0], B = [0.5]));julia> lc2 = LinearConstraint(; ineq = PartialLinearConstraint(; A = [0.0 1.0], B = [0.25]));julia> PortfolioOptimisers.merge_linear_constraints([lc1, lc2])LinearConstraint ineq ┼ PartialLinearConstraint │ A ┼ 2×2 Matrix{Float64} │ B ┴ Vector{Float64}: [0.5, 0.25] eq ┴ nothingRelated
PortfolioOptimisers.VecLc — Type
abstract type AbstractArray{var"#s936"<:LinearConstraint, 1}Alias for an abstract vector of LinearConstraint elements.
Related
PortfolioOptimisers.Lc_VecLc — Type
const Lc_VecLc = Union{<:LinearConstraint, <:VecLc}Alias for a union of a single LinearConstraint or a vector of them.
Related
PortfolioOptimisers.LinearConstraintEstimator — Type
struct LinearConstraintEstimator{__T_val, __T_key} <: AbstractConstraintEstimatorHolds the linear constraint equations to parse, and the universe key their names resolve against.
linear_constraints parses val and assembles the coefficient matrices of a LinearConstraint from it.
Fields
val: Constraint equation(s) to parse.
key: Key to specify the universe insets.dictthat names resolve against. Ifnothing, the key is taken fromsets.xkey— or, where the caller is written against another declared axis, from that axis' key.
Constructors
LinearConstraintEstimator(; val::EqnType, key::Option{<:AbstractString} = nothing) -> LinearConstraintEstimatorKeywords correspond to the struct's fields.
Validation
!isempty(val).
Examples
julia> lce = LinearConstraintEstimator(; val = ["w_A + w_B == 1", "w_A >= 0.1"]);julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["w_A", "w_B"]));julia> linear_constraints(lce, sets)LinearConstraint ineq ┼ PartialLinearConstraint │ A ┼ 1×2 LinearAlgebra.Transpose{Float64, Matrix{Float64}} │ B ┴ Vector{Float64}: [-0.1] eq ┼ PartialLinearConstraint │ A ┼ 1×2 LinearAlgebra.Transpose{Float64, Matrix{Float64}} │ B ┴ Vector{Float64}: [1.0]Related
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1.
PortfolioOptimisers.LcE_Lc — Type
const LcE_Lc = Union{<:LinearConstraintEstimator, <:LinearConstraint}Alias for a union of linear constraint estimator and linear constraint types.
Related
PortfolioOptimisers.VecLcE — Type
abstract type AbstractArray{var"#s936"<:LinearConstraintEstimator, 1}Alias for an abstract vector of LinearConstraintEstimator elements.
Related
PortfolioOptimisers.LcE_VecLcE — Type
const LcE_VecLcE = Union{<:LinearConstraintEstimator, <:VecLcE}Alias for a union of a single LinearConstraintEstimator or a vector of them.
Related
PortfolioOptimisers.VecLcE_Lc — Type
abstract type AbstractArray{var"#s936"<:(Union{var"#s936", var"#s935"} where {var"#s936"<:LinearConstraintEstimator, var"#s935"<:LinearConstraint}), 1}Alias for an abstract vector of LcE_Lc elements.
Related
PortfolioOptimisers.ParsingResult — Type
struct ParsingResult{__T_vars, __T_coef, __T_op, __T_rhs, __T_eqn} <: AbstractParsingResultStructured result for standard linear constraint equation parsing.
It is the canonical output of parse_equation for standard linear constraints, and it carries everything get_linear_constraints needs to assemble a row: the variable names, their coefficients, the comparison operator, the right-hand side value, and a formatted equation string.
Fields
vars: Variable names in the parsed constraint expression.
coef: Coefficients corresponding to the constraint variables.
op: Comparison operator (==,<=, or>=).
rhs: Right-hand side value of the constraint.
eqn: Formatted string representation of the constraint equation.
Constructors
ParsingResult( vars::VecStr, coef::VecNum, op::AbstractString, rhs::Number, eqn::AbstractString) -> ParsingResultPositional arguments correspond to the struct's fields. There is no keyword constructor, because parse_equation is the producer of this type.
Validation
length(vars) == length(coef).
Related
PortfolioOptimisers.VecPR — Type
abstract type AbstractArray{var"#s936"<:ParsingResult, 1}Alias for an abstract vector of ParsingResult elements.
Related
PortfolioOptimisers.PR_VecPR — Type
const PR_VecPR = Union{<:ParsingResult, <:VecPR}Alias for a union of a single ParsingResult or a vector of them.
Related
PortfolioOptimisers.replace_group_by_assets — Function
replace_group_by_assets(res::PR_VecPR,
sets::UniverseSets; bl_flag::Bool = false, ep_flag::Bool = false,
rho_flag::Bool = false)If res is a vector of ParsingResult objects, this function will be applied to each element of the vector.
Expand group or special variable references in a ParsingResult to their corresponding asset names.
This function takes a ParsingResult containing variable names (which may include group names, prior(...) expressions, or correlation views like (A, B)), and replaces these with the actual asset names from the provided UniverseSets. It supports Black-Litterman-style group expansion, entropy pooling prior views, and correlation view parsing for advanced constraint generation.
Arguments
res: AParsingResultobject containing variables and coefficients to be expanded.sets: AUniverseSetsobject specifying the asset universe and groupings.bl_flag: Iftrue, enables Black-Litterman-style group expansion.ep_flag: Iftrue, enables expansion ofprior(...)expressions for entropy pooling.rho_flag: Iftrue, enables expansion of correlation views(A, B)for entropy pooling.
Validation
bl_flagcan only betrueif bothep_flagandrho_flagarefalse.rho_flagcan only betrueifep_flagis alsotrue.
Details
- Group names in
res.varsare replaced by the corresponding asset names fromsets.dict. - If
bl_flagistrue, coefficients for group references are divided equally among the assets in the group. - If
ep_flagistrue, expandsprior(asset)orprior(group)expressions for entropy pooling. - If
rho_flagistrue, expands correlation view expressions(A, B)orprior(A, B)for entropy pooling, mapping them to asset pairs. - If a variable or group is not found in
sets.dict, it is skipped.
Returns
res::ParsingResult: A newParsingResultwith all group and special variable references expanded to asset names.
Examples
julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"], "group1" => ["A", "B"]));julia> res = parse_equation("group1 + 2C == 1")ParsingResult vars ┼ Vector{String}: ["C", "group1"] coef ┼ Vector{Float64}: [2.0, 1.0] op ┼ String: "==" rhs ┼ Float64: 1.0 eqn ┴ SubString{String}: "2.0*C + group1 == 1.0"julia> replace_group_by_assets(res, sets)ParsingResult vars ┼ Vector{String}: ["C", "A", "B"] coef ┼ Vector{Float64}: [2.0, 1.0, 1.0] op ┼ String: "==" rhs ┼ Float64: 1.0 eqn ┴ String: "2.0*C + 1.0*A + 1.0*B == 1.0"Related
PortfolioOptimisers.estimator_to_val — Function
estimator_to_val(dict::MultiEstValType, sets::UniverseSets,
val::Option{<:Number} = nothing,
key::Option{<:AbstractString} = nothing;
datatype::DataType = Float64, strict::Bool = false)
estimator_to_val(dict::PairStrNum, sets::UniverseSets,
val::Option{<:Number} = nothing,
key::Option{<:AbstractString} = nothing;
datatype::DataType = Float64, strict::Bool = false)Return value for assets or groups, based on a mapping and asset sets.
The function creates the vector and sets the values for assets or groups as specified by dict, using the asset universe and groupings in sets. If a key in dict is not found in the asset sets, the function either throws an error or issues a warning, depending on the strict flag.
Arguments
dict: A dictionary, vector of pairs, or single pair mapping asset or group names to values.sets: TheUniverseSetscontaining the asset universe and group definitions.val: The value assigned to every asset beforedictis applied.nothingmeanszero(datatype).key: (Optional) Key in theUniverseSetsto specify the asset universe for constraint generation. When provided, takes precedence overkeyfield ofUniverseSets.datatype: Element type of the value the array is filled with whenvalisnothing.strict: Iftrue, throws an error if a key indictis not found in the asset sets; iffalse, issues a warning.
Details
- Iterates over the (key, value) pairs in
dict.
If the same asset is found in subsequent iterations, its value will be overwritten in favour of the most recent one. To ensure determinism, use an OrderedDict or a vector of pairs.
- If a key in
dictmatches an asset in the universe, the corresponding entry inarris set to the specified value. - If a key matches a group in
sets, all assets in the group are set to the specified value usingname_to_val!. - If a key is not found and
strictistrue, anArgumentErroris thrown; otherwise, a warning is issued. - The array is allocated by this method and filled in-place, one key at a time.
Returns
arr::VecNum: Value array.
Related
estimator_to_val(val::Option{<:Number}, args...; kwargs...)Fallback no-op for value mapping in asset/group estimators.
This method returns the input value val as-is, without modification or mapping. It serves as a fallback for cases where the input is already a numeric value, a vector of numeric values, or nothing, and no further processing is required.
Arguments
val: A value of typeNothingor a single numeric value.args...: Additional positional arguments (ignored).kwargs...: Additional keyword arguments (ignored).
Returns
val::Option{<:Number}: The inputval, unchanged.
Related
estimator_to_val(val::VecNum, sets::UniverseSets, ::Any = nothing,
key::Option{<:AbstractString} = nothing; kwargs...)Return a numeric vector for asset/group estimators, validating length against asset universe.
This method checks that the input vector val matches the length of the asset universe in sets, and returns it unchanged if valid. It is used as a fast path for workflows where the value vector is already constructed and requires only defensive validation.
Arguments
val: Numeric vector to be mapped to assets/groups.sets:UniverseSetscontaining the asset universe and group definitions.::Any: Fill value for API consistency (ignored).key: (Optional) Key in theUniverseSetsto specify the asset universe for constraint generation. When provided, takes precedence overkeyfield ofUniverseSets.kwargs...: Additional keyword arguments (ignored).
Validation
length(val) == length(sets.dict[ifelse(isnothing(key), sets.xkey, key)].
Returns
val::VecNum: The input vector, unchanged.
Related
estimator_to_val(val::MatNum, sets::UniverseSets, ::Any = nothing,
key::Option{<:AbstractString} = nothing; dims::Int = 2, kwargs...)Return a numeric matrix for asset/group estimators, validating length against asset universe.
This method checks that size of dims of the input matrix val matches the length of the asset universe in sets, and returns it unchanged if valid. It is used as a fast path for workflows where the value vector is already constructed and requires only defensive validation.
Arguments
val: Numeric matrix to be mapped to assets/groups.sets:UniverseSetscontaining the asset universe and group definitions.::Any: Fill value for API consistency (ignored).key: (Optional) Key in theUniverseSetsto specify the asset universe for constraint generation. When provided, takes precedence overkeyfield ofUniverseSets.dims: Dimension along which to validate the matrix size.kwargs...: Additional keyword arguments (ignored).
Validation
size(val, dims) == length(sets.dict[ifelse(isnothing(key), sets.xkey, key)].
Returns
val::VecNum: The input vector, unchanged.
Related
estimator_to_val(
::UniformValues,
sets::UniverseSets;
...
) -> Any
estimator_to_val(
::UniformValues,
sets::UniverseSets,
;
...
) -> Any
estimator_to_val(
::UniformValues,
sets::UniverseSets,
,
key::Union{Nothing, AbstractString};
datatype,
kwargs...
) -> Any
Return a uniform value vector for all assets in the universe defined by sets.
Each entry equals $1/N$ where $N$ is the number of assets.
Related
PortfolioOptimisers.parse_equation — Function
parse_equation(eqn::EqnType;
ops1::Tuple = ("==", "<=", ">="), ops2::Tuple = (:call, :(==), :(<=), :(>=)),
datatype::DataType = Float64, kwargs...)Parse a linear constraint equation from a string into a structured ParsingResult.
Arguments
eqn: The equation string to parse.eqn::AbstractVector: Each element needs to meet the criteria below.eqn::AbstractString: Must contain exactly one comparison operator fromops1.ops1: Tuple of valid comparison operators as strings.
eqn::Expr: Must contain exactly one comparison operator fromops1.ops2: Tuple of valid comparison operator expressions.
datatype: The numeric type to use for coefficients and right-hand side.kwargs...: Additional keyword arguments, ignored.
Validation
- The equation must contain exactly one valid comparison operator from
ops1. - Both sides of the equation must be valid Julia expressions.
Details
If
eqn::AbstractVector, the function is applied element-wise.The function first checks for invalid operator patterns (e.g.,
"++").It searches for the first occurrence of a valid comparison operator from
ops1in the equation string. Errors if there are more than one or none.The equation is split into left- and right-hand sides using the detected operator.
If
eqn::AbstractString:- Both sides are parsed into Julia expressions using
Meta.parse.
- Both sides are parsed into Julia expressions using
If
eqn::Expr:- Expression is ready as is.
Numeric functions and constants (e.g.,
Inf) are recursively evaluated.All terms are moved to the left-hand side and collected, separating coefficients and variables.
The constant term is moved to the right-hand side, and the equation is formatted for display.
The result is returned as a
ParsingResultcontaining the collected information.
Returns
If
eqn::Str_Expr:res::ParsingResult: Structured parsing result.
If
eqn::AbstractVector:res::Vector{ParsingResult}: Vector of structured parsing results.
Examples
julia> parse_equation("w_A + 2w_B <= 1")ParsingResult vars ┼ Vector{String}: ["w_A", "w_B"] coef ┼ Vector{Float64}: [1.0, 2.0] op ┼ String: "<=" rhs ┼ Float64: 1.0 eqn ┴ SubString{String}: "w_A + 2.0*w_B <= 1.0"Related
PortfolioOptimisers.linear_constraints — Function
linear_constraints(lcs::Option{<:LinearConstraint}, args...; kwargs...)
linear_constraints(lcs::AbstractVector{<:LinearConstraint}, ::Nothing, args...; kwargs...)No-op fallback for returning an existing LinearConstraint object, nothing, or a vector of them.
This method is used to pass through an already constructed LinearConstraint object or nothing without modification. It enables composability and uniform interface handling in constraint generation workflows, allowing functions to accept either raw equations or pre-built constraint objects.
The vector arity is narrowed to a nothing universe on purpose. A vector needs no UniverseSets precisely because every element is already assembled, and that is the shape a Pipeline hands an optimiser when more than one constraint step ran; with a real UniverseSets the broader vector methods take over and map this one over the elements.
Arguments
lcs: An existingLinearConstraintobject,nothing, or a vector of constraints.args...: Additional positional arguments (ignored).kwargs...: Additional keyword arguments (ignored).
Returns
lcs: The input, unchanged.
Related
linear_constraints(eqn::EqnType,
sets::UniverseSets; ops1::Tuple = ("==", "<=", ">="),
key::Option{<:AbstractString} = nothing;
ops2::Tuple = (:call, :(==), :(<=), :(>=)), datatype::DataType = Float64,
strict::Bool = false, bl_flag::Bool = false)Parse and convert one or more linear constraint equations into a LinearConstraint object.
This function parses one or more constraint equations (as strings, expressions, or vectors thereof), replaces group or asset references using the provided UniverseSets, and constructs the corresponding constraint matrices. The result is a LinearConstraint object containing both equality and inequality constraints, suitable for use in portfolio optimisation routines.
Arguments
eqn: A single constraint equation (asAbstractStringorExpr), or a vector of such equations.sets: AUniverseSetsobject specifying the asset universe and groupings.ops1: Tuple of valid comparison operators as strings.ops2: Tuple of valid comparison operators as expression heads.datatype: Numeric type for coefficients and right-hand side.strict: Iftrue, throws an error if a variable or group is not found insets; iffalse, issues a warning.bl_flag: Iftrue, enables Black-Litterman-style group expansion.
Details
- Each equation is parsed using
parse_equation, supporting both string and expression input. - Asset and group references in the equations are expanded using
replace_group_by_assetsand the providedsets. - The function separates equality and inequality constraints, assembling the corresponding matrices and right-hand side vectors.
- Input validation is performed using
@argcheckto ensure non-empty and consistent constraints. - Returns
nothingif no valid constraints are found after parsing and expansion.
Returns
lcs::LinearConstraint: An object containing the assembled equality and inequality constraints, ornothingif no constraints are present.
Examples
julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["w_A", "w_B", "w_C"]));julia> linear_constraints(["w_A + w_B == 1", "w_A >= 0.1"], sets)LinearConstraint ineq ┼ PartialLinearConstraint │ A ┼ 1×3 LinearAlgebra.Transpose{Float64, Matrix{Float64}} │ B ┴ Vector{Float64}: [-0.1] eq ┼ PartialLinearConstraint │ A ┼ 1×3 LinearAlgebra.Transpose{Float64, Matrix{Float64}} │ B ┴ Vector{Float64}: [1.0]Related
linear_constraints(lcs::LcE_VecLcE,
sets::UniverseSets; datatype::DataType = Float64, strict::Bool = false,
bl_flag::Bool = false)If lcs is a vector of LinearConstraintEstimator objects, this function is broadcast over the vector.
This method is a wrapper calling:
linear_constraints(lcs.val, sets, lcs.key; datatype = datatype, strict = strict, bl_flag = bl_flag)It is used for type stability and to provide a uniform interface for processing constraint estimators, as well as simplifying the use of multiple estimators simultaneously.
The loadings are accepted and dropped
rr is accepted so that a caller holding loadings — processed_jump_optimiser_attributes does — can pass them uniformly to whatever sits in lcse, without inspecting its type first. A bare LinearConstraintEstimator drops them: the asset frame is the absence of a re-basis, and an estimator that quietly re-based itself because loadings happened to be available would make the space depend on the prior rather than on what the user wrote. A re-basis is asked for by wrapping in an ExposureConstraintEstimator and by nothing else.
rd rides along for the same reason and is dropped for a stronger one: only a space can ask for a refit, and a bare estimator has no space.
Related
linear_constraints(lcs::ExposureConstraintEstimator, sets::UniverseSets;
datatype::DataType = Float64, strict::Bool = false,
bl_flag::Bool = false,
rr::Option{<:AbstractRegressionResult} = nothing,
rd::Option{<:ReturnsResult} = nothing)Generate the asset-space constraint a re-based one is equivalent to.
Validates the space's basis once via constraint_space_basis, then re-bases the wrapped shape. What comes back is an ordinary LinearConstraint — or a vector of them, when a vector was wrapped — indistinguishable from one written in asset names, which is why nothing downstream of constraint generation needs to know a re-basis happened.
rd is the returns a space may refit its basis from. It is nothing here, which is the standalone route: a space whose re is an estimator throws rather than refitting, and the message names the fixes. See factor_space_regression.
Related
linear_constraints(lcs::VecEcE_LcE_Lc, sets::UniverseSets; datatype::DataType = Float64,
strict::Bool = false, bl_flag::Bool = false,
rr::Option{<:AbstractRegressionResult} = nothing,
rd::Option{<:ReturnsResult} = nothing)Broadcast over a vector that may mix re-based and asset-space constraints, forwarding the loadings and the returns to each. The narrower VecLcE method still wins for a vector that holds only LinearConstraintEstimators.
Each element resolves its own basis, so a vector may mix a space that reads the prior with one that states or refits its own.
Related
PortfolioOptimisers.get_linear_constraints — Function
get_linear_constraints(lcs::PR_VecPR, sets::UniverseSets,
key::Option{<:AbstractString} = nothing;
datatype::DataType = Float64, strict::Bool = false,
rr::Option{<:AbstractRegressionResult} = nothing)Convert parsed linear constraint equations into a LinearConstraint object.
get_linear_constraints takes one or more ParsingResult objects (as produced by parse_equation), expands variable names using the provided UniverseSets, and assembles the corresponding constraint matrices and right-hand side vectors. The result is a LinearConstraint object containing both equality and inequality constraints, suitable for use in portfolio optimisation routines.
Arguments
lcs: A singleParsingResultor a vector of such objects, representing parsed constraint equations.sets: AUniverseSetsobject specifying the universes and groupings.key: Key naming the universe the variables resolve against. Defaults tosets.xkey; a re-based constraint passessets.fkey.datatype: Numeric type for coefficients and right-hand side.strict: Iftrue, throws an error if a variable or group is not found insets; iffalse, issues a warning.rr: Loadings to re-base through, ornothingfor an ordinary asset-space constraint. SeeExposureConstraintEstimator— callers do not pass this directly.
Details
- For each constraint, variable names are matched to the universe stored under
keyinsets. - Coefficient vectors are assembled for each constraint, with entries corresponding to the order of assets in
sets. - When
rris supplied, each matched term is projected throughrr.Mas it is accumulated, so the assembled row is asset-length and what the function returns is an ordinary asset-spaceLinearConstraint. - Constraints are separated into equality (
==) and inequality (<=,>=) types. - The function validates that all constraints reference valid assets or groups, using
@argcheckfor defensive programming. - Returns
nothingif no valid constraints are found after processing.
Returns
lcs::LinearConstraint: An object containing the assembled equality and inequality constraints, ornothingif no constraints are present.
Related
PortfolioOptimisers.prefixed_sets_keys — Function
prefixed_sets_keys(
dict::AbstractDict,
prefix::AbstractString
) -> Vector{String}
Collect the dict keys that start with prefix, as the candidate pool of a suggest_declared_key suggestion inside UniverseSets.
A missing partition key is reported by the group that asked for it, so the whole key set is the wrong pool: the nearest neighbour of nx_sector in Dict("ux_sector" => …) is ux_sector, the very key under validation, and the caller would be told to rename the one thing that is correct. Narrowing the pool to the prefix the missing key must carry leaves only keys that could genuinely have been meant.
Arguments
dict: TheUniverseSetsdictionary being validated.prefix: The axis prefix the missing key must carry,xkeyorfkey.
Returns
candidates::Vector{String}: The keys ofdictthat start withprefix.
Related
PortfolioOptimisers.unclaimed_sets_keys — Function
unclaimed_sets_keys(
dict::AbstractDict,
claimed
) -> Vector{String}
Collect the dict keys that no axis in claimed has taken, as the candidate pool of the missing-xkey suggestion inside UniverseSets.
The counterpart of prefixed_sets_keys for the one key with no prefix of its own. The asset universe is whichever key holds the asset names, so it cannot be found by a prefix; what can be ruled out is every key another declared axis already speaks for. Without that, a dict carrying only a feature axis answers a mistyped xkey with the feature key, which is a different axis and never the right fix.
Arguments
dict: TheUniverseSetsdictionary being validated.claimed: The other declared axis prefixes,uxkey,fkey,ufkeyandzkey.
Returns
candidates::Vector{String}: The keys ofdictthat start with no entry ofclaimed.
Related
PortfolioOptimisers.universe_axis — Function
universe_axis(sets::UniverseSets, key::AbstractString) -> StringName of the axis the universe stored under key belongs to, read off the key itself: "factor" for anything carrying the fkey prefix, "asset" otherwise. It exists only so unknown_variable_msg and empty_row_msg can name the axis the user wrote in.
The key is the evidence, for both callers, and the reason is that both resolve names against sets.dict[key] and nothing else: whatever axis that universe belongs to is the axis a failed lookup failed on. get_black_litterman_views takes the key from the estimator that owns the views, and get_linear_constraints from the constraint space — FactorSpace resolving at sets.fkey. Reading it off the re-basis instead would be a second encoding of the same fact, and a worse one: a wrapped estimator carrying its own key overrides the space's, so a re-based row can legitimately resolve against a universe the loadings are not written in, and the message must name the universe that was searched.
The prefix rather than equality is what makes a factor group key ("nf_sector") resolve as the factor axis too, and the disjoint-prefix rule UniverseSets enforces at construction is what makes that unambiguous.
Related
PortfolioOptimisers.constraint_row_length — Function
constraint_row_length(rr, nx::VecStr) -> IntLength of the assembled constraint row. Without a re-basis this is the size of the universe the names resolve against; with one it is the number of assets the loadings project onto, because the projection is applied while the row is assembled and what leaves is an ordinary asset-space row.
Related
PortfolioOptimisers.constraint_row_term — Function
constraint_row_term(rr, Ai, c)Contribution of one matched variable to a constraint row.
Without a re-basis this is the indicator Ai scaled by the coefficient c. With one it is the corresponding columns of the loadings, summed and scaled — which is the identity
\[\boldsymbol{a}^\intercal \boldsymbol{w}_f = \boldsymbol{a}^\intercal \mathbf{M}^\intercal \boldsymbol{w}_a = (\mathbf{M} \boldsymbol{a})^\intercal \boldsymbol{w}_a\]
applied one term at a time. The columns are summed rather than indexed by findfirst, so a factor universe carrying a duplicated name contributes every column bearing it, matching how the asset path treats a duplicated asset name.
rr.M is used, never rr.L: M's columns are the named original factors, and a constraint must be written in names a user can put in an equation. Risk decomposition wants L and is right to; see ADR 0047.
Related
PortfolioOptimisers.AbstractParsingResult — Type
abstract type AbstractParsingResult <: AbstractConstraintResultAbstract supertype for all equation parsing result types.
All concrete and/or abstract types representing parsing results should be subtypes of AbstractParsingResult.
Related
PortfolioOptimisers.name_to_val! — Function
name_to_val!(nx::VecStr, sdict::AbstractDict, key::Any, val::Number,
arr::VecNum, strict::Bool, nxkey::AbstractString)Set values in a vector for the asset or the group of assets that key names.
name_to_val! resolves key through resolve_axis_name — an asset name resolves to itself, a group name expands to its members — maps the result to indices in the asset universe nx, and sets the corresponding entries of arr to val. If key names neither, the function either throws an error or issues a warning, depending on the strict flag.
Arguments
nx: Vector of asset names.sdict: Dictionary mapping group names to vectors of asset names.key: Name of the asset or the group of assets to set values for.val: The value to assign.arr: The array to be modified in-place.strict: Iftrue, throws an error ifkeyresolves to nothing; iffalse, issues a warning.nxkey: Name of the asset-universe key insets.dict(e.g."nx"), used only to name the universe in the diagnostic message — seeunknown_variable_msg/missing_group_assets_msg.
Details
- An asset name takes precedence over a group name of the same spelling.
- Members that miss the universe are dropped and reported once, through
strict_diagnostic. sdictis never modified:resolve_axis_namereturns a copy of the member list.- Diagnostic messages name only the universe size (never the full universe or the input value dictionary), routed through the shared builders in
01_Base.jl.
Returns
nothing. The operation is performed in-place onarr.
Related
PortfolioOptimisers._parse_equation — Function
_parse_equation(lhs, opstr::AbstractString, rhs; datatype::DataType = Float64)Parse and canonicalise a linear constraint equation from Julia expressions.
_parse_equation takes the left-hand side (lhs) and right-hand side (rhs) of a constraint equation, both as Julia expressions, and a comparison operator string (opstr). It evaluates numeric functions, moves all terms to the left-hand side, collects coefficients and variables, and returns a ParsingResult with the canonicalised equation.
Arguments
lhs: Left-hand side of the equation as a Julia expression.opstr: Comparison operator as a string.rhs: Right-hand side of the equation as a Julia expression.datatype: Numeric type for coefficients and right-hand side.
Details
- Recursively evaluates numeric functions and constants (e.g.,
Inf) on both sides. - Moves all terms to the left-hand side (
lhs - rhs == 0). - Collects and sums like terms, separating variables and constants.
- Moves the constant term to the right-hand side, variables to the left.
- Formats the simplified equation as a string.
- Returns a
ParsingResultcontaining variable names, coefficients, operator, right-hand side value, and formatted equation.
Returns
res::ParsingResult: Structured result with canonicalised variables, coefficients, operator, right-hand side, and formatted equation.
Related
PortfolioOptimisers.rethrow_parse_error — Function
rethrow_parse_error(expr; side = :lhs)Internal utility for error handling during equation parsing.
rethrow_parse_error is used to detect and handle incomplete or invalid expressions encountered while parsing constraint equations. It is called on both sides of an equation during parsing to ensure that the expressions are valid and complete. If an incomplete expression is detected, a Meta.ParseError is thrown; otherwise, the function returns nothing.
Arguments
expr: The parsed Julia expression to check. Can be anExpr,Nothing, or any other type.side: Symbol indicating which side of the equation is being checked (:lhsor:rhs). Used for error messages.
Details
- If
exprisNothing(the side is empty, e.g. a truncated equation string), aMeta.ParseErroris thrown — the parser fails closed rather than assuming zero, because a silently-assumed zero constraint is one the author never wrote. Callers who mean zero must write it explicitly. - If
expris an incomplete expression (expr.head == :incomplete), aMeta.ParseErroris thrown with a descriptive message. - For all other cases, the function returns
nothingand does not modify the input.
Validation
- Throws a
Meta.ParseErrorif the expression is empty or incomplete.
Returns
nothing.
Related
PortfolioOptimisers.format_term — Function
format_term(coeff, var)Format a single term in a linear constraint equation as a string.
format_term takes a coefficient and a variable name and returns a string representation suitable for display in a canonicalised linear constraint equation. Handles special cases for coefficients of 1 and -1 to avoid redundant notation.
Arguments
coeff: Numeric coefficient for the variable.var: Variable name as a string.
Details
- If
coeff == 1, returns"$var"(no explicit coefficient). - If
coeff == -1, returns"-$(var)"(no explicit coefficient). - Otherwise, returns
"$(coeff)*$(var)".
Returns
term_str::String: The formatted term as a string.
Related
PortfolioOptimisers.collect_terms! — Function
collect_terms!(expr, coeff, terms)Recursively collect and expand terms from a Julia expression for linear constraint parsing.
collect_terms! traverses a Julia expression tree representing a linear equation, expanding and collecting all terms into a vector of (coefficient, variable) pairs. It handles numeric constants, variables, and arithmetic operations (+, -, *, /), supporting canonicalisation of linear constraint equations for further processing.
Arguments
expr: The Julia expression to traverse.coeff: The current numeric coefficient to apply.terms: A vector to which(coefficient, variable)pairs are appended in-place. Each pair is of the form(Float64, Option{<:String}), whereNothingindicates a constant term.
Details
expr:Number: Appends(coeff * oftype(coeff, expr), nothing)toterms.Symbol: Appends(coeff, string(expr))toterms.Expr:- For multiplication (
*), distributes the coefficient to the numeric part. - For division (
/), divides the coefficient by the numeric denominator. - For addition (
+), recursively collects terms from all arguments. - For subtraction (
-), recursively collects terms from all arguments except the last, which is negated. - For all other expressions, treats as a variable and appends as
(coeff, string(expr)).
- For multiplication (
Returns
nothing. The function modifiestermsin-place.
Related
PortfolioOptimisers._collect_terms — Function
_collect_terms(expr::Union{Symbol, Expr, <:Number})Expand and collect all terms from a Julia expression representing a linear constraint equation.
_collect_terms takes a Julia expression (such as the left-hand side of a constraint equation), recursively traverses its structure, and returns a vector of (coefficient, variable) pairs. It supports numeric constants, variables, and arithmetic operations (+, -, *, /), and is used to canonicalise linear constraint equations for further processing.
Arguments
expr: The Julia expression to expand.
Details
- Calls
collect_terms!internally with an initial coefficient of1.0and an empty vector. - Numeric constants are collected as
(coefficient, nothing). - Variables are collected as
(coefficient, variable_name). - Arithmetic expressions are recursively expanded and collected.
Returns
terms::Vector{Tuple{Float64, Option{<:String}}}: A vector of(coefficient, variable)pairs, wherevariableis a string for variable terms ornothingfor constant terms.
Related
PortfolioOptimisers.allowed_functions — Constant
allowed_functions = Dict{Symbol, Function}(:+ => +, :- => -, :* => *, :/ => /,
:^ => ^, :sqrt => sqrt, :cbrt => cbrt,
:exp => exp, :exp2 => exp2, :exp10 => exp10,
:log => log, :log2 => log2, :log10 => log10,
:abs => abs, :min => min, :max => max)Enumerated table of the functions permitted in equation parsing, mapping each allowed name directly to its function object. Evaluating constraint/view strings crosses a trust boundary (config files, spreadsheets, UI), so the parser must be able to call only these 16 mathematical functions. Using an explicit Symbol => Function table — rather than resolving a name against Base with getfield(Base, fname) — bounds that capability to exactly this table: a name absent from the keys fails closed with a Meta.ParseError, and the set of callable functions cannot drift from the set of allowed names, because they are the same list. See docs/adr/0025-enumerated-parser-allowlist.md.
The prior(...) marker is deliberately absent from this table: it names assets/groups (not numbers) and is expanded structurally by eval_numeric_functions/replace_group_by_assets, never evaluated numerically.
PortfolioOptimisers.eval_numeric_functions — Function
eval_numeric_functions(expr, datatype::DataType = Float64)Recursively evaluate numeric functions and constants in a Julia expression.
eval_numeric_functions traverses a Julia expression tree and evaluates any sub-expressions that are purely numeric, including standard mathematical functions and constants (such as Inf). This is used to simplify constraint equations before further parsing and canonicalisation.
When an allowlisted function is actually evaluated (all its arguments are numeric), its arguments are coerced to datatype (a float type) first, so the arithmetic happens in the same numeric domain the optimiser will use rather than in machine Int64. This prevents integer literals from combining and wrapping — e.g. 2^64 yields 1.8446744073709552e19 rather than silently wrapping to 0, and 2^-1 yields 0.5 rather than a DomainError. Numeric literals that survive inside an unevaluated (nonlinear) subexpression are left untouched, so 2^z still renders as 2 ^ z.
Only the functions enumerated in allowed_functions may be evaluated; any other call head fails closed with a Meta.ParseError. The prior(...) marker is handled structurally (see replace_group_by_assets) and throws a Meta.ParseError if given purely numeric arguments.
Arguments
expr: The Julia expression to evaluate. Can be aNumber,Symbol, orExpr.datatype: Float type into which numeric arguments are coerced before an allowlisted function is evaluated.
Details
expr:Number: It is returned as-is.:Inf: ReturnsInf.Expr: Representing a function call whose arguments are all numeric, the allowlisted function is evaluated (on arguments coerced todatatype) and replaced with its result.- Otherwise, the function recurses into sub-expressions, returning a new expression with numeric parts evaluated.
Returns
- The evaluated expression, with all numeric sub-expressions replaced by their computed values. Non-numeric or symbolic expressions are returned in their original or partially simplified form.
Related
PortfolioOptimisers.has_invalid_plus — Function
has_invalid_plus(expr)Check whether a Julia expression contains an invalid + operator in a constraint context.
Internal helper used during linear constraint parsing to detect unsupported + operator usage in constraint expressions.
Arguments
expr: Julia expression to check.
Returns
Bool:trueif the expression contains an invalid+,falseotherwise.
PortfolioOptimisers.port_opt_view — Method
port_opt_view(
sets::UniverseSets,
i,
args...
) -> UniverseSets{var"#s179", var"#s1791", var"#s1792", var"#s1793", var"#s1794", <:AbstractDict{var"#s934", var"#s933"}} where {var"#s179"<:AbstractString, var"#s1791"<:AbstractString, var"#s1792"<:AbstractString, var"#s1793"<:AbstractString, var"#s1794"<:AbstractString, var"#s934"<:AbstractString, var"#s933"}
Return a view of a UniverseSets restricted to the assets at index i.
Slices all xkey-prefixed groups by i, and derives unique-entry uxkey-prefixed groups from the corresponding sliced xkey group.
The factor axis is left alone
fkey- and ufkey-prefixed entries come back bit-identical: an asset index has no meaning on the factor axis. Declaring the axis is what makes that exemption a property of the data — before it, a factor-flavoured sets sitting in a @vprop field was sliced by asset indices and failed with a length mismatch, and the only defence was omitting the annotation by hand, per field.
There is deliberately no factor-index arity. port_opt_view(rd, i, j, k) can slice rd.nf, but no internal caller passes a non-colon k; a user who slices factors updates their sets themselves.
The feature axis is left alone too, for a different reason
zkey's entry also comes back bit-identical, but not because an asset index is meaningless on it — some of its nodes are assets. It is left alone because the axis is declared rather than derived: the caller wrote the node list down, so it is the program's coordinate system and not a summary of the current universe. That is what makes size(Z, 2) fold-invariant for a graded asset_sets_features program — exactly the opposite of the group-name-key path, where the viewed producer rebuilds the axis from the viewed taxonomy and a group with no members left simply disappears.
The consequence is accepted and documented rather than filtered: an asset node whose asset the view dropped survives as an all-zero column.
Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(lc::LinearConstraint, i, args...) -> LinearConstraintReturn a precomputed LinearConstraint unchanged under an asset sub-selection.
The identity is deliberate, and it is not the claim that a full-universe row means the same thing over a subset — it does not. It is what the lcse slot already did: the slot was passed unviewed until a constraint space gained a basis a view has to follow, and slicing A here would change the behaviour of a path this method exists only to leave alone. A NestedClustered inner solve refuses a bare precomputed constraint outright for exactly this reason; Stacking and SubsetResampling carry no such guard, and that gap pre-dates the view.
A constraint reaching a meta-optimiser through an ExposureConstraintEstimator is a different case and is handled: its A is factor-width and is re-projected against the viewed prior's loadings, so the view it needs is of the basis, not of the row.
Related
PortfolioOptimisers.factor_universe — Function
factor_universe(sets::UniverseSets, K::Integer, need::AbstractString,
source::AbstractString) -> VecStrRead the declared factor universe, sets.dict[sets.fkey], checking that it exists and that it agrees with source — the observations × factors matrix whose K columns it must name — on how many factors there are.
The factor axis is optional on UniverseSets but is not optional for a consumer written against it, so the failure has to be diagnosed at the point of need. Both messages name sets.fkey and the matrix, because the two are what a caller has to reconcile: a user arriving from the pre-declaration shape put the factor names under xkey and would otherwise be told about an asset universe they never wrote in.
need names the consumer ("a FactorSpace constraint"), source the matrix ("rr.M", "F") — so one helper serves every consumer of the axis without any of them re-encoding the checks.
Related
PortfolioOptimisers.feature_universe — Function
feature_universe(sets::UniverseSets, need::AbstractString) -> VecStrRead the declared feature axis, sets.dict[sets.zkey], checking that it exists.
The sibling of factor_universe, written the same way and for the same reason: the axis is optional on UniverseSets but is not optional for a consumer written against it, so the failure is diagnosed at the point of need, by one shared helper whose message names the key and says what to add.
Existence, and nothing to reconcile
The one deliberate difference from factor_universe is that there is no arity to check. factor_universe reconciles the declared axis against the column count of a matrix that already exists — rr.M, F — and a mismatch there means the names and the columns describe different universes. The feature axis has no such matrix, because it defines the width: asset_sets_features allocates assets × length(nz) from this list. So existence and a good message is the whole job.
need names the consumer, as in factor_universe.
Related
PortfolioOptimisers._expr_depth_exceeds — Function
_expr_depth_exceeds(x, limit::Integer) -> BoolReturn true if the expression tree x is deeper than limit.
Guards the Expr form of parse_equation against a deeply nested AST that no string length cap covers. The check itself recurses at most limit + 1 frames deep and short-circuits the moment the limit is breached, so it cannot exhaust the stack it protects.
References
- [4]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).