Linear Constraints (a): private API
PortfolioOptimisers.VecLc — Type
const VecLc = AbstractVector{<:LinearConstraint}Every abstract vector whose elements are LinearConstraints. The group exists so that one method signature accepts a whole block of assembled constraints, which is what a caller holds after several constraint steps have each produced one.
Related
PortfolioOptimisers.Lc_VecLc — Type
const Lc_VecLc = Union{<:LinearConstraint, <:VecLc}One assembled LinearConstraint, or a vector of them. The group exists because a caller that ran one constraint step and a caller that ran several reach the same slot, so every method that reads that slot must accept both arities.
Related
PortfolioOptimisers.VecPR — Type
const VecPR = AbstractVector{<:ParsingResult}Every abstract vector whose elements are ParsingResults. The group exists because parse_equation returns a vector of results for a vector of equations, and every later stage maps over that vector.
Related
PortfolioOptimisers.PR_VecPR — Type
const PR_VecPR = Union{<:ParsingResult, <:VecPR}One ParsingResult, or a vector of them. The group exists because an equation may be written singly or in a list, and every stage after parse_equation carries whichever arity it was given through to get_linear_constraints.
Related
PortfolioOptimisers.AbstractParsingResult — Type
abstract type AbstractParsingResult <: AbstractConstraintResultAbstract supertype for all equation parsing result types.
Every type that holds a parsed equation subtypes AbstractParsingResult. Each member holds one parsed equation in canonical form, which is the variable names, their coefficients, the comparison operator and the right-hand side. The stages after parse_equation therefore read one shape, whatever form the equation had.
Related
PortfolioOptimisers.allowed_functions — Constant
allowed_functions = Dict{Symbol, Function}(:+ => +, :- => -, :* => *, :/ => /,
:^ => ^, :sqrt => sqrt, :cbrt => cbrt,
:exp => exp, :exp2 => exp2, :exp10 => exp10,
:log => log, :log2 => log2, :log10 => log10,
:abs => abs, :min => min, :max => max)Maps the name of each of the 16 functions that an equation can call to its function object.
An equation string can come from a configuration file, a spreadsheet or a user interface, so the parser calls only the functions of this table. The table maps each name to its function directly and does not look the name up in Base. A name that is not a key fails closed with a Meta.ParseError, and the callable functions cannot drift from the allowed names, because the two are one list.
The prior(...) marker is not in the table. It names assets or groups and not numbers, so eval_numeric_functions keeps it and replace_group_by_assets expands it, and no function evaluates it as a number.
Related
PortfolioOptimisers.merge_partial_linear_constraints — Function
merge_partial_linear_constraints(
ps
) -> Union{Nothing, PartialLinearConstraint{var"#s185", <:AbstractVector{var"#s137"}} where {var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s185"<:AbstractMatrix{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}}
Concatenate the rows of the same half of several PartialLinearConstraints, skipping the absent ones.
Algorithm
- Collect the entries of
psthat are notnothing, givingkept. - Return
nothingwhenkeptis empty, because the half is absent from every input. - Read the row width of the first entry of
kept, givingN, and check every other entry against it. - Stack the
Amatrices ofkeptin input order, and stack theirBvectors the same way. - Return the
PartialLinearConstraintbuilt from the two stacks.
Arguments
ps: The halves to concatenate, each aPartialLinearConstraintornothing.
Validation
- Every kept half is written over the same number of variables,
size(p.A, 2) == N. ADimensionMismatchis thrown otherwise.
Returns
- A
PartialLinearConstraint, ornothingwhen every input was absent.
Related
PortfolioOptimisers.merge_linear_constraints — Function
merge_linear_constraints(
lcs::AbstractVector{<:LinearConstraint}
) -> LinearConstraint
Combine several LinearConstraints into the single one that holds all their rows.
A LinearConstraint is a block of rows, and two blocks applied one after the other are the same as the one block that stacks them. The inequality halves concatenate, the equality halves concatenate, and an absent half adds no row. Generation does the same when it takes several estimators at once. For example, centrality_constraints over a vector of CentralityConstraints appends every row to one result, and does not return one result per estimator.
A caller that computes its constraints one at a time, such as a Pipeline that runs one step per estimator, can merge them here. The optimiser then gets the value that the vector form gives.
Algorithm
- Return the one element unchanged when
lcsholds a single constraint. - Merge the
ineqhalf of every element withmerge_partial_linear_constraints, giving the inequality half of the result. - Merge the
eqhalf of every element the same way, giving the equality half. - Return the
LinearConstraintbuilt from the two halves.
Arguments
lcs: The constraints to merge.
Validation
lcsis non-empty.- Every merged half is written over the same number of variables.
Returns
lc::LinearConstraint: One constraint carrying every row, in input order.
Examples
julia> lc1 = LinearConstraint(; ineq = PartialLinearConstraint(; A = [1.0 0.0], B = [0.5]));julia> lc2 = LinearConstraint(; ineq = PartialLinearConstraint(; A = [0.0 1.0], B = [0.25]));julia> PortfolioOptimisers.merge_linear_constraints([lc1, lc2])LinearConstraint ineq ┼ PartialLinearConstraint │ A ┼ 2×2 Matrix{Float64} │ B ┴ Vector{Float64}: [0.5, 0.25] eq ┴ nothingRelated
PortfolioOptimisers.prefixed_sets_keys — Function
prefixed_sets_keys(
dict::AbstractDict,
prefix::AbstractString
) -> Vector{String}
Collect the dict keys that start with prefix, as the candidate pool of a suggest_declared_key suggestion inside UniverseSets.
The group that needs a missing partition key reports it, so the whole key set is the wrong pool. In Dict("ux_sector" => …) the nearest neighbour of the missing nx_sector is ux_sector, the key under validation, and a suggestion from the whole set tells the caller to rename the one key that is correct. A pool of the keys that carry the prefix of the missing key holds only keys that the caller can have meant.
Algorithm
- Return the keys of
dictthat start withprefix, as strings, in the orderdictiterates in.
Arguments
dict: TheUniverseSetsdictionary being validated.prefix: The axis prefix the missing key must carry,xkey,tfkeyorcfkey.
Returns
candidates::Vector{String}: The keys ofdictthat start withprefix.
Related
PortfolioOptimisers.unclaimed_sets_keys — Function
unclaimed_sets_keys(
dict::AbstractDict,
claimed
) -> Vector{String}
Collect the dict keys that no axis in claimed has taken, as the candidate pool of the missing-xkey suggestion inside UniverseSets.
It is the counterpart of prefixed_sets_keys for xkey, the one key with no prefix of its own. Any key can hold the asset names, so no prefix finds the asset universe. The function removes every key that another declared axis claims. Without that step, a dict that holds only a factor axis answers a mistyped xkey with the factor key, which names a different axis.
Algorithm
- Return the keys of
dictthat start with no entry ofclaimed, as strings, in the orderdictiterates in.
Arguments
dict: TheUniverseSetsdictionary being validated.claimed: The other declared axis prefixes,uxkey,tfkey,utfkey,cfkeyanducfkey.
Returns
candidates::Vector{String}: The keys ofdictthat start with no entry ofclaimed.
Related
PortfolioOptimisers.assert_factor_partition — Function
assert_factor_partition(dict::AbstractDict, k::AbstractString, fkey::AbstractString,
axis::AbstractString) -> NothingAssert that the factor partition k names a declared factor axis fkey, and that the two agree on how many factors there are.
UniverseSets carries two factor axes, and one rule holds on both, so this function states the rule once and the constructor calls it once per axis. axis names the axis in both messages. A caller who declared the time-series axis and wrote a cross-sectional partition learns which of the two is missing, which the key value alone does not show.
Arguments
dict: TheUniverseSetsdictionary being validated.k: Thefkey-prefixed key under validation.fkey: The factor axis key the prefix belongs to,tfkeyorcfkey.axis: Names the axis in both diagnostic messages, for example"time-series factor".
Validation
haskey(dict, fkey). AKeyErrornamingaxisis thrown otherwise.length(dict[k]) == length(dict[fkey]). ADimensionMismatchnamingaxisis thrown otherwise.
Returns
nothing.
Related
PortfolioOptimisers.assert_factor_unique_group — Function
assert_factor_unique_group(dict::AbstractDict, k::AbstractString, fkey::AbstractString,
ufkey::AbstractString, axis::AbstractString) -> NothingAssert that the unique-entry factor group k names a declared factor axis fkey, that the partition it draws its entries from exists, and that the partition has the length of the axis.
It is the sibling of assert_factor_partition, for the same reason. UniverseSets carries two factor axes, and one rule holds on both, so this function states the rule once and the constructor calls it once per axis.
Arguments
dict: TheUniverseSetsdictionary being validated.k: Theufkey-prefixed key under validation.fkey: The factor axis key the group summarises,tfkeyorcfkey.ufkey: The unique-entry prefixkcarries,utfkeyorucfkey.axis: Names the axis in every diagnostic message, for example"cross-sectional factor".
Validation
haskey(dict, fkey). AKeyErrornamingaxisis thrown otherwise.haskey(dict, fkey * chopprefix(k, ufkey)). AKeyErrorcarrying a spelling suggestion is thrown otherwise.length(dict[fkey * chopprefix(k, ufkey)]) == length(dict[fkey]). ADimensionMismatchis thrown otherwise.
Returns
nothing.
Related
PortfolioOptimisers.name_to_val! — Function
name_to_val!(nx::VecStr, sdict::AbstractDict, key::Any, val::Number,
arr::VecNum, strict::Bool, nxkey::AbstractString,
other::VecStr = String[])Set values in a vector for the asset or the group of assets that key names.
name_to_val! resolves key through resolve_axis_name, maps the members to indices in the asset universe nx, and sets those entries of arr to val. An asset name resolves to itself, and a group name expands to its members. If key names neither, the function throws or warns, as strict selects. Each diagnostic message names the size of the universe and never the universe itself or the value dictionary, because a shared message builder writes each message.
other is the counterpart axis, the asset names that the same UniverseSets declares on the axis that this call does not resolve against. The function skips a name that it finds there in silence, whatever strict is, and this is the only exception to strict. strict catches a typo of the caller, and a name on the counterpart axis is not a typo. It was a correct name over the universe that the caller had, and the data moved it. A caller cannot know in advance which asset a prior will fail to estimate, so a refusal, or a warning once per constraint and once per window of a walk-forward, reports a thing that no one can act on. The door that derived the mask reports the departure once.
The two asset axes are counterparts of each other, and the relation is symmetric. When the call resolves on the asset universe, other is the Non-Investable Axis, so the function drops a bound for an asset that left. When the call resolves on the Non-Investable Axis, which is how a forced-liquidation rate is priced, other is the asset universe, so the same rule drops a liquidation rate for an asset that stayed. A factor axis has no counterpart, and other is then empty.
Algorithm
- Resolve
keythroughresolve_axis_name, givingmembers. An asset name resolves to itself, and a group name expands to a copy of its member list. An asset name takes precedence over a group name of the same spelling. - Return in silence when
membersisnothingandkeynames an entry ofother, because the name is on the counterpart axis. It is a correct name, and this axis has no entry to write it into. - Report through
strict_diagnosticand return whenmembersisnothing, becausekeynames neither an asset nor a group. The suggestion pool is widened fromnxtonxtogether with the keys ofsdict, because a missing name may be a mistyped asset or a mistyped group. - Map
membersto positions innxwithaxis_name_indices, givingidx, and drop the members that miss the universe. Remove from the report the members onother, by the rule of step 2, and report the rest once throughstrict_diagnostic. A group whose departed members are all onotheris therefore silent, and a group that holds a real typo names it. - Set the entries of
arratidxtoval.
Arguments
nx: Vector of asset names.sdict: Dictionary mapping group names to vectors of asset names. It is never modified, becauseresolve_axis_namereturns a copy of the member list.key: Name of the asset or the group of assets to set values for.val: The value to assign.arr: The array that the function changes in place.strict: Iftrue, akeythat resolves to nothing throws. Iffalse, it warns.nxkey: The key of the asset universe insets.dict, for example"nx". The messages ofunknown_variable_msgandmissing_group_assets_msgread it to name the universe.other: The counterpart asset axis. The function skips its names in silence and does not report them.
Validation
keynames an asset ofnx, a group ofsdict, or an entry ofother. AnArgumentErroris thrown whenstrictistrue, and a warning is issued otherwise.- Every member of a resolved group names an entry of
nxor ofother. A member that misses both is dropped, and the drop raises whenstrictistrueand issues a warning otherwise.
Returns
nothing. The function changesarrin place.
Related
PortfolioOptimisers.rethrow_parse_error — Function
rethrow_parse_error(expr, side = :lhs)Raise when a parsed side of an equation is empty or incomplete.
The parser calls it on both sides of an equation. An empty or incomplete side raises a Meta.ParseError, and any other side returns nothing. The parser fails closed on an empty side and does not assume zero, because an assumed zero is a constraint that the author never wrote. A caller who means zero writes it.
Algorithm
The method that Julia selects is the algorithm, and one method handles each shape that a parsed side can take.
- When
exprisNothing, which an empty side gives, raise and namesidein the message. - When
expris anExpr, raise when its head is:incomplete, and returnnothingotherwise. - When
expris anything else, a number or a symbol among them, returnnothing.
Arguments
expr: The parsed Julia expression to check. Can be anExpr,Nothing, or any other type.side: The side of the equation,:lhsor:rhs, which the message names.
Validation
expris notNothing. AMeta.ParseErrornamingsideis thrown otherwise.expr.head != :incomplete. AMeta.ParseErrornamingsideand the expression is thrown otherwise.
Returns
nothing.
Related
PortfolioOptimisers.format_term — Function
format_term(coeff, var)Format a single term in a linear constraint equation as a string.
The function writes a coefficient of one as the bare name, a coefficient of minus one as the name behind a minus sign, and every other coefficient as c*name.
Algorithm
- Return the variable name alone when
coeffis one. - Return the variable name behind a minus sign when
coeffis minus one. - Return the coefficient, a
*, and the variable name, in every other case.
Arguments
coeff: Numeric coefficient for the variable.var: Variable name as a string.
Returns
term_str::String: The formatted term as a string.
Related
PortfolioOptimisers.collect_terms! — Function
collect_terms!(expr, coeff, terms)Recursively collect and expand terms from a Julia expression for linear constraint parsing.
The function walks the expression tree of a linear equation and appends one (coefficient, variable) pair per term to terms. It reads numeric constants, variables and the operations +, -, * and /.
Algorithm
- Append
(coeff * expr, nothing)whenexpris aNumber, so a constant carries the coefficient and no variable. - Append
(coeff, string(expr))whenexpris not a call. ASymbolis a bare variable that carries the coefficient it arrived with, and any other leaf, a string literal among them, is a variable named by its text. - For a multiplication, multiply
coeffby every numeric factor, givingc. The parser writes2*3*xas one call with three factors, so the step reads every factor and not only the first two. Recurse into the one factor that is not a number withc. When no factor is left, append(c, nothing). When more than one factor is left, the product is opaque, so append the product of those factors whole, as(c, text). - For a division
a / b, recurse intoawithcoeffdivided byb, whenbis a number. - For an addition, recurse into every argument with
coeffunchanged. - For a subtraction, recurse into every argument but the last with
coeff, and into the last with-coeff. A unary minus holds no argument but the last, so this negates its one operand. - Append any other call whole, as
(coeff, string(expr)). A division by a denominator that is not a number is such a call. This step makes a term such assqrt(x)opaque. It becomes one variable named by its own text, and the row builder resolves that text against the universe like any other name.
Arguments
expr: The Julia expression to traverse.coeff: The current numeric coefficient to apply.terms: The vector that the function appends the pairs to, in place. A pair holds a coefficient and a variable name, ornothingin place of the name for a constant term.
Returns
nothing. The function changestermsin place.
Related
PortfolioOptimisers._collect_terms — Function
_collect_terms(expr::Union{Symbol, Expr, <:Number}, datatype::DataType = Float64)Expand and collect all terms from a Julia expression representing a linear constraint equation.
The function walks the expression and returns one (coefficient, variable) pair per term. It reads numeric constants, variables and the operations +, -, * and /, and _parse_equation calls it to put an equation in canonical form.
The walk starts from the coefficient one(datatype), so the coefficients that it builds come from the arithmetic of datatype, the number type of the optimiser, as the right-hand side does.
Algorithm
- Open an empty vector
terms. - Walk
exprwithcollect_terms!, from the starting coefficientone(datatype). The walk appends one pair totermsper term that it reaches. A constant gives(coefficient, nothing), and any other term gives(coefficient, name). - Return
terms.
Arguments
expr: The Julia expression to expand.datatype: Numeric type of the coefficients the walk builds.
Returns
terms::Vector{Any}: A vector of(coefficient, variable)pairs, wherevariableis a string for variable terms ornothingfor constant terms. The vector is untyped, and each coefficient has the type that the arithmetic of the walk gives fromone(datatype).
Related
PortfolioOptimisers.eval_numeric_functions — Function
eval_numeric_functions(expr, datatype::DataType = Float64)Recursively evaluate numeric functions and constants in a Julia expression.
The function walks an expression tree and evaluates each call whose arguments are all numbers, and it reads the symbol Inf as the number. _parse_equation calls it on both sides of an equation before it collects the terms.
Before a call runs, the function converts every argument to datatype, so the arithmetic runs in the number type of the optimiser and not in machine Int64. Integer literals therefore do not wrap. 2^64 gives 1.8446744073709552e19 and not 0, and 2^-1 gives 0.5 and not a DomainError. A numeric literal inside a call that is not evaluated keeps its type, so 2^z stays 2 ^ z.
A call head that allowed_functions does not hold fails closed with a Meta.ParseError. The function keeps the prior(...) marker for replace_group_by_assets, and raises a Meta.ParseError when all its arguments are numbers.
Algorithm
- Fold every argument of
exprfirst, by applying this function to each, whenexpris anExpr. A node whose head is not:callis rebuilt from its folded arguments and returned. - Rebuild a
:callnode whose head ispriorfrom its folded arguments, and return it. The marker names assets or groups, so it is never folded to a number. - Look the head of any other
:callnode up inallowed_functions, givingf. A head that the table does not hold raises. - Rebuild the call and return it when any folded argument is not a
Number, so a nonlinear subexpression keeps its own literals untouched. - Coerce every folded argument to
datatype, applyfto them, and return the value that comes back. - Return the
Float64valueInfwhenexpris the symbol:Inf. Returnexpritself in every other case, so a number stays a number and a variable name stays a symbol.
Arguments
expr: The Julia expression to evaluate. Can be aNumber,Symbol, orExpr.datatype: The number type that the arguments of an evaluated call are converted to.
Validation
- The head of a
:callnode is a key ofallowed_functions, or thepriormarker. AMeta.ParseErrornaming the head is thrown otherwise. prior(...)carries at least one argument that is not a number. AMeta.ParseErroris thrown otherwise.
Returns
- The evaluated expression, with all numeric sub-expressions replaced by their computed values. Non-numeric or symbolic expressions are returned in their original or partially simplified form.
Related
PortfolioOptimisers.factor_universe — Function
factor_universe(sets::UniverseSets, key::AbstractString, K::Integer,
need::AbstractString, source::AbstractString) -> VecStrRead the declared factor universe sets.dict[key], and check that it exists and names one factor per column of source.
source is the observations × factors matrix, and K is its number of columns. A factor axis is optional on UniverseSets, but a consumer that reads one needs it, so this function reports the failure where the consumer reads the axis. Both messages name key and the matrix, because the caller must make these two agree. A caller who put the factor names under xkey learns that the factor key is missing, and not that an asset universe is wrong.
The caller gives key, because UniverseSets declares two factor axes and this function cannot tell which one the caller means. A caller that holds a loadings result reads the key from it with factor_axis_key. A caller that reads the columns of the returns data F gives sets.tfkey, the axis of those columns.
Every consumer of either axis calls this one function, and none of them repeats the checks.
Arguments
sets: TheUniverseSetswhose factor axis is read.key: The factor axis key to read,sets.tfkeyorsets.cfkey.K: The number of columns ofsource, which the declared axis must name.need: Names the consumer in both diagnostic messages, for example"a FactorSpace constraint".source: Names the matrix in both diagnostic messages, for example"rr.M"or"F".
Validation
haskey(sets.dict, key). AKeyErrornamingneedis thrown otherwise.length(sets.dict[key]) == K. ADimensionMismatchnamingsourceis thrown otherwise.
Returns
nf::VecStr: The declared factor names, in the column order ofsource.
Related
PortfolioOptimisers.factor_axis_key — Function
factor_axis_key(sets::UniverseSets, rr::Regression) -> AbstractString
factor_axis_key(sets::UniverseSets, rr::CrossSectionalFactorModel) -> AbstractString
factor_axis_key(sets::UniverseSets,
re::AbstractTimeSeriesRegressionEstimator) -> AbstractStringReturn the UniverseSets key naming the factor axis that rr's loadings are written on.
UniverseSets declares two factor axes, so a consumer that resolves factor names must say which one it means. The consumer does not choose by hand. The key follows the result that carries M. A Regression is fitted per asset over the observations, so its columns are the columns of rd.F, and it gives sets.tfkey. A CrossSectionalFactorModel is fitted per observation across the assets, so its columns are the exposures of the fit, and it gives sets.cfkey. A caller therefore cannot name the wrong axis, and no consumer needs a field for the key.
The third method is for a consumer that holds an unfitted specification and not a result. Only the time-series family has a specification here, because RegE_Reg takes an AbstractTimeSeriesRegressionEstimator and no other estimator, and every result that family fits is a Regression. The three methods therefore cover RegE_Reg exactly.
No method takes AbstractLoadingsRegressionResult as a fallback. A new member of that root inherits the axis of such a fallback, and that axis can be the wrong list of names with the right length. A constraint written against it then solves and constrains the wrong factors. A missing method is a MethodError that names the type.
Arguments
sets: TheUniverseSetswhose factor axis key is read.rr/re: The loadings result, or the specification whose verb produces one.
Returns
key::AbstractString:sets.tfkeyfor the time-series family,sets.cfkeyfor the cross-sectional one.
Related
PortfolioOptimisers.non_investable_sets — Function
non_investable_sets(sets::Nothing, ni) -> Nothing
non_investable_sets(sets::UniverseSets, ni::VecStr) -> UniverseSetsWrite the Non-Investable Axis on sets, which declares ni, the names that the Investable Mask left out, under sets.nikey.
A door calls this function after it reduces an optimiser to the Investable Mask, so a UniverseSets that carries the axis was reduced by one door for one problem. port_opt_view drops the axis, so no sub-problem inherits it.
A caller can also declare the axis by hand. Outside a door, that is the only way to resolve a forced-liquidation rate, for example in a direct call of fees_constraints with no optimisation around it. Inside a door the mask comes from the data, so this function overwrites an entry that the caller wrote and does not merge the two.
An empty ni returns sets unchanged. No asset left the universe, and an empty axis makes fees_constraints resolve a carrier that prices no position.
Algorithm
- Return
setsunchanged whenniis empty. - Otherwise copy
sets.dict, writeniundersets.nikey, and rebuild theUniverseSetsfrom it and the seven unchanged key prefixes, which revalidates uniqueness and disjointness over the minted axis.
Arguments
sets: TheUniverseSetsto write the axis on, ornothing.ni: The names the Investable Mask left out, in the order the complement of the mask visits them.
Returns
sets: TheUniverseSetscarrying the Non-Investable Axis, ornothing.
Related
PortfolioOptimisers.non_investable_names — Function
non_investable_names(nx::Nothing, imsk::BitVector) -> VecStr
non_investable_names(nx::VecStr, imsk::BitVector) -> VecStrRead the names the Investable Mask leaves out, in the order the complement of the mask visits them.
The order matters. A forced-liquidation carrier is sliced by index to the complement of the mask, and the rate that prices it resolves against these names by position, so both must walk the complement in one order. This function indexes nx with .!imsk, which is ascending, and port_opt_view(::Fees, i, X) takes the complement of i over the width of X, which is also ascending.
Returns data with no names gives an empty vector and does not throw. The axis is a list of names, so a problem with no names has no Non-Investable Axis and no name-keyed constraint to resolve against it.
Arguments
nx: The asset names of the unreduced returns data, ornothing.imsk: The Investable Mask the optimisation reduced on:trueat every asset whose prior moments were finite. It isnothingwhen every asset was investable, and that sentinel is what skips both the reduction and the expansion.investable_maskderives it once from the full-universe prior result, and the result carries it, because the reduced prior can no longer yield it.
Returns
ni::VecStr: The names the mask leaves out, or an empty vector.
Related
PortfolioOptimisers.record_non_investable_drop! — Function
record_non_investable_drop!(ledger::Nothing, what::AbstractString) -> Nothing
record_non_investable_drop!(ledger::AbstractVector, what::AbstractString) -> NothingRecord one thing that a departure cost, for the door to report.
Each site drops a departed name where it finds it, for example a view row or a group member. The site is far from the door, and only the door knows that a departure happened. A report at the site repeats once per row and once per window of a walk-forward. No report at all leaves a caller who wrote three views and got one fitted with no way to learn it. So the site writes what it dropped into a ledger, and the door reads the ledger once and reports the departures and the drops together, through announce_non_investable.
A nothing ledger collects nothing, and it is the default everywhere. A caller who assembles constraints outside a door has no door to report to, and pays nothing for a ledger that it does not keep. Dispatch on the type of the ledger selects the branch.
what is a noun phrase that names the dropped item, not a sentence. The door joins the phrases into one message and gives the verb.
Arguments
ledger: The door's ledger, ornothingwhen nobody is collecting.what: A noun phrase naming what was dropped, for example $"the view row `a + c == 0.05`"$.
Returns
nothing. A vector ledger is appended to in place.
Related
PortfolioOptimisers.record_group_shed! — Function
record_group_shed!(ledger::Option{<:AbstractVector}, group::AbstractString,
shed::Integer, kept::Integer, eqn::AbstractString) -> NothingRecord what a group shed to a departure, for the door to report through announce_non_investable.
A group that loses some of its members still describes the rest, so its row stays, with the coefficient spread over the survivors. A group that loses all of its members describes nothing, and its row goes. The two cases tell the caller different things, so the function writes a different phrase for each, and no other function writes either phrase. shed_departed_members is the only caller, and replace_group_by_assets reaches it from each of its four expansion branches.
The phrases give counts and not names. The door names the departed assets once, and a list per group makes the message longer than the facts that it reports.
A shed of nothing records nothing, so the all-investable path costs one comparison.
Arguments
ledger: The door's ledger, ornothingwhen nobody is collecting.group: The group name as the caller wrote it, or the pair"(a, b)"for a correlation view.shed: How many members the group lost.kept: How many members survived.eqn: The row the group appears in, as the caller wrote it.
Returns
nothing.
Related
PortfolioOptimisers.announce_non_investable — Function
announce_non_investable(ni::VecStr, drops::VecStr = String[],
process::AbstractString = "optimisation",
consequence::AbstractString = "…";
warn::Bool = false) -> NothingAnnounce, once per door, the assets that left the investable universe and what their leaving cost.
Only the door knows that a departure happened. After the door, a departed asset is absent. A bound stated for it resolves on the Non-Investable Axis and is skipped, and a view row that names it is dropped whole. A report at each of those sites repeats once per row and once per window of a walk-forward, so each site writes its drop into a ledger with record_non_investable_drop!, and the door reports everything once, here.
process names the work that excludes the departed assets, because more than one kind of door writes the axis. An optimisation reduces at its entry, and a wrapping prior reduces at its own entry before it builds a view. consequence states what a departure means to this door. Only an optimisation prices a forced-liquidation carrier, so the defaults of the two arguments give the message of the optimisation door.
The message is an @info by default, not a warning and not a strict_diagnostic. Nothing is wrong. The data changed, and the work runs correctly over the assets that are left. A raise under strict refuses the departure, and the reduction to the Investable Mask exists to remove that refusal. warn = true gives a @warn for the one case that is not routine, a departure that removed the last of something the caller asked for, such as the last view of a view set. The result is then the unconditioned answer, and the caller has no other way to learn it.
An empty ni logs nothing. This is the path when every asset is investable and when the data has no names. An empty drops gives a message that names the departed assets and no drops, which is what a door logs before it resolves anything over them.
Arguments
ni: The names the Investable Mask left out.drops: The ledger of casualties, asrecord_non_investable_drop!filled it.process: Noun phrase naming the work, for example"optimisation"or"entropy pooling fit".consequence: Sentence stating what a departure means to this door.warn: Raise the message to@warn, for a departure that changed the model rather than trimming it.
Returns
nothing.
Related
PortfolioOptimisers.counterpart_axis_names — Function
counterpart_axis_names(sets::UniverseSets, nxkey::AbstractString) -> VecStrReturn the asset axis that nxkey is the counterpart of, or an empty vector when it has none.
UniverseSets declares two axes over assets. The investable universe is under xkey, and the Non-Investable Axis is under nikey, which a door writes from the complement of the Investable Mask. A name-keyed estimator resolves against one of them. A name that it does not find there can still be a correct name on the other axis, for example a bound for an asset that left, or a forced-liquidation rate for an asset that stayed. name_to_val! reads this list to tell such a name from a typo.
The relation is symmetric and covers only these two axes. A factor axis names factors, so it has no counterpart and the answer is empty. A key that names any other list also gives an empty answer.
An axis that sets does not declare gives an empty answer. This is the common case, because a problem in which every asset is investable has no nikey entry.
Arguments
sets: TheUniverseSetswhose axes are read.nxkey: The key of the axis being resolved against.
Returns
other::VecStr: The counterpart axis, or an empty vector.
Related
PortfolioOptimisers.shed_departed_members — Function
shed_departed_members(members::AbstractVector, other::VecStr,
ledger::Option{<:AbstractVector}, group::AbstractString,
eqn::AbstractString) -> AbstractVector
shed_departed_members(members1::AbstractVector, members2::AbstractVector,
other::VecStr, ledger::Option{<:AbstractVector},
group::AbstractString, eqn::AbstractString) -> TupleRemove from the member list of a group the names that are on the counterpart axis, and record the removal in the ledger of the door.
The data resolves a group, and the caller does not choose its members. "tech" means the technology assets of this problem, and when one of them delists, the group still names the rest. replace_group_by_assets therefore sheds the departed members before it spreads the coefficient of the group. A Black-Litterman mean then divides by the surviving count, an entropy pooling sum runs over the survivors, and the row still computes what its right-hand side states. A removal after the spread leaves k - 1 terms of c/k against an unchanged target. A name that the caller writes out is different, because a departed name drops its whole row.
A group that loses every member keeps its first member and does not return empty. The row then names a departed asset, so get_linear_constraints drops the whole row in silence, by the counterpart rule. An empty return leaves a row with no variable, the same row as a caller who writes 1 == 0.004, and that row gets a diagnostic. A group that holds no member at all stays empty.
The second method is the pair form, for a correlation view over two groups. It walks the two lists together and keeps a position only when both of its names survived. A pair is one correlation, so a pair that lost one side has nothing to measure. The joint shed also keeps the two lists the same length, which replace_group_by_assets checked before. When no pair survives, the method keeps the first pair, for the same reason as the first method.
An empty other means that every asset is investable. Both methods then return their arguments unchanged and record nothing, so a problem with no departure costs one comparison and no allocation.
The record is written here and not at the four call sites, so each branch of replace_group_by_assets needs one line for the shed, the record and the fallback. Four copies of those steps can drift apart.
Arguments
members/members1,members2: The group's member names, assets.dictholds them.other: The counterpart axis, read withcounterpart_axis_names. Usually the Non-Investable Axis.ledger: The door's ledger, ornothingwhen nobody is collecting.group: The group name as the caller wrote it, for the ledger.eqn: The row the group appears in, for the ledger.
Returns
members::AbstractVector: The members that are not onother, in their original order, or the first departed member when none survived. An emptymembersstays empty.(members1, members2)::Tuple: The pair form, restricted to the positions both lists survived, or the first pair when none did.
Related