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, and LinearConstraint states the form of each half. One row of A and the entry of B beside it are one constraint, so a pair holding more bounds than rows, or more rows than bounds, is satisfied by no value of the constrained variable.
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
- [5] 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. The optimiser writes every row scaled and homogenised, as 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 returned solution is de-homogenised, so it satisfies the form below whatever the objective is.
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.
- $\boldsymbol{a}^\intercal$: One row of a coefficient matrix.
- $b$: The entry of a response vector beside that row.
One row and the entry beside it are one constraint. The row runs over the entries of $\boldsymbol{x}$, in the order of the universe the constraint is written against.
The inequality half is defined in the $\leq$ sense, so the sense a row is written in fixes the half that holds it. The row $\boldsymbol{a}^\intercal \boldsymbol{x} = b$ is an equality and belongs to the $\text{eq}$ half. The row $\boldsymbol{a}^\intercal \boldsymbol{x} \leq b$ belongs to the $\text{ineq}$ half as it stands. The row $\boldsymbol{a}^\intercal \boldsymbol{x} \geq b$ is the same constraint as $-\boldsymbol{a}^\intercal \boldsymbol{x} \leq -b$, so it belongs to the $\text{ineq}$ half with both sides negated.
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)).
View parameters
LinearConstraint defines its own port_opt_view method rather than deriving one from field tags.
- The method reads the index and drops it. Both halves are carried through unchanged, and
Ais never sliced along the asset axis. - A row is written over the whole universe it was assembled against, so slicing
Awould change what the row asserts.port_opt_viewstates why the identity is the behaviour this slot needs.
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
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equation 9.1.
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
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1.
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).
Examples
julia> PortfolioOptimisers.ParsingResult(["w_A", "w_B"], [1.0, 2.0], "<=", 1.0, "w_A + 2.0*w_B <= 1.0")ParsingResult vars ┼ Vector{String}: ["w_A", "w_B"] coef ┼ Vector{Float64}: [1.0, 2.0] op ┼ String: "<=" rhs ┼ Float64: 1.0 eqn ┴ String: "w_A + 2.0*w_B <= 1.0"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)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. When res is a vector of ParsingResult objects, the function is applied to each element of the vector.
Mathematical definition
\[\begin{align} c\, g &\to \sum_{j=1}^{k} c\, m_j\,, \\ c\, g &\to \sum_{j=1}^{k} \frac{c}{k}\, m_j\,. \end{align}\]
Where:
- $g$: A group name written in the equation.
- $m_j$: The $j$-th member of the group $g$.
- $k$: The number of members of the group $g$.
- $c$: The coefficient the group name carries.
The two lines are different operations. The first repeats the coefficient on every member, so the expanded row constrains the sum over the group. The second divides the coefficient by the member count, so the expanded row constrains the mean over the group. A group of one member is the only case in which the two agree.
Algorithm
- Copy
res.varsandres.coefintovariables_newandcoeffs_new, and open the empty accumulatorsvariables_tmp,coeffs_tmpandidx_rm. - For each variable name of
res.vars, match it against the prior patternprior(...)and against the correlation pattern(a, b). The four combinations of the two matches select steps 3 to 6. - A name matching neither pattern, with
rho_flagfalse, is a plain name. Look it up insets.dict, and leave it where it stands when the dictionary does not hold it, because a name that is not a group is already the name of one column. A group name sheds its departed members withshed_departed_members, then expands to what survived, each member carrying the coefficient the mathematics above gives over the surviving count, and the index of the group joinsidx_rm. A group that shed every member expands to nothing and its index joinsidx_rmall the same. - A name matching the correlation pattern expands to one entry naming the two member lists, and that entry carries the coefficient of the view unchanged. A correlation view is one row over a pair of universes, so no coefficient is spread over members. The two lists shed jointly, so a pair survives only when both of its names did.
- A name matching the prior pattern expands the name inside
prior(...)exactly as step 3 does, and wraps each member back inprior(...). - A name matching both patterns expands as step 4 does, and wraps each of the two member lists in
prior(...). - Return
resunchanged when nothing was struck, so an equation written in asset names costs no allocation. - Delete the entries at
idx_rmfromvariables_newandcoeffs_new, append the two accumulators to them, and render the expanded equation string. - Return the
ParsingResultbuilt from the new names and coefficients, together with the operator and the right-hand side ofres, which the expansion leaves untouched.
Arguments
res: AParsingResultobject containing variables and coefficients to be expanded.sets: AUniverseSetsobject specifying the asset universe and groupings.bl_flag: Selects which of the two expansions above runs.falsetakes the first, which constrains the sum over the group.truetakes the second, the Black-Litterman-style expansion, which constrains the mean.ep_flag: Iftrue, enables expansion ofprior(...)expressions for entropy pooling.rho_flag: Iftrue, enables expansion of correlation views(A, B)for entropy pooling.ledger: The door's ledger of departure casualties, ornothingwhen nobody is collecting. A shed group is recorded into it throughrecord_group_shed!.
Validation
The three flags are not independent, and five guards hold the grammar they describe.
bl_flagcan only betrueif bothep_flagandrho_flagarefalse.rho_flagcan only betrueifep_flagis alsotrue.- The pattern
(a, b)can only be used whenep_flagandrho_flagare bothtrue. - The pattern
prior(a)can only be used whenep_flagistrue. - The pattern
prior(a, b)can only be used whenrho_flagistrue.
Two further guards hold the shape of a correlation view.
- A correlation view is written
(a, b), and a correlation view prior is writtenprior(a, b). - Both sides of a correlation view name a group that
sets.dictholds, and the two groups have the same number of members. A view whose two sides are both absent fromsets.dictis skipped instead of raised on.
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}: ["group1", "C"] coef ┼ Vector{Float64}: [1.0, 2.0] op ┼ String: "==" rhs ┼ Float64: 1.0 eqn ┴ SubString{String}: "group1 + 2.0*C == 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 + A + 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.
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.
Algorithm
- Take
valas the fill value, orzero(datatype)whenvalisnothing. - Take
keyas the universe keynxkey, orsets.xkeywhenkeyisnothing, and read the universenxfromsets.dictunder it. - Allocate
arr, one entry per name ofnx, filled with the value of step 1. - Read the counterpart axis with
counterpart_axis_names, the other of the two asset axesUniverseSetsdeclares. - For each
(key, val)pair ofdict, in the orderdictiterates in, writevalintoarrthroughname_to_val!. A key that names an asset writes one entry, a key that names a group writes one entry per member, a key that names the counterpart axis is skipped in silence, and a key that names none of them is reported through thestrictflag. - Return
arr.
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.
Validation
- A key of
dictthat names neither an asset, nor a group, nor an entry of the counterpart axis raises anArgumentErrorwhenstrictistrue. A warning is issued otherwise.
Returns
arr::VecNum: Value array, one entry per name of the universe.
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.
Algorithm
- Return
val. The method reads none of its other arguments and none of its keywords.
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.
Algorithm
- Take
keyas the universe key, orsets.xkeywhenkeyisnothing, and read the universe fromsets.dictunder it. - Check
valagainst the length of that universe. - Return
val.
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 matrix is already constructed and requires only defensive validation.
Algorithm
- Take
keyas the universe key, orsets.xkeywhenkeyisnothing, and read the universe fromsets.dictunder it. - Check the size of
valalongdimsagainst the length of that universe. - Return
val.
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::MatNum: The input matrix, unchanged.
Related
estimator_to_val(::UniformValues, sets::UniverseSets, ::Any = nothing,
key::Option{<:AbstractString} = nothing;
datatype::DataType = Float64, kwargs...)Return a uniform value vector for all assets in the universe defined by sets.
UniformValues states the closed form the entries take. The value is a range rather than a vector, so no array is allocated.
Algorithm
- Take
keyas the universe key, orsets.xkeywhenkeyisnothing, and read the universe fromsets.dictunder it, giving its lengthN. - Compute
iN, the reciprocal ofNindatatype. - Return the range of length
Nwhose start and stop are bothiN.
Arguments
::UniformValues: The algorithm that selects this method.sets: TheUniverseSetswhose universe givesN.::Any: Fill value for API consistency (ignored).key: (Optional) Key in theUniverseSetsnaming the universe the value is written over. When provided, takes precedence oversets.xkey.datatype: Element type of the returned range.kwargs...: Additional keyword arguments (ignored).
Returns
val::StepRangeLen: A range of lengthN, each entry the reciprocal ofN.
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.
An equation string crosses a trust boundary, so both entry shapes carry a limit from EQUATION_LIMITS[] before any recursive walk runs. The string form is capped on length before Meta.parse runs, and no length applies to the pre-built Expr form. Both forms are then capped on the depth of the expression tree, so one number bounds the recursion whichever shape the input takes. docs/adr/0027-cap-equation-parser-recursion.md owns both limits.
Algorithm
The method that Julia selects is the algorithm, and one method answers each shape of eqn.
eqnis a vector: apply this function to each element, and return the vector of results.eqnis a string: check its length againstEQUATION_LIMITS[].max_length, and refuse the pattern++.- Find the first operator of
ops1that occurs in the string, givingopstr, and split the string on it intolhsandrhs. - Parse both parts with
Meta.parse, givinglexprandrexpr, check each withrethrow_parse_error, and check the depth of each againstEQUATION_LIMITS[].max_depthwith_expr_depth_exceeds. eqnis anExpr: check its depth againstEQUATION_LIMITS[].max_depthwith_expr_depth_exceeds, and refuse a++pattern withhas_invalid_plus.- Check that the head of the expression is a call and is exactly one operator of
ops2, givingopstr, and readlhsandrhsoff the arguments of the call. - Hand
opstrand the two sides to_parse_equation, which canonicalises them and builds theParsingResult.
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
length(eqn) <= EQUATION_LIMITS[].max_length, for the string form. AMeta.ParseErrornaming both lengths is thrown otherwise.- The expression tree of
eqnis no deeper thanEQUATION_LIMITS[].max_depth, for both forms. The string form is checked afterMeta.parse, on each side of the operator. AMeta.ParseErrornaming the limit is thrown otherwise. eqnholds no++pattern.eqnholds exactly one comparison operator, fromops1for the string form and fromops2for theExprform.- The head of the
Exprform is a call. - Neither side of the equation is empty or incomplete, which
rethrow_parse_errorchecks.
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.
Algorithm
- Return
lcs. Neither method reads its further positional arguments or its keywords.
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.
Algorithm
This method is the whole pipeline, and each step names the stage that owns it.
- Parse
eqnwithparse_equation, givinglcs, oneParsingResultper equation. Each result carries the equation in canonical form. - Expand every group name of
lcsinto its members withreplace_group_by_assets, giving results written in names of the universe.bl_flagselects which of the two expansions runs. - Assemble the coefficient matrices and the right-hand sides from
lcswithget_linear_constraints, which resolves each name against the universekeynames and separates the equality rows from the inequality rows. - Return what
get_linear_constraintsgives: aLinearConstraint, ornothingwhen no row survived.
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.key: Key naming the universe the variables resolve against. Defaults tosets.xkey.rr: Loadings to re-base through, ornothingfor an ordinary asset-space constraint.
Validation
- Every stage validates its own input:
parse_equationthe equation text,replace_group_by_assetsthe flag grammar, andget_linear_constraintsthe names against the universe.
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::LinearConstraintEstimator, sets::UniverseSets;
datatype::DataType = Float64, strict::Bool = false,
bl_flag::Bool = false,
rr::Option{<:AbstractLoadingsRegressionResult} = nothing,
rd::Option{<:ReturnsResult} = nothing)
linear_constraints(lcs::VecLcE, sets::UniverseSets;
datatype::DataType = Float64, strict::Bool = false,
bl_flag::Bool = false,
rr::Option{<:AbstractLoadingsRegressionResult} = nothing,
rd::Option{<:ReturnsResult} = nothing)Parse the equations a LinearConstraintEstimator carries, against the universe key that estimator names.
The method reads val and key off the estimator and hands both to the equation method, which gives one uniform interface for a single constraint estimator and for a vector of them. A vector is answered element by element, and the result is a vector of the same length.
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.
Algorithm
- Read
valandkeyofflcs. - Drop
rrandrd, for the reason the paragraph above gives. - Return the
LinearConstraintthat the equation method builds fromval,setsandkey. - Apply steps 1 to 3 to each element, and return the vector of results, when
lcsis a vector.rrandrdreach every element, and every element drops them.
Arguments
lcs: TheLinearConstraintEstimatorto parse, or a vector of them.sets: AUniverseSetsobject specifying the asset universe and groupings.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.rr: Accepted and dropped. A bare estimator never re-bases.rd: Accepted and dropped. A bare estimator never asks for a refit.
Returns
lcs::Option{<:LinearConstraint}: The assembled constraint, ornothingwhen no row survived. A vector input gives one such value per element.
Related
linear_constraints(lcs::ExposureConstraintEstimator, sets::UniverseSets;
datatype::DataType = Float64, strict::Bool = false,
bl_flag::Bool = false,
rr::Option{<:AbstractLoadingsRegressionResult} = 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.
Arguments
lcs: TheExposureConstraintEstimatorwhose rows are re-based.sets: The declared universe, carrying the factor axisrrnames, undersets.tfkeyorsets.cfkey.datatype: Data type of the assembled row.strict: Iftrue, a name the universe does not resolve throws; iffalse, it warns and the term is dropped.bl_flag: Iftrue, enables Black-Litterman-style group expansion.rr: The loadings, when the caller holds them and the space states none.rd: Returns the space may refit from.nothingon this route.
Returns
lc: An asset-spaceLinearConstraint,nothingwhen every row was dropped, or a vector of either when a vector was wrapped.
Related
linear_constraints(lcs::VecEcE_LcE_Lc, sets::UniverseSets; datatype::DataType = Float64,
strict::Bool = false, bl_flag::Bool = false,
rr::Option{<:AbstractLoadingsRegressionResult} = 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.
Arguments
lcs: The vector of shapes, re-based and asset-space mixed.sets: The declared universe the names resolve against.datatype: Data type of the assembled rows.strict: Iftrue, a name the universe does not resolve throws; iffalse, it warns and the term is dropped.bl_flag: Iftrue, enables Black-Litterman-style group expansion.rr: The loadings, forwarded to every element.rd: Returns an element's space may refit from, forwarded to every element.
Returns
lcs: One result per entry of the input, in the order of the input. An entry is aLinearConstraintornothing.
Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(
sets::UniverseSets,
i,
args...
) -> UniverseSets{var"#s185", var"#s1851", var"#s1852", var"#s1853", var"#s1854", var"#s1855", var"#s1856", <:AbstractDict{var"#s1771", var"#s1770"}} where {var"#s185"<:AbstractString, var"#s1851"<:AbstractString, var"#s1852"<:AbstractString, var"#s1853"<:AbstractString, var"#s1854"<:AbstractString, var"#s1855"<:AbstractString, var"#s1856"<:AbstractString, var"#s1771"<:AbstractString, var"#s1770"}
Return a view of a UniverseSets restricted to the assets at index i.
The asset axis is the only axis this view slices, and the other three are exempt for two different reasons. Both factor axes are exempt because an asset index has no meaning on either, and they are treated alike: a cfkey-prefixed entry comes back bit-identical exactly as a tfkey-prefixed one does. Declaring an axis is what makes the exemption a property of the data: before the declaration, 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, field by field. There is deliberately no factor-index arity either. port_opt_view(rd, i, j, k) can slice rd.nf, but no internal caller passes a non-colon k, so a user who slices factors updates their sets themselves.
Algorithm
- Read
xkeyanduxkeyfromsets, and open an empty dictionarydictof the typesets.dicthas. - For an entry of
sets.dictwhose key starts withxkey, takeview(v, i), the group restricted to the selected assets. - For an entry whose key starts with
uxkey, take the unique entries of thexkey-prefixed partition it names, restricted toi. The unique-entry group is therefore derived from the sliced partition and never from the original one. - Skip the
nikeyentry, matched exactly. Only a door mints the Non-Investable Axis, so a view never carries one: a cluster of a nested optimisation would otherwise inherit its parent's departures and charge every one of them again, once per cluster. The match is exact rather than by prefix so that a plain group whose name merely starts withnikey—"nikkei225"under the default"ni"— is not silently dropped with it. - Carry every other entry through unchanged, into the same
dict. Thetfkey-,utfkey-,cfkey- anducfkey-prefixed entries, and every plain group, come back bit-identical. - Return the
UniverseSetsbuilt fromdictand the eight unchanged key prefixes, which revalidates the prefix grammar over the viewed universe.
Arguments
sets: TheUniverseSetsto view.i: The asset index selection.args...: Additional positional arguments (ignored).
Returns
sets::UniverseSets: A newUniverseSetsover the selected assets, declaring the same seven key prefixes as the original.
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.
Algorithm
- Return
lc. The method reads neither the index nor the tail that follows it.
Arguments
lc: The precomputedLinearConstraint.::Any: The asset index selection (ignored).args...: Additional positional arguments (ignored).
Returns
lc::LinearConstraint: The input, unchanged.
Related
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).