Linear Constraints (a): private API

PortfolioOptimisers.AbstractParsingResult — Type
abstract type AbstractParsingResult <: AbstractConstraintResult

Abstract 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

source
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

source
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

  1. Collect the entries of ps that are not nothing, giving kept.
  2. Return nothing when kept is empty, because the half is absent from every input.
  3. Read the row width of the first entry of kept, giving N, and check every other entry against it.
  4. Stack the A matrices of kept in input order, and stack their B vectors the same way.
  5. Return the PartialLinearConstraint built from the two stacks.

Arguments

Validation

  • Every kept half is written over the same number of variables, size(p.A, 2) == N. A DimensionMismatch is thrown otherwise.

Returns

Related

source
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

  1. Return the one element unchanged when lcs holds a single constraint.
  2. Merge the ineq half of every element with merge_partial_linear_constraints, giving the inequality half of the result.
  3. Merge the eq half of every element the same way, giving the equality half.
  4. Return the LinearConstraint built from the two halves.

Arguments

  • lcs: The constraints to merge.

Validation

  • lcs is 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 ┴ nothing

Related

source
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

  1. Return the keys of dict that start with prefix, as strings, in the order dict iterates in.

Arguments

  • dict: The UniverseSets dictionary being validated.
  • prefix: The axis prefix the missing key must carry, xkey, tfkey or cfkey.

Returns

  • candidates::Vector{String}: The keys of dict that start with prefix.

Related

source
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

  1. Return the keys of dict that start with no entry of claimed, as strings, in the order dict iterates in.

Arguments

  • dict: The UniverseSets dictionary being validated.
  • claimed: The other declared axis prefixes, uxkey, tfkey, utfkey, cfkey and ucfkey.

Returns

  • candidates::Vector{String}: The keys of dict that start with no entry of claimed.

Related

source
PortfolioOptimisers.assert_factor_partition — Function
assert_factor_partition(dict::AbstractDict, k::AbstractString, fkey::AbstractString,
                        axis::AbstractString) -> Nothing

Assert 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: The UniverseSets dictionary being validated.
  • k: The fkey-prefixed key under validation.
  • fkey: The factor axis key the prefix belongs to, tfkey or cfkey.
  • axis: Names the axis in both diagnostic messages, for example "time-series factor".

Validation

  • haskey(dict, fkey). A KeyError naming axis is thrown otherwise.
  • length(dict[k]) == length(dict[fkey]). A DimensionMismatch naming axis is thrown otherwise.

Returns

  • nothing.

Related

source
PortfolioOptimisers.assert_factor_unique_group — Function
assert_factor_unique_group(dict::AbstractDict, k::AbstractString, fkey::AbstractString,
                           ufkey::AbstractString, axis::AbstractString) -> Nothing

Assert 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: The UniverseSets dictionary being validated.
  • k: The ufkey-prefixed key under validation.
  • fkey: The factor axis key the group summarises, tfkey or cfkey.
  • ufkey: The unique-entry prefix k carries, utfkey or ucfkey.
  • axis: Names the axis in every diagnostic message, for example "cross-sectional factor".

Validation

  • haskey(dict, fkey). A KeyError naming axis is thrown otherwise.
  • haskey(dict, fkey * chopprefix(k, ufkey)). A KeyError carrying a spelling suggestion is thrown otherwise.
  • length(dict[fkey * chopprefix(k, ufkey)]) == length(dict[fkey]). A DimensionMismatch is thrown otherwise.

Returns

  • nothing.

Related

source
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

  1. Resolve key through resolve_axis_name, giving members. 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.
  2. Return in silence when members is nothing and key names an entry of other, because the name is on the counterpart axis. It is a correct name, and this axis has no entry to write it into.
  3. Report through strict_diagnostic and return when members is nothing, because key names neither an asset nor a group. The suggestion pool is widened from nx to nx together with the keys of sdict, because a missing name may be a mistyped asset or a mistyped group.
  4. Map members to positions in nx with axis_name_indices, giving idx, and drop the members that miss the universe. Remove from the report the members on other, by the rule of step 2, and report the rest once through strict_diagnostic. A group whose departed members are all on other is therefore silent, and a group that holds a real typo names it.
  5. Set the entries of arr at idx to val.

Arguments

  • nx: Vector of asset names.
  • sdict: Dictionary mapping group names to vectors of asset names. It is never modified, because resolve_axis_name returns 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: If true, a key that resolves to nothing throws. If false, it warns.
  • nxkey: The key of the asset universe in sets.dict, for example "nx". The messages of unknown_variable_msg and missing_group_assets_msg read it to name the universe.
  • other: The counterpart asset axis. The function skips its names in silence and does not report them.

Validation

  • key names an asset of nx, a group of sdict, or an entry of other. An ArgumentError is thrown when strict is true, and a warning is issued otherwise.
  • Every member of a resolved group names an entry of nx or of other. A member that misses both is dropped, and the drop raises when strict is true and issues a warning otherwise.

Returns

  • nothing. The function changes arr in place.

Related

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

  1. When expr is Nothing, which an empty side gives, raise and name side in the message.
  2. When expr is an Expr, raise when its head is :incomplete, and return nothing otherwise.
  3. When expr is anything else, a number or a symbol among them, return nothing.

Arguments

  • expr: The parsed Julia expression to check. Can be an Expr, Nothing, or any other type.
  • side: The side of the equation, :lhs or :rhs, which the message names.

Validation

  • expr is not Nothing. A Meta.ParseError naming side is thrown otherwise.
  • expr.head != :incomplete. A Meta.ParseError naming side and the expression is thrown otherwise.

Returns

  • nothing.

Related

