Linear Constraints

PortfolioOptimisers.PartialLinearConstraintType
struct PartialLinearConstraint{__T_A, __T_B} <: AbstractConstraintResult

Holds 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) -> PartialLinearConstraint

Keywords correspond to the struct's fields.

Validation

  • !isempty(A).
  • !isempty(B).
  • size(A, 1) == length(B), one row of A per entry of B.

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.
source
PortfolioOptimisers.LinearConstraintType
struct LinearConstraint{__T_ineq, __T_eq} <: AbstractConstraintResult

Holds 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) -> LinearConstraint

Keywords correspond to the struct's fields.

Validation

  • Both eq and ineq cannot be nothing at 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 A is never sliced along the asset axis.
  • A row is written over the whole universe it was assembled against, so slicing A would change what the row asserts. port_opt_view states 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.
source
PortfolioOptimisers.LinearConstraintEstimatorType
struct LinearConstraintEstimator{__T_val, __T_key} <: AbstractConstraintEstimator

Holds 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 in sets.dict that names resolve against. If nothing, the key is taken from sets.xkey — or, where the caller is written against another declared axis, from that axis' key.

Constructors

LinearConstraintEstimator(;    val::EqnType,    key::Option{<:AbstractString} = nothing) -> LinearConstraintEstimator

Keywords 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.
source
PortfolioOptimisers.ParsingResultType
struct ParsingResult{__T_vars, __T_coef, __T_op, __T_rhs, __T_eqn} <: AbstractParsingResult

Structured 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) -> ParsingResult

Positional 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

source
PortfolioOptimisers.replace_group_by_assetsFunction
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

  1. Copy res.vars and res.coef into variables_new and coeffs_new, and open the empty accumulators variables_tmp, coeffs_tmp and idx_rm.
  2. For each variable name of res.vars, match it against the prior pattern prior(...) and against the correlation pattern (a, b). The four combinations of the two matches select steps 3 to 6.
  3. A name matching neither pattern, with rho_flag false, is a plain name. Look it up in sets.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 with shed_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 joins idx_rm. A group that shed every member expands to nothing and its index joins idx_rm all the same.
  4. 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.
  5. A name matching the prior pattern expands the name inside prior(...) exactly as step 3 does, and wraps each member back in prior(...).
  6. A name matching both patterns expands as step 4 does, and wraps each of the two member lists in prior(...).
  7. Return res unchanged when nothing was struck, so an equation written in asset names costs no allocation.
  8. Delete the entries at idx_rm from variables_new and coeffs_new, append the two accumulators to them, and render the expanded equation string.
  9. Return the ParsingResult built from the new names and coefficients, together with the operator and the right-hand side of res, which the expansion leaves untouched.

Arguments

  • res: A ParsingResult object containing variables and coefficients to be expanded.
  • sets: A UniverseSets object specifying the asset universe and groupings.
  • bl_flag: Selects which of the two expansions above runs. false takes the first, which constrains the sum over the group. true takes the second, the Black-Litterman-style expansion, which constrains the mean.
  • ep_flag: If true, enables expansion of prior(...) expressions for entropy pooling.
  • rho_flag: If true, enables expansion of correlation views (A, B) for entropy pooling.
  • ledger: The door's ledger of departure casualties, or nothing when nobody is collecting. A shed group is recorded into it through record_group_shed!.

Validation

The three flags are not independent, and five guards hold the grammar they describe.

  • bl_flag can only be true if both ep_flag and rho_flag are false.
  • rho_flag can only be true if ep_flag is also true.
  • The pattern (a, b) can only be used when ep_flag and rho_flag are both true.
  • The pattern prior(a) can only be used when ep_flag is true.
  • The pattern prior(a, b) can only be used when rho_flag is true.

Two further guards hold the shape of a correlation view.

  • A correlation view is written (a, b), and a correlation view prior is written prior(a, b).
  • Both sides of a correlation view name a group that sets.dict holds, and the two groups have the same number of members. A view whose two sides are both absent from sets.dict is skipped instead of raised on.

Returns

  • res::ParsingResult: A new ParsingResult with 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

source
PortfolioOptimisers.estimator_to_valFunction
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.

Warning

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

  1. Take val as the fill value, or zero(datatype) when val is nothing.
  2. Take key as the universe key nxkey, or sets.xkey when key is nothing, and read the universe nx from sets.dict under it.
  3. Allocate arr, one entry per name of nx, filled with the value of step 1.
  4. Read the counterpart axis with counterpart_axis_names, the other of the two asset axes UniverseSets declares.
  5. For each (key, val) pair of dict, in the order dict iterates in, write val into arr through name_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 the strict flag.
  6. Return arr.

