Linear Constraints (b)

PortfolioOptimisers.LinearConstraintEstimator — Type
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 of the universe in sets.dict that names resolve against. If nothing, the caller reads the key from sets.xkey, or from the key of another declared axis when the caller is written against that axis.

Constructors

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

Keywords correspond to the struct's fields.

Validation

  • !isempty(val), when val is a string or a vector.
  • !isempty(key), when key is not nothing.

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.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;
                        ledger::Option{<:AbstractVector} = nothing)

Expand group or special variable references in a ParsingResult to their corresponding asset names.

A variable name of res can be a group name, a prior(...) expression or a correlation view such as (A, B). The function replaces each of them with the asset names that the UniverseSets holds. It gives the Black-Litterman mean expansion, the prior(...) views of entropy pooling and the correlation views. A vector of results gives a vector, one result per element.

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 keeps its first member, so the row names a departed asset, and get_linear_constraints drops that row in silence. A group that holds no 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, the function expands prior(...) expressions, for entropy pooling.
  • rho_flag: If true, the function expands 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.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 can come from outside the program, so both forms of input meet a limit of EQUATION_LIMITS[] before any recursive walk. The function caps the length of the string form before Meta.parse runs, and no length limit applies to the Expr form. It then caps the depth of the expression tree of both forms, so one number bounds the recursion for either form.

Julia parses the equation, so the number syntax of Julia applies. 2e1 is the number 20.0 and 2f1 is the number 20.0f0, and neither is a coefficient times a variable e1 or f1. Write 2*f1 for a variable whose name starts like an exponent.

Algorithm

The method that Julia selects is the algorithm, and one method handles each shape of eqn.

  1. When eqn is a vector, apply this function to each element, and return the vector of results.
  2. When 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. When 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. Give opstr and the two sides to _parse_equation, which puts them in canonical form 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 ops2.

      • 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_constraints — Function
linear_constraints(lcs::Option{<:LinearConstraint}, args...; kwargs...)
linear_constraints(lcs::AbstractVector{<:LinearConstraint}, ::Nothing, args...; kwargs...)

Return an assembled LinearConstraint, nothing, or a vector of assembled constraints unchanged.

A constraint slot can hold equations or an assembled LinearConstraint, so a caller can call linear_constraints on the slot with no check of its type.

The vector method takes a nothing universe and no other. A vector of assembled constraints needs no UniverseSets, and a Pipeline gives an optimiser this shape when more than one constraint step ran. With a real UniverseSets, a wider vector method takes the call and applies this method to each element.

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,
                   key::Option{<:AbstractString} = nothing;
                   ops1::Tuple = ("==", "<=", ">="),
                   ops2::Tuple = (:call, :(==), :(<=), :(>=)), datatype::DataType = Float64,
                   strict::Bool = false, bl_flag::Bool = false,
                   rr::Option{<:AbstractLoadingsRegressionResult} = nothing)

Parse and convert one or more linear constraint equations into a LinearConstraint object.

The function parses one or more equations, given as strings, as expressions or as a vector of them. It expands the group names through the UniverseSets, builds the coefficient matrices, and returns a LinearConstraint that holds the equality rows and the inequality rows.

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, which is 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, a variable name that the universe does not hold throws. If false, it warns.
  • bl_flag: If true, a group expands to the mean over its members, as the Black-Litterman views need. If false, it expands to the sum.
  • 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 from the estimator and gives both to the equation method, so a single estimator and a vector of estimators have one interface. A vector gives a vector of the same length, one result per element.

The method takes rr, so that a caller that holds loadings, such as processed_jump_optimiser_attributes, can pass them to the value of lcse with no check of its type. A bare LinearConstraintEstimator ignores them. The asset frame is the absence of a re-basis, and an estimator that re-based itself because loadings were available makes the space depend on the prior and not on what the user wrote. Only a wrapper ExposureConstraintEstimator asks for a re-basis. The method takes rd for the same reason and ignores it for a stronger one. Only a constraint space can ask for a refit, and a bare estimator has no space.

Algorithm

  1. Read val and key from lcs.
  2. Ignore rr and rd, for the reason that 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 ignores 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, a variable name that the universe does not hold throws. If false, it warns.
  • bl_flag: If true, a group expands to the mean over its members, as the Black-Litterman views need. If false, it expands to the sum.
  • 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_view — Method
port_opt_view(lc::LinearConstraint, i, args...) -> LinearConstraint

Return a precomputed LinearConstraint unchanged under an asset sub-selection.

A row over the full universe does not mean the same thing over a subset. The method returns the constraint unchanged all the same, because a slice of A changes what each row states, and a matrix holds no names that a view can re-resolve. A NestedClustered inner solve refuses a bare precomputed constraint for this reason. Stacking and SubsetResampling have no such guard.

A constraint that reaches a meta-optimiser through an ExposureConstraintEstimator is a different case. Its A has one column per factor, and the optimiser projects it again against the loadings of the viewed prior, so the view applies to the basis and not to 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).