No Risk: private API

PortfolioOptimisers.norisk_flagFunction
norisk_flag(r) -> Bool

Return true when r is a NoRisk, or a collection holding one.

The quantifier is any, not all, and it is earned on the hierarchical path. HierarchicalRiskParity and HierarchicalEqualRiskContribution allocate by inverse risk, so a single NoRisk can drive a divisor to zero even with a real measure beside it. NoRisk's functor returns zero, so its unitary risks are all zero; HRP then forms alpha = 1 - lrisk / (lrisk + rrisk) and HERC forms inv.(risk) ./ sum(inv.(risk)).

Measured on r = [NoRisk(), Variance()] over a 200×4 return matrix:

ScalariserHRP alphaHERC inner weights
SumScalariserfiniteNaN
MinScalariserNaNNaN
MaxScalariserfinitefinite
LogSumExpScalariserfinite, near 0.5NaN

Only MaxScalariser escapes on both, being the one reduction that can discard the zero term. HERC fails under three of the four: inv(0) is Inf and Inf / Inf is NaN, and that NaN is produced inside the per-measure closure, so every scalariser that combines the terms propagates it. HRP fails only under MinScalariser, where the aggregate is itself zero and alpha becomes 0 / 0; under LogSumExpScalariser it stays finite but collapses towards 0.5, an uninformative split rather than an error.

settings.rke is inert on the hierarchical path, so a measure's type is the only signal available there. An all quantifier would admit every NaN row above.

This is not the right quantifier for the JuMP axis, where the question is whether the :risk expression is identically zero. See zero_risk_expression_flag.

Related

source
PortfolioOptimisers.zero_risk_expression_flagFunction
zero_risk_expression_flag(r) -> Bool

Return true when the model's :risk expression is identically zero.

The degeneracy guard tests the state of the expression, not the type of the term. Two routes reach that state, and this predicate is the disjunction of both:

  1. A NoRisk measure is present. This is norisk_flag, whose quantifier is any — earned on the hierarchical path, where one NoRisk zeroes a divisor under three of the four scalarisers, and documented there. On this axis any is wider than the state it names: [NoRisk(), Variance()] leaves :risk non-zero, yet the predicate returns true, so the pair is refused under MinimumRisk and MaximumRatio with a cause string that overstates the case. The refusal is kept because the configuration is vestigial — a NoRisk beside a real measure buys nothing, which is the opposite of what NoRisk exists for — but it is a type test surviving inside a state test.
  2. Every measure carries settings.rke = false, so set_risk_expression! pushes nothing. The quantifier here is all, because one included measure leaves the expression non-zero.

The halves compose only because route 1's any subsumes the mixed case that a composed test would otherwise miss. The exact mirror of the return axis is all(isa NoRisk || !rke), which agrees with this predicate everywhere except [NoRisk(), Variance()], where the fused form is the accurate one. The return axis must fuse because it has no such subsuming half; see zero_return_expression_flag.

all on the state route keeps a constraint-only measure expressible, which is the risk side of the term set_return_bounds! supports on the return side:

r = [Variance(), Variance(; settings = RiskMeasureSettings(; rke = false, ub = u))]

The second measure binds a ub without entering the objective, and this predicate does not refuse it.

Related

source
PortfolioOptimisers.assert_risk_measure_requiredFunction
assert_risk_measure_required(r, T::Symbol; flag)

Assert that r gives a non-zero risk expression, for optimisers built around one.

A zero risk expression is only coherent in MeanRisk, under an objective that never consults it. Every other risk-taking optimiser is its risk measure — a risk budget with nothing to budget, a risk contribution that is always zero, a clustering optimiser dividing by a zero risk — so they reject it rather than return a degenerate answer.

flag selects the predicate, because the two families of caller do not see the same routes:

T names the calling optimiser, for the error message. TimeDependent schedules are skipped here and reached instead through assert_time_dependent_substitution.

Related

source
PortfolioOptimisers.assert_no_risk_objective_compatibilityFunction
assert_no_risk_objective_compatibility(r, obj)

Assert that a zero risk expression is paired with an objective that ignores risk.

Rejects a zero :risk under MinimumRisk — whose objective would be identically zero, so the solver could return any feasible portfolio, silently — and under MaximumRatio, whose risk-normalisation constraint would go vacuous and leave the model unbounded.

The criterion is zero_risk_expression_flag, so the guard covers both routes to a zero expression: a NoRisk measure, and every measure carrying settings.rke = false. The second route shipped unguarded since the inclusion flag was introduced.

Called from MeanRisk's constructor. TimeDependent schedules are skipped here and reached instead through assert_time_dependent_substitution, which re-runs the constructor on each scheduled entry.

Related

source