Arguments

  • dict: A dictionary, vector of pairs, or single pair mapping asset or group names to values.
  • sets: The UniverseSets containing the asset universe and group definitions.
  • val: The value assigned to every asset before dict is applied. nothing means zero(datatype).
  • key: (Optional) Key in the UniverseSets to specify the asset universe for constraint generation. When provided, takes precedence over key field of UniverseSets.
  • datatype: Element type of the value the array is filled with when val is nothing.
  • strict: If true, throws an error if a key in dict is not found in the asset sets; if false, issues a warning.

Validation

  • A key of dict that names neither an asset, nor a group, nor an entry of the counterpart axis raises an ArgumentError when strict is true. A warning is issued otherwise.

Returns

  • arr::VecNum: Value array, one entry per name of the universe.

Related

source
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

  1. Return val. The method reads none of its other arguments and none of its keywords.

Arguments

  • val: A value of type Nothing or a single numeric value.
  • args...: Additional positional arguments (ignored).
  • kwargs...: Additional keyword arguments (ignored).

Returns

  • val::Option{<:Number}: The input val, unchanged.

Related

source
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

  1. Take key as the universe key, or sets.xkey when key is nothing, and read the universe from sets.dict under it.
  2. Check val against the length of that universe.
  3. Return val.

Arguments

  • val: Numeric vector to be mapped to assets/groups.
  • sets: UniverseSets containing the asset universe and group definitions.
  • ::Any: Fill value for API consistency (ignored).
  • key: (Optional) Key in the UniverseSets to specify the asset universe for constraint generation. When provided, takes precedence over key field of UniverseSets.
  • 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

source
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

  1. Take key as the universe key, or sets.xkey when key is nothing, and read the universe from sets.dict under it.
  2. Check the size of val along dims against the length of that universe.
  3. Return val.

Arguments

  • val: Numeric matrix to be mapped to assets/groups.
  • sets: UniverseSets containing the asset universe and group definitions.
  • ::Any: Fill value for API consistency (ignored).
  • key: (Optional) Key in the UniverseSets to specify the asset universe for constraint generation. When provided, takes precedence over key field of UniverseSets.
  • 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

source
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

  1. Take key as the universe key, or sets.xkey when key is nothing, and read the universe from sets.dict under it, giving its length N.
  2. Compute iN, the reciprocal of N in datatype.
  3. Return the range of length N whose start and stop are both iN.

Arguments

  • ::UniformValues: The algorithm that selects this method.
  • sets: The UniverseSets whose universe gives N.
  • ::Any: Fill value for API consistency (ignored).
  • key: (Optional) Key in the UniverseSets naming the universe the value is written over. When provided, takes precedence over sets.xkey.
  • datatype: Element type of the returned range.
  • kwargs...: Additional keyword arguments (ignored).

Returns

  • val::StepRangeLen: A range of length N, each entry the reciprocal of N.

Related

source
PortfolioOptimisers.parse_equationFunction
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.

  1. eqn is a vector: apply this function to each element, and return the vector of results.
  2. eqn is a string: check its length against EQUATION_LIMITS[].max_length, and refuse the pattern ++.
  3. Find the first operator of ops1 that occurs in the string, giving opstr, and split the string on it into lhs and rhs.
  4. Parse both parts with Meta.parse, giving lexpr and rexpr, check each with rethrow_parse_error, and check the depth of each against EQUATION_LIMITS[].max_depth with _expr_depth_exceeds.
  5. eqn is an Expr: check its depth against EQUATION_LIMITS[].max_depth with _expr_depth_exceeds, and refuse a ++ pattern with has_invalid_plus.
  6. Check that the head of the expression is a call and is exactly one operator of ops2, giving opstr, and read lhs and rhs off the arguments of the call.
  7. Hand opstr and the two sides to _parse_equation, which canonicalises them and builds the 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 from ops1.

      • ops1: Tuple of valid comparison operators as strings.
    • eqn::Expr: Must contain exactly one comparison operator from ops1.

      • 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. A Meta.ParseError naming both lengths is thrown otherwise.
  • The expression tree of eqn is no deeper than EQUATION_LIMITS[].max_depth, for both forms. The string form is checked after Meta.parse, on each side of the operator. A Meta.ParseError naming the limit is thrown otherwise.
  • eqn holds no ++ pattern.
  • eqn holds exactly one comparison operator, from ops1 for the string form and from ops2 for the Expr form.
  • The head of the Expr form is a call.
  • Neither side of the equation is empty or incomplete, which rethrow_parse_error checks.

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