source
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

  1. Return the variable name alone when coeff is one.
  2. Return the variable name behind a minus sign when coeff is minus one.
  3. 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

source
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

  1. Append (coeff * expr, nothing) when expr is a Number, so a constant carries the coefficient and no variable.
  2. Append (coeff, string(expr)) when expr is not a call. A Symbol is 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.
  3. For a multiplication, multiply coeff by every numeric factor, giving c. The parser writes 2*3*x as 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 with c. 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).
  4. For a division a / b, recurse into a with coeff divided by b, when b is a number.
  5. For an addition, recurse into every argument with coeff unchanged.
  6. 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.
  7. 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 as sqrt(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, or nothing in place of the name for a constant term.

Returns

  • nothing. The function changes terms in place.

Related

source
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

  1. Open an empty vector terms.
  2. Walk expr with collect_terms!, from the starting coefficient one(datatype). The walk appends one pair to terms per term that it reaches. A constant gives (coefficient, nothing), and any other term gives (coefficient, name).
  3. 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, where variable is a string for variable terms or nothing for constant terms. The vector is untyped, and each coefficient has the type that the arithmetic of the walk gives from one(datatype).

Related

source
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

  1. Fold every argument of expr first, by applying this function to each, when expr is an Expr. A node whose head is not :call is rebuilt from its folded arguments and returned.
  2. Rebuild a :call node whose head is prior from its folded arguments, and return it. The marker names assets or groups, so it is never folded to a number.
  3. Look the head of any other :call node up in allowed_functions, giving f. A head that the table does not hold raises.
  4. Rebuild the call and return it when any folded argument is not a Number, so a nonlinear subexpression keeps its own literals untouched.
  5. Coerce every folded argument to datatype, apply f to them, and return the value that comes back.
  6. Return the Float64 value Inf when expr is the symbol :Inf. Return expr itself 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 a Number, Symbol, or Expr.
  • datatype: The number type that the arguments of an evaluated call are converted to.

Validation

  • The head of a :call node is a key of allowed_functions, or the prior marker. A Meta.ParseError naming the head is thrown otherwise.
  • prior(...) carries at least one argument that is not a number. A Meta.ParseError is 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

source
PortfolioOptimisers.factor_universe — Function
factor_universe(sets::UniverseSets, key::AbstractString, K::Integer,
                need::AbstractString, source::AbstractString) -> VecStr

Read 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: The UniverseSets whose factor axis is read.
  • key: The factor axis key to read, sets.tfkey or sets.cfkey.
  • K: The number of columns of source, 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). A KeyError naming need is thrown otherwise.
  • length(sets.dict[key]) == K. A DimensionMismatch naming source is thrown otherwise.

Returns

  • nf::VecStr: The declared factor names, in the column order of source.

Related

source
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) -> AbstractString

Return 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: The UniverseSets whose factor axis key is read.
  • rr / re: The loadings result, or the specification whose verb produces one.

Returns

  • key::AbstractString: sets.tfkey for the time-series family, sets.cfkey for the cross-sectional one.

Related

source
PortfolioOptimisers.non_investable_sets — Function
non_investable_sets(sets::Nothing, ni) -> Nothing
non_investable_sets(sets::UniverseSets, ni::VecStr) -> UniverseSets

Write 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

  1. Return sets unchanged when ni is empty.
  2. Otherwise copy sets.dict, write ni under sets.nikey, and rebuild the UniverseSets from it and the seven unchanged key prefixes, which revalidates uniqueness and disjointness over the minted axis.

Arguments

  • sets: The UniverseSets to write the axis on, or nothing.
  • ni: The names the Investable Mask left out, in the order the complement of the mask visits them.

Returns

  • sets: The UniverseSets carrying the Non-Investable Axis, or nothing.

Related

source
PortfolioOptimisers.non_investable_names — Function
non_investable_names(nx::Nothing, imsk::BitVector) -> VecStr
non_investable_names(nx::VecStr, imsk::BitVector) -> VecStr

Read 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, or nothing.
  • imsk: The Investable Mask the optimisation reduced on: true at every asset whose prior moments were finite. It is nothing when every asset was investable, and that sentinel is what skips both the reduction and the expansion. investable_mask derives 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

source
PortfolioOptimisers.record_non_investable_drop! — Function
record_non_investable_drop!(ledger::Nothing, what::AbstractString) -> Nothing
record_non_investable_drop!(ledger::AbstractVector, what::AbstractString) -> Nothing

Record 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, or nothing when 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

source
PortfolioOptimisers.record_group_shed! — Function
record_group_shed!(ledger::Option{<:AbstractVector}, group::AbstractString,
                   shed::Integer, kept::Integer, eqn::AbstractString) -> Nothing

Record 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, or nothing when 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

source
PortfolioOptimisers.announce_non_investable — Function
announce_non_investable(ni::VecStr, drops::VecStr = String[],
                        process::AbstractString = "optimisation",
                        consequence::AbstractString = "…";
                        warn::Bool = false) -> Nothing

Announce, 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, as record_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

source
PortfolioOptimisers.counterpart_axis_names — Function
counterpart_axis_names(sets::UniverseSets, nxkey::AbstractString) -> VecStr

Return 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: The UniverseSets whose axes are read.
  • nxkey: The key of the axis being resolved against.

Returns

  • other::VecStr: The counterpart axis, or an empty vector.

Related

source
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) -> Tuple

Remove 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, as sets.dict holds them.
  • other: The counterpart axis, read with counterpart_axis_names. Usually the Non-Investable Axis.
  • ledger: The door's ledger, or nothing when 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 on other, in their original order, or the first departed member when none survived. An empty members stays empty.
  • (members1, members2)::Tuple: The pair form, restricted to the positions both lists survived, or the first pair when none did.

Related

source