Linear Constraints (b): private API
PortfolioOptimisers.LcE_Lc — Type
const LcE_Lc = Union{<:LinearConstraintEstimator, <:LinearConstraint}An unparsed LinearConstraintEstimator, or an assembled LinearConstraint. The group exists because a constraint slot accepts both. linear_constraints parses the first and returns the second unchanged, so a caller can give equations or a block that it built earlier.
Related
PortfolioOptimisers.VecLcE — Type
const VecLcE = AbstractVector{<:LinearConstraintEstimator}Every abstract vector whose elements are LinearConstraintEstimators. The group is narrower than VecLcE_Lc, because every element must still be parsed. linear_constraints maps over it and gives one constraint per element.
Related
PortfolioOptimisers.LcE_VecLcE — Type
const LcE_VecLcE = Union{<:LinearConstraintEstimator, <:VecLcE}One LinearConstraintEstimator, or a vector of them. The group excludes an assembled LinearConstraint, so a method that dispatches on it knows that every element still carries equations to parse.
Related
PortfolioOptimisers.VecLcE_Lc — Type
const VecLcE_Lc = AbstractVector{<:LcE_Lc}Every abstract vector whose elements are LcE_Lcs. The group exists so that one slot may hold a mixed list of equations still to parse and constraints already assembled.
Related
PortfolioOptimisers.LcE_Lc_VecLcE_Lc — Type
const LcE_Lc_VecLcE_Lc = Union{<:LcE_Lc, <:VecLcE_Lc}One LcE_Lc, or a vector of them. It is the widest linear-constraint group that the library declares. It names every shape that a user can write into a linear-constraint field, so the type bound of that field uses it.
Related
PortfolioOptimisers.get_linear_constraints — Function
get_linear_constraints(lcs::PR_VecPR, sets::UniverseSets,
key::Option{<:AbstractString} = nothing;
datatype::DataType = Float64, strict::Bool = false,
rr::Option{<:AbstractLoadingsRegressionResult} = nothing,
ledger::Option{<:AbstractVector} = nothing)Convert parsed linear constraint equations into a LinearConstraint object.
The function resolves the variable names of one or more ParsingResults, as parse_equation returns them, against the universe of the UniverseSets. It assembles the coefficient matrices and the right-hand sides, and returns a LinearConstraint that holds the equality rows and the inequality rows.
A row takes one of two shapes. Without rr, it runs over the universe that the names resolve against. With rr, it runs over the assets, because the loadings re-base each term during the assembly and the result is an ordinary asset-space row.
The function drops whole rows and never single terms. A row is one statement over several names with one right-hand side, so a name that the function cannot resolve drops the whole row. a + c == 0.05 without c fits a == 0.05, which is a different and stronger statement than the caller wrote. The cause of the failure decides only whether the function reports the drop. The function drops a row that names the counterpart axis in silence, whatever strict is. counterpart_axis_names reads that axis, which in practice is the Non-Investable Axis that a door wrote. The name was correct over the universe that the caller had, and the data moved it, so the door reports the departure once. A name on neither axis is a typo, and the function reports it.
Algorithm
- Take
kaskey, orsets.xkeywhenkeyisnothing, read the universenxfromsets.dictunder it, name the axis withuniverse_axis, and read the counterpart axis withcounterpart_axis_names. - Take
N, the row length, fromconstraint_row_length, and allocate the working rowAtof that length. - Zero
Atfor each parsing result, and start that result not dropped. - Build the indicator of each variable name of the result over
nx. A name that matches no entry marks the row dropped, and is reported throughstrict_diagnosticunless it names the counterpart axis. Every name of the row is still visited, so a row carrying two typos names both. - Add the contribution that
constraint_row_termgives for the name and its coefficient toAt. Withrrthe contribution is already projected, soAthas one entry per asset. - Move to the next result when the row was marked dropped.
- Report the row through
strict_diagnosticand drop it whenAtis still zero. Every name resolved before this step, so the message says that the row sums to zero, because of the loadings underrr, because its coefficients cancel, or because it holds no name. It never says that a name has a typo. - Read the sign and the inequality flag of the operator from
comparison_sign_ineq_flag, and scale the row and its right-hand side by the sign. That negates a>=row, so both senses of an inequality are written in the<=sense, which is the conventionLinearConstraintstates. - Append the row to the inequality accumulator when the flag is
true, and to the equality accumulator when it isfalse. - Reshape each accumulator that holds a row into a matrix of
Ncolumns, and build thePartialLinearConstraintof that half. - Return the
LinearConstraintholding the halves that were built, ornothingwhen neither half holds a row.
Arguments
lcs: A singleParsingResultor a vector of such objects, representing parsed constraint equations.sets: AUniverseSetsobject specifying the universes and groupings.key: Key naming the universe the variables resolve against. Defaults tosets.xkey; a re-based constraint passes the factor axis key thatfactor_axis_keyreads off the loadings.datatype: Numeric type for coefficients and right-hand side.strict: Iftrue, a variable name that the universe does not hold throws. Iffalse, it warns.rr: Loadings to re-base through, ornothingfor an ordinary asset-space constraint. Callers do not pass it directly, andExposureConstraintEstimatorpasses it.ledger: The door's ledger of departure casualties, ornothingwhen nobody is collecting. A row dropped for a name on the counterpart axis is recorded into it throughrecord_non_investable_drop!.
Validation
lcsis non-empty, when it is a vector.- A variable name that matches no entry of the universe and none of the counterpart axis raises when
strictistrue, and issues a warning otherwise. The row is dropped either way. - A variable name on the counterpart axis drops its row in silence, under both settings of
strict. - A row whose terms all fall away raises when
strictistrue, and issues a warning otherwise. The row is dropped either way. - Each
opis one of"==","<="or">=", whichcomparison_sign_ineq_flagenforces.
Returns
lcs::LinearConstraint: An object containing the assembled equality and inequality constraints, ornothingif no constraints are present.
Related
PortfolioOptimisers.universe_axis — Function
universe_axis(sets::UniverseSets, key::AbstractString) -> StringReturn the name of the axis of the universe under key, read from the key itself.
A key that starts with tfkey or cfkey gives "factor", and any other key gives "asset". unknown_variable_msg and empty_row_msg read it to name the axis that the user wrote in.
Both callers resolve names against sets.dict[key] alone, so the axis of that universe is the axis where a lookup failed, and the key is the right source. get_black_litterman_views takes the key from the estimator that owns the views, and get_linear_constraints takes it from the constraint space. FactorSpace resolves at the factor axis that factor_axis_key reads from the loadings, sets.tfkey for the time-series family and sets.cfkey for the cross-sectional family. The re-basis is the wrong source. A wrapped estimator with its own key replaces the key of the space, so a re-based row can resolve against a universe that the loadings do not use, and the message must name the universe that the lookup searched.
The test is on the prefix and not on equality, so a factor group key such as "nf_sector" or "ncf_sector" also gives the factor axis. UniverseSets refuses a key prefix that starts with another, so the answer is unique.
Algorithm
- Return
"factor"whenkeystarts withsets.tfkeyor withsets.cfkey. - Return
"asset"in every other case.
Arguments
sets: TheUniverseSetswhosetfkeyandcfkeyname the two factor axes.key: The key the names were resolved against.
Returns
axis::String:"factor"or"asset", the word a diagnostic message uses to name the axis.
Related
PortfolioOptimisers.constraint_row_length — Function
constraint_row_length(rr, nx::VecStr) -> IntReturn the length of an assembled constraint row.
Without a re-basis, the length is the size of the universe that the names resolve against. With a re-basis, it is the number of assets that the loadings project onto, because the assembly of the row applies the projection and gives an ordinary asset-space row.
Algorithm
The method that Julia selects is the algorithm, and the re-basis selects it.
- When
rrisnothing, return the length ofnx, the universe the names resolve against. - When
rris a regression result, return the number of rows ofrr.M, which is the number of assets the loadings project onto.
Arguments
rr: Loadings to re-base through, ornothingfor an ordinary asset-space row.nx: The universe the names resolve against.
Returns
N::Int: The number of entries one assembled row has.
Related
PortfolioOptimisers.constraint_row_term — Function
constraint_row_term(::Nothing, Ai, c)
constraint_row_term(rr::AbstractLoadingsRegressionResult, Ai, c)Return the contribution of one matched variable to a constraint row.
Without a re-basis, the contribution is the indicator Ai times the coefficient c. With a re-basis, it is the sum of the columns of the loadings that Ai selects, times c. The function sums the columns and does not take only the first match, so a duplicated name in a factor universe contributes every column with that name, as a duplicated asset name does on the asset path.
The function reads rr.M and never rr.L. The columns of M are the named original factors, and a constraint must use names that a user can write in an equation. Risk decomposition reads L instead.
Mathematical definition
\[\begin{align} \boldsymbol{a}^\intercal \boldsymbol{w}_f &= \boldsymbol{a}^\intercal \mathbf{M}^\intercal \boldsymbol{w}_a = (\mathbf{M} \boldsymbol{a})^\intercal \boldsymbol{w}_a\,. \end{align}\]
Where:
- $\boldsymbol{a}$: A constraint row written in factor names.
- $\boldsymbol{w}_f$: The factor weights that row is written against.
- $\boldsymbol{w}_a$: The asset weights the optimiser holds.
- $\mathbf{M}$: The factor loadings, one column per named factor and one row per asset.
The identity lets a row written in factor names bind the asset weights with no change of variables. The re-based row is an ordinary asset-space row over $\boldsymbol{w}_a$.
Algorithm
The method that Julia selects is the algorithm, and the re-basis selects it.
- When
rrisnothing, returnAitimesc, one entry per name of the universe. - When
rris a regression result, sum the columns ofrr.MthatAiselects, multiply the sum byc, and return it. The value has one entry per asset, whatever names the row uses.
Arguments
rr: Loadings to re-base through, ornothingfor an ordinary asset-space row.Ai: The indicator of the matched name over the universe the names resolve against.c: The coefficient the matched name carries.
Returns
- The contribution of this term to the row, of the length
constraint_row_lengthgives.
Related
PortfolioOptimisers._parse_equation — Function
_parse_equation(lhs, opstr::AbstractString, rhs, datatype::DataType = Float64)Parse and canonicalise a linear constraint equation from Julia expressions.
The function takes both sides of an equation as expressions, and the comparison operator as a string. It evaluates the numeric calls, moves every term to the left-hand side, sums the coefficient of each variable, and returns the equation in canonical form as a ParsingResult.
Algorithm
- Fold the constant subexpressions of both sides with
eval_numeric_functions, givinglexprandrexpr, and check each withrethrow_parse_error. - Build
diff_expr, the expressionlexpr - (rexpr). This moves every term of the equation to the left-hand side. - Walk
diff_exprwith_collect_terms, givingterms, one(coefficient, variable)pair per term. - Accumulate
termsintovarmap, which holds the summed coefficient of each variable name, and intoconstant, the sum of the coefficients that carry no variable. - Read
variablesandcoefficientsoffvarmap, and takerhs_valas the negatedconstant. This moves the constant to the right-hand side. - Render each pair with
format_term, join the renderings with+, and fold+ -into-, giving the canonical stringformatted. - Return the
ParsingResultbuilt fromvariables,coefficients,opstr,rhs_valandformatted.
Arguments
lhs: Left-hand side of the equation as a Julia expression.opstr: Comparison operator as a string.rhs: Right-hand side of the equation as a Julia expression.datatype: Numeric type for coefficients and right-hand side.
Returns
res::ParsingResult: The variables, coefficients, operator, right-hand side and formatted string of the equation in canonical form. The order ofvarsis the order the variable map iterates in, and it is not the order the equation was written in.
Related
PortfolioOptimisers.has_invalid_plus — Function
has_invalid_plus(expr)Check whether a Julia expression contains the operator ++ anywhere in its tree.
It is the Expr counterpart of the ++ check that the string form of parse_equation runs on the raw text.
Algorithm
- Return
falsewhenexpris not a call, because only a call can carry the head this function refuses. - Return
truewhen the head of the call is the++operator. - Apply this function to every argument of the call that is itself an expression, and return
truewhen any of them does.
Arguments
expr: Julia expression to check.
Returns
Bool:trueif the tree holds a++call,falseotherwise.
Related
PortfolioOptimisers._expr_depth_exceeds — Function
_expr_depth_exceeds(x, limit::Integer) -> BoolReturn true if the expression tree x is deeper than limit.
It protects the Expr form of parse_equation from a deeply nested tree that no string length limit covers. The depth counts the levels of Expr along the deepest path, so :(a + b) has depth one. The check recurses at most limit + 1 frames deep and stops at the first branch that passes the limit, so it cannot use up the stack that it protects.
Algorithm
- Return
truewhenlimitis negative, because the walk has already gone one level past the cap. - Return
falsewhenxis not an expression, because a leaf adds no depth. - Apply this function to every argument of
x, withlimitlowered by one, and returntruewhen any of them does. The scan stops at the first argument that answerstrue.
Arguments
x: The expression tree to measure.limit: The greatest depth the tree may have.
Returns
Bool:truewhen the tree is deeper thanlimit,falseotherwise.
Related
PortfolioOptimisers.assert_investable_constraint_width — Function
assert_investable_constraint_width(lcs::Nothing, N::Integer, slot::AbstractString)
assert_investable_constraint_width(lc::LinearConstraint, N::Integer,
slot::AbstractString)
assert_investable_constraint_width(lcs::VecLc, N::Integer, slot::AbstractString)Refuse a precomputed LinearConstraint whose rows are wider than the investable universe, with a message that states why.
A name-keyed estimator stays valid after a reduction to the Investable Mask. It resolves against the UniverseSets that the door gives it, and a name that left resolves on the Non-Investable Axis and is not refused. A precomputed constraint cannot do this. Its A is a matrix, and the position is the only link between a column and an asset, so there is no name to resolve again and no correct way to narrow the matrix. A dropped column changes what Ax ≤ B means, and for that reason port_opt_view(::LinearConstraint, i) is the identity.
The row therefore passes the door at its original width and meets a shorter weight vector. Without this check, the model raises a bare DimensionMismatch between two numbers, and nothing links either number to the asset that delisted. This function raises once, before the model, and its message states what the caller did and what the caller can do instead.
The repair is always the same. State the constraint as a LinearConstraintEstimator, which resolves its names again over the universe that the door leaves.
Algorithm
- Return when there is nothing to check, which is a
nothingslot or anothinghalf of aLinearConstraint. - Otherwise compare
size(A, 2)of each half againstNand throw aDimensionMismatchnaming the slot, the two widths and the repair when they disagree.
Arguments
lcs: The resolved constraint, a vector of them, ornothing.N: The number of investable assets the optimisation runs over.slot: Names the field the constraint came from in the message, for example"lcse".
Validation
- Every half of every precomputed constraint has one column per investable asset.
Returns
nothing.
Related