source
PortfolioOptimisers.linear_constraintsFunction
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

  1. Return lcs. Neither method reads its further positional arguments or its keywords.

Arguments

  • lcs: An existing LinearConstraint object, nothing, or a vector of constraints.
  • args...: Additional positional arguments (ignored).
  • kwargs...: Additional keyword arguments (ignored).

Returns

  • lcs: The input, unchanged.

Related

source
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.

  1. Parse eqn with parse_equation, giving lcs, one ParsingResult per equation. Each result carries the equation in canonical form.
  2. Expand every group name of lcs into its members with replace_group_by_assets, giving results written in names of the universe. bl_flag selects which of the two expansions runs.
  3. Assemble the coefficient matrices and the right-hand sides from lcs with get_linear_constraints, which resolves each name against the universe key names and separates the equality rows from the inequality rows.
  4. Return what get_linear_constraints gives: a LinearConstraint, or nothing when no row survived.

Arguments

  • eqn: A single constraint equation (as AbstractString or Expr), or a vector of such equations.
  • sets: A UniverseSets object 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: If true, throws an error if a variable or group is not found in sets; if false, issues a warning.
  • bl_flag: If true, enables Black-Litterman-style group expansion.
  • key: Key naming the universe the variables resolve against. Defaults to sets.xkey.
  • rr: Loadings to re-base through, or nothing for an ordinary asset-space constraint.

Validation

Returns

  • lcs::LinearConstraint: An object containing the assembled equality and inequality constraints, or nothing if 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

source
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

  1. Read val and key off lcs.
  2. Drop rr and rd, for the reason the paragraph above gives.
  3. Return the LinearConstraint that the equation method builds from val, sets and key.
  4. Apply steps 1 to 3 to each element, and return the vector of results, when lcs is a vector. rr and rd reach every element, and every element drops them.

Arguments

  • lcs: The LinearConstraintEstimator to parse, or a vector of them.
  • sets: A UniverseSets object specifying the asset universe and groupings.
  • datatype: Numeric type for coefficients and right-hand side.
  • strict: If true, throws an error if a variable or group is not found in sets; if false, issues a warning.
  • bl_flag: If true, 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, or nothing when no row survived. A vector input gives one such value per element.

Related

source
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: The ExposureConstraintEstimator whose rows are re-based.
  • sets: The declared universe, carrying the factor axis rr names, under sets.tfkey or sets.cfkey.
  • datatype: Data type of the assembled row.
  • strict: If true, a name the universe does not resolve throws; if false, it warns and the term is dropped.
  • bl_flag: If true, 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. nothing on this route.

Returns

  • lc: An asset-space LinearConstraint, nothing when every row was dropped, or a vector of either when a vector was wrapped.

Related

source
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: If true, a name the universe does not resolve throws; if false, it warns and the term is dropped.
  • bl_flag: If true, 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 a LinearConstraint or nothing.

Related

source
PortfolioOptimisers.port_opt_viewMethod
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

  1. Read xkey and uxkey from sets, and open an empty dictionary dict of the type sets.dict has.
  2. For an entry of sets.dict whose key starts with xkey, take view(v, i), the group restricted to the selected assets.
  3. For an entry whose key starts with uxkey, take the unique entries of the xkey-prefixed partition it names, restricted to i. The unique-entry group is therefore derived from the sliced partition and never from the original one.
  4. Skip the nikey entry, 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 with nikey"nikkei225" under the default "ni" — is not silently dropped with it.
  5. Carry every other entry through unchanged, into the same dict. The tfkey-, utfkey-, cfkey- and ucfkey-prefixed entries, and every plain group, come back bit-identical.
  6. Return the UniverseSets built from dict and the eight unchanged key prefixes, which revalidates the prefix grammar over the viewed universe.

Arguments

  • sets: The UniverseSets to view.
  • i: The asset index selection.
  • args...: Additional positional arguments (ignored).

Returns

  • sets::UniverseSets: A new UniverseSets over the selected assets, declaring the same seven key prefixes as the original.

Related

source
PortfolioOptimisers.port_opt_viewMethod
port_opt_view(lc::LinearConstraint, i, args...) -> LinearConstraint

Return 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

  1. Return lc. The method reads neither the index nor the tail that follows it.

Arguments

  • lc: The precomputed LinearConstraint.
  • ::Any: The asset index selection (ignored).
  • args...: Additional positional arguments (ignored).

Returns

  • lc::LinearConstraint: The input, unchanged.

Related

source

References

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