Base Phylogeny
PortfolioOptimisers.AbstractPhylogenyEstimator — Type
abstract type AbstractPhylogenyEstimator <: AbstractEstimatorAbstract supertype for all phylogeny estimator types.
All concrete and/or abstract types implementing phylogeny-based estimation algorithms should be subtypes of AbstractPhylogenyEstimator.
Related
PortfolioOptimisers.AbstractPhylogenyAlgorithm — Type
abstract type AbstractPhylogenyAlgorithm <: AbstractAlgorithmAbstract supertype for all phylogeny algorithm types.
All concrete and/or abstract types implementing specific phylogeny algorithms should be subtypes of AbstractPhylogenyAlgorithm.
Related
PortfolioOptimisers.AbstractPhylogenyResult — Type
abstract type AbstractPhylogenyResult <: AbstractResultAbstract supertype for all phylogeny result types.
All concrete and/or abstract types representing the result of a phylogeny estimation should be subtypes of AbstractPhylogenyResult.
Related
PortfolioOptimisers.PlE_Pl — Type
const PlE_Pl = Union{<:AbstractPhylogenyEstimator, <:AbstractPhylogenyResult}Alias for a phylogeny estimator or result.
Matches either an AbstractPhylogenyEstimator or an AbstractPhylogenyResult. Used internally for dispatch when either a phylogeny estimation configuration or pre-computed result is accepted.
Related
PortfolioOptimisers.factory — Method
factory(
pl::Union{AbstractPhylogenyEstimator, AbstractPhylogenyResult},
args...;
kwargs...
) -> NetworkEstimator{<:CovarianceEstimator, <:AbstractDistanceEstimator, KruskalTree{Tuple{}, @NamedTuple{}}, <:AbstractSeparationAlgorithm}
Return the phylogeny estimator or result pl unchanged.
Identity pass-through used when a phylogeny estimator or pre-computed result is provided in a context that calls factory.
Related
PortfolioOptimisers.factory — Method
factory(
alg::AbstractPhylogenyAlgorithm,
args...;
kwargs...
) -> AbstractClustersAlgorithm
Return the phylogeny algorithm alg unchanged.
Identity pass-through used when a phylogeny algorithm is provided in a context that calls factory.
Related
PortfolioOptimisers.AbstractSeparationAlgorithm — Type
abstract type AbstractSeparationAlgorithm <: AbstractAlgorithmAbstract supertype for all separation algorithms.
A separation algorithm is the rule saying how far apart two assets sit in a network, and how far is too far. It answers two questions with one object, through three kernels: separation_graph builds the structure the member measures over, separation_matrix reads the dense assets × assets separations off that structure, and separation_budget resolves the budget beyond which a pair counts as unrelated. The family is open: a new member is a struct and one method of each.
The two questions travel together because they share a unit. A hop count is budgeted in hops and a weighted path length in the distance estimator's units, so a budget stated apart from the rule that measures it would be a number nobody could interpret — which is why the budget lives on the member rather than on NetworkEstimator.
Building the structure is a separate kernel from measuring it
The measuring kernel takes the structure, not the estimator that produces one: separation_matrix(sep, g) is the interface, and separation_matrix(sep, nte, X) is a wrapper that calls separation_graph first. The split is calc_weighted_adjacency_graph's two-entry-point shape, for the same reason — the structure is expensive and a caller often holds one already. Under VariationInfoDistance building it is 98% of clusterise's runtime, so a consumer that resolves a budget rule and measures the separations must build once and pass the graph, not call two estimator-taking kernels.
It is also the seam a test or an extension enters through. Every structure a shipped estimator can build is connected — a spanning tree or a PMFG — so a disconnected graph, and with it the unreachable sentinel below, is reachable only by handing one in.
Two more kernels, and why neither is a third question
resolve_separation turns a member whose budget is a rule into one whose budget is a value, and is called by every consumer before the other two kernels. It is not a third question about the network: a member whose budget is already a number is returned unchanged by the fallback on this type, so an extension inherits the kernel and never writes one.
is_related applies the budget to one entry of the separation matrix, over is_reachable's sentinel test. Both are single generic methods on this type rather than per-member ones, because the rule — not the sentinel, and no further than the budget — is the same rule whatever the unit; a member whose underlying routine reports an exotic sentinel overrides is_reachable alone. Every consumer applying a budget calls them instead of open-coding the comparison, which is what keeps the ordering obligation of separation_matrix's "the unreachable sentinel" inside an interface.
The two shipped members widen their budget field to admit a rule — HopCountValue and PathLengthValue — so a caller who cannot state the budget in advance states what would produce it instead. The resolution happens where the data is in hand, which is the only place a rule can be answered, and it is why separation_budget refuses an unresolved member rather than returning a function.
Separation is not decay
AbstractSeparationDecayAlgorithm turns a separation into a score; this family produces the separation and says where it runs out. The seam is that sep decides which pairs are related — every consumer of a network needs that — while decay decides how strongly, as a number, which only the feature producer wants. That is why sep sits on NetworkEstimator and decay sits on Proximity.
The family is unqualified on purpose
The name says nothing about graphs. A taxonomy depth is a separation too, so the room is left for a member that measures one, rather than being closed off by an AbstractGraphSeparationAlgorithm.
Related
PortfolioOptimisers.HopCountAlgorithm — Type
abstract type HopCountAlgorithm <: AbstractAlgorithmAbstract supertype for all rules computing a hop count from the network and the data.
A HopCount budget is usually a number the caller states. It does not have to be. A subtype of HopCountAlgorithm is a callable struct standing in the n field, and resolve_separation calls it with the network estimator and the data matrix in hand. That is what lets a budget follow a universe whose size the caller cannot know in advance — a cross-validation fold, or a subproblem of a meta optimiser such as NestedClustered.
The extension contract
A subtype defines one method, the functor:
(rule::MySubtype)(nte::AbstractNetworkEstimator, X::MatNum, g::Graphs.AbstractGraph; dims::Int = 1, kwargs...) -> Integerg is the structure separation_graph built for the separation the rule stands in, so a rule reads what it needs off a graph it did not pay to build — through separation_matrix, which takes g directly. It still pays for the all-pairs traversal; see resolve_separation.
nte owns the separation and X is the data g was built from. Both are inert for the shipped rule, and are the channel through which an extension reaches what the graph does not carry — the distance estimator, the observation count, a covariance.
The return value must be an Integer, and this is checked rather than bounded. A functor's return type is not part of its signature, so the family cannot state the requirement in the type system. resolve_separation checks it instead, and the check is not a formality: three readers use 0:n as a matrix-power count, where 0:1.5 silently drops a power rather than failing.
A bare Function is admitted in the same field and carries the same obligation, unchecked at construction. Subtype this instead when the rule has parameters — the struct holds them, prints them, and is comparable.
Related
PortfolioOptimisers.PathLengthAlgorithm — Type
abstract type PathLengthAlgorithm <: AbstractAlgorithmAbstract supertype for all rules computing a path-length budget from the network and the data.
The PathLength counterpart of HopCountAlgorithm: a callable struct standing in the dmax field, called by resolve_separation with the network estimator, the data matrix, and the structure already built from them in hand.
The two families are separate because their return obligations differ, and the split is what lets one of them be checked. A hop count must be an Integer; a path-length budget is stated in the distance estimator's units, so it is any Number — or nothing, which resolves to the observed diameter exactly as a stated nothing does.
The extension contract
A subtype defines one method, the functor:
(rule::MySubtype)(nte::AbstractNetworkEstimator, X::MatNum, g::Graphs.AbstractGraph; dims::Int = 1, kwargs...) -> Numberg is separation_graph's structure, weighted by distance on both branches under a PathLength. A bare Function is admitted in the same field and carries the same obligation, unchecked at construction.
A rule must return a Number, and nothing is not one. nothing in the dmax field means the observed diameter, which is a statement the caller makes instead of stating a rule. A rule that meant to ask for the diameter is asking for something the field already spells, and a rule that returned nothing by accident would silently get the maximal ball. So PathLengthValue covers the rules and the numbers, and the field is an Option of it.
Related
PortfolioOptimisers.HopCountRule — Type
const HopCountRule = Union{<:HopCountAlgorithm, <:Function}Alias for the dynamic forms of a hop count.
Matches the two things resolve_separation calls rather than reads: a HopCountAlgorithm and a bare Function. Used for dispatch, so that HopCount{<:HopCountRule} names an unresolved separation and HopCount{<:Integer} a resolved one.
Related
PortfolioOptimisers.HopCountValue — Type
const HopCountValue = Union{<:Integer, <:HopCountAlgorithm, <:Function}Alias for everything HopCount's n field accepts.
Widens the field from the stated Integer to the rules of HopCountRule as well. The Integer case is the resolved one and every reader takes it directly; a rule is resolved by resolve_separation before any reader sees it.
Related
PortfolioOptimisers.PathLengthRule — Type
const PathLengthRule = Union{<:PathLengthAlgorithm, <:Function}Alias for the dynamic forms of a path-length budget.
The PathLength counterpart of HopCountRule.
Related
PortfolioOptimisers.PathLengthValue — Type
const PathLengthValue = Union{<:Number, <:PathLengthAlgorithm, <:Function}Alias for everything PathLength's dmax field accepts, apart from nothing.
The PathLength counterpart of HopCountValue, and the field is Option{PathLengthValue} rather than this alias alone. The asymmetry is deliberate: nothing in that field means the observed diameter, which is one of the stated budgets and not something a rule may answer with. Keeping it outside the alias is what makes resolve_separation's check a plain isa(dmax, Number).
Related
PortfolioOptimisers.HopCount — Type
struct HopCount{__T_n} <: AbstractSeparationAlgorithmSeparation measured as the number of graph edges between two assets.
The separation between two assets is the length of the shortest path between them counted in edges, ignoring the weights those edges carry, and the budget is n of them. It is the separation the network family has always used: phylogeny_matrix's sum(A^i for i in 0:n) and both clusterise methods' power sums are hop budgets, and this member is where that n now lives.
The budget is a field rather than an argument because it is stated in hops, a unit only this member uses. PathLength measures the same structure in the distance estimator's units and carries its own budget in those, so no caller has to know which unit is in play.
The power sum is the source's range connection matrix
phylogeny_matrix's $\mathbf{P} = \mathbb{1}_{x \geq 1}\left(\sum_{i=0}^{n} \mathbf{A}^{i}\right) - \mathbf{I}$ is the range connection matrix $\mathbf{B}_{1,n}$ of the source, spelled with one indicator instead of n of them. The source builds a per-length connection matrix $\mathbf{B}_{k} = \mathbb{1}_{x \geq 1}(\mathbf{A}^{k} + \mathbf{I}) - \mathbf{I}$ and then indicates their sum; the library adds the powers first, and the $\mathbf{A}^{0} = \mathbf{I}$ term the sum picks up is the term the trailing $- \mathbf{I}$ removes again. The two agree entry for entry: on the source's own six-node example the matrices are identical for every n from 1 to 5.
The budget may be a rule instead of a number
n also takes a HopCountAlgorithm or a bare Function, which resolve_separation calls as n(nte, X, g; dims = dims, kwargs...) at the point of use, g being the structure the consumer already built. A caller who cannot state the budget in advance — because the universe is a cross-validation fold or a subproblem of a meta optimiser — states the rule that produces it instead of a number that was right for one universe. HopCountQuantile is the shipped rule.
n is still an Integer once resolved, and never a Real. Three readers use 0:(nte.sep.n) as a matrix-power count, where 0:1.5 silently drops a power instead of failing, so resolve_separation checks the rule's return value rather than trusting it.
Fields
n: Number of steps to take in the network for deciding adjacency. AnIntegeris used as it stands. AHopCountAlgorithmor aFunctionis a rule, called asn(nte, X, g; dims = dims, kwargs...)byresolve_separationat the point of use,gbeing the structure the consumer already built, and must return anInteger.
Constructors
HopCount(; n::HopCountValue = 1) -> HopCountKeywords correspond to the struct's fields.
Validation
- If
nis anInteger,1 <= n <= RESOURCE_LIMITS[].max_hop_count(three readers sumA^ioveri in 0:n, so the compute cost is linear inn; seeRESOURCE_LIMITS). A rule is checked when it is resolved, not when it is stored.
Examples
julia> HopCount()HopCount n ┴ Int64: 1julia> HopCount(; n = HopCountQuantile())HopCount n ┼ HopCountQuantile │ q ┴ Float64: 0.25Related
AbstractSeparationAlgorithmPathLengthHopCountValueHopCountAlgorithmHopCountQuantileresolve_separationseparation_matrixseparation_budgetNetworkEstimatorProximity
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 13.1.2, Equations 13.1-13.2.
PortfolioOptimisers.PathLength — Type
struct PathLength{__T_dmax} <: AbstractSeparationAlgorithmSeparation measured as the length of the shortest weighted path between two assets.
The separation between two assets is the sum of the distances along the shortest path joining them in the network, and the budget is dmax of the same units. It is the graded counterpart of HopCount: both measure how far apart two assets sit in the same structure, but one counts the edges and the other adds up how long they are.
It is a library generalisation and rests on no published source. HopCount has one — it is the range connection matrix of a walk length — but that literature counts edges throughout, and states no budget in the units a distance estimator emits.
The path runs over distances on both branches
The path is taken over the distance matrix restricted to the structure's edge set — calc_distance_weighted_graph — whichever branch built the structure. On the tree branch that is the graph's own weights; on the PMFG branch the structure is selected by similarity and then re-weighted by the distance that the similarity is a strictly decreasing function of.
Pathing over the PMFG's similarities instead is not a second convention, it is backwards: a shortest path over similarities minimises total similarity, so it prefers the route through the weakest links. It fails quietly — measured over the four similarity algorithms, the backwards answer correlates 0.95 to 0.97 with the right one, which is far too close to catch by looking.
It is not comparable with a hop count, only interchangeable with one
PathLength and HopCount satisfy the same contract, so any consumer reading a separation through that contract takes either — Proximity and phylogeny_matrix both do. Their outputs are not comparable as values: the budgets are in different units, the supports differ, and under LinearDecay the scales differ.
Both clusterise methods are the exception, and refuse PathLength at dispatch. They do not read the separation through the contract at all: they index a matrix power by sep.n, and a radius has no analogue of one.
On a real universe the two agree far more than that suggests — measured over twenty assets, rho = 0.99 on a minimum spanning tree and 0.95 to 0.98 on a PMFG, with 0.16% of pairs of pairs strictly inverted on the tree and none at all on the PMFG — because both structures are selected by distance to begin with. That agreement is empirical, not guaranteed: it is a fact about the graphs an AbstractDistanceEstimator tends to produce, not a property of either separation.
The budget
dmax = nothing is the default and means the whole connected component, implemented as the observed diameter: the largest finite entry of the separation matrix. It is the default because nobody has an intuition for a summed path in the units an AbstractDistanceEstimator emits — dmax = 0.37 is not a number a caller can reason about, whereas "look at the whole component and let the decay do the falling off" is. Choosing a number is how a caller buys fold-stability.
separation_budget clamps a chosen dmax to the observed diameter. The clamp cuts nothing — no pair sits beyond the diameter — so it is a scale-top correction and bites only LinearDecay, the one decay that reads the budget: without it, a dmax far above the diameter would flatten Z towards a constant while forbidding no pair at all.
The default reads very differently through phylogeny_matrix, which selects on the budget instead of shaping a fall-off inside it. "The whole connected component" there means every reachable pair is related, so NetworkEstimator(; sep = PathLength()) yields a matrix of ones off the diagonal — the opposite end of the dial from HopCount's default n = 1. State a numeric dmax to select anything narrower.
The budget may be a rule instead of a number
dmax also takes a PathLengthAlgorithm or a bare Function, which resolve_separation calls as dmax(nte, X, g; dims = dims, kwargs...) at the point of use, g being the structure the consumer already built. This is the answer to the paragraph above for a caller who cannot state a number: PathLengthQuantile asks for the budget that relates a stated fraction of the reachable pairs, which is a quantity a caller does have an intuition for, and which means the same thing on every fold of a cross-validation and in every subproblem of a meta optimiser.
A fixed dmax and a rule buy different things, and the difference is the whole point. A fixed dmax holds the radius still and lets the related-pair count move with the graph. A quantile rule holds the count still and lets the radius move. Neither is fold-stable in both senses at once, because the graph is refitted either way.
Fields
dmax: Separation budget, in the units the separation is measured in.nothingmeans the observed diameter of the structure. APathLengthAlgorithmor aFunctionis a rule, called asdmax(nte, X, g; dims = dims, kwargs...)byresolve_separationat the point of use,gbeing the structure the consumer already built, and must return aNumber.
Constructors
PathLength(; dmax::Option{PathLengthValue} = nothing) -> PathLengthKeywords correspond to the struct's fields.
Validation
- If
dmaxis aNumber,dmax > 0. A rule is checked when it is resolved, not when it is stored.
Examples
julia> PathLength()PathLength dmax ┴ nothingjulia> PathLength(; dmax = 0.5)PathLength dmax ┴ Float64: 0.5julia> PathLength(; dmax = PathLengthQuantile(; q = 0.3))PathLength dmax ┼ PathLengthQuantile │ q ┴ Float64: 0.3Related
PortfolioOptimisers.is_reachable — Function
is_reachable(sep::AbstractSeparationAlgorithm, d::Number)Is d a separation at all, or the sentinel an unreachable pair carries?
separation_matrix passes the underlying routine's sentinel through unrepaired, so this is the test that tells a measured separation from a missing one. It is one generic method on AbstractSeparationAlgorithm rather than one per member, because the two shipped sentinels are both covered by the same expression; a member whose routine reports something else overrides this method, and inherits is_related unchanged.
The test is not isfinite alone, and not typemax alone
Both clauses carry a sentinel of their own.
isfiniteistruefor everyInteger, so on its own it admitsHopCount'stypemax(Int)— whichReciprocalDecaythen overflows.typemaxof aFloat64isInf, so the comparison coversPathLength's sentinel as well;isfinitestays to reject aNaN, which no shipped path produces and which would comparefalseagainst every budget anyway.
Arguments
sep: Separation algorithm. Inert for the shipped members, and the dispatch channel for an extension whose routine reports a different sentinel.d: One entry of a separation matrix fromseparation_matrix.
Returns
reachable::Bool:truewhendis a measured separation.
Related
PortfolioOptimisers.is_related — Function
is_related(sep::AbstractSeparationAlgorithm, d::Number, dmax::Number)Does a separation of d count as related under a budget of dmax?
The one place the budget is applied to an entry of a separation matrix: reachable, and no further than dmax. Every consumer that selects on a budget calls this instead of writing the comparison out, so the rule has one spelling — phylogeny_matrix and phylogeny_features had two, and one of them was a budget test with no sentinel test behind it.
The reachability test comes first
d <= dmax is not sufficient on its own. It happens to reject both shipped sentinels, because separation_budget clamps a PathLength budget to the observed finite diameter and a HopCount budget is capped far below typemax(Int) — but that is a property of the two shipped budgets, not of the comparison. is_reachable makes the rejection the predicate's own, so a budget that reached its unit's ceiling would still exclude an unreachable pair.
It does not remove the caller's obligation to short-circuit
A consumer that scores the separation must still keep the evaluation of the score inside a short-circuiting branch — is_related(...) ? separation_decay(...) : zero(...), never ifelse — because an ifelse evaluates both arms and ReciprocalDecay overflows 1 + d at typemax(Int), which a fractional power turns into a DomainError. The predicate owns the rule; the call site owns the laziness.
Arguments
sep: Separation algorithm, forwarded tois_reachable.d: One entry of a separation matrix fromseparation_matrix.dmax: Separation budget in scope, fromseparation_budget. In the unitssepmeasures in, which is why the two arrive together.
Returns
related::Bool:truewhen the pair is reachable and inside the budget.
Related
PortfolioOptimisers.AbstractSeparationDecayAlgorithm — Type
abstract type AbstractSeparationDecayAlgorithm <: AbstractAlgorithmAbstract supertype for all separation decay algorithms.
A separation decay turns a separation d >= 0 — how far apart two assets sit in whatever structure the caller is reading — into a score, and is applied by separation_decay. The family is open: a caller wanting a different fall-off defines a member and a separation_decay method for it, exactly as AbstractSimilarityMatrixAlgorithm is extended through distance_to_similarity.
d is a real separation rather than an integer hop count, so one family serves an unweighted graph — where hop counts enter as integer-valued reals — and any structure whose separation is continuous.
The contract
- Defined for every
d >= 0. f(0) > 0and maximal. Self-inclusion is load-bearing rather than cosmetic: a decay that does not put an asset at the top of its own scale silently produces a structural equivalence matrix instead of a proximity one — seePhylogenyFeatures's "Why the diagonal includes self".- Monotone non-increasing in
d. - Never assumed to reach zero. Truncation is a separate knob: the consumer applies its own budget —
separation_budgetof theAbstractSeparationAlgorithmin scope — and the decay only shapes the fall-off inside it. An exponential never reaches zero, so budget and fall-off cannot be the same dial. f(d) >= 0for0 <= d <= dmax.0is the unreachable sentinel, so a negative score inside the budget would place a reachable pair strictly below an unreachable one — an ordering inversion within the producer's own scale. It is not a claim that a signed score is wrong in general: the feature matrix is signed-tolerant by decision, andassert_metric_domainchecks non-negativity per metric at the consumer rather than blanket. This clause is producer-local, and it is non-negativity rather than strict positivity because a decay that bottoms out at zero says no relatedness, which is the same claim an unreachable pair makes.
The clause is scoped to the budget because the sign outside it is unobservable — the consumer's h[u] <= n test short-circuits before the decay is ever evaluated there — and because the family's own default violates the wider statement: LinearDecay crosses zero at d = dmax + 1 and is negative above it. Binding the clause on all d >= 0 would need the max(0, ⋅) floor the budget knob exists to avoid, a second truncation biting before n does.
A zero in the resulting feature matrix therefore means functionally unreachable: either the graph is disconnected there, or the decay has fallen to nothing — the same claim about the pair, and nothing downstream can act on the difference. No shipped member emits zero anywhere inside the budget, so for what ships a zero is disconnection and nothing else.
The budget is an argument, not a field
separation_decay takes the budget in scope as its third argument, dmax, and members may ignore it — only LinearDecay reads it, to set f(0). Keeping it off the member is what makes the two knobs impossible to desync: the AbstractSeparationAlgorithm stays the single source of truth for the budget, rather than mirroring it on an algorithm that cannot see it at construction. ExponentialDecay provides the self-versus-neighbour contrast a free top-of-scale would have bought, without the hazard of a second truncation hiding inside the decay.
Enforcement
The contract is enforced rather than merely documented, by a probing assert_separation_decay fallback on this type. The shipped members satisfy it by construction and override that fallback to a no-op, so the check is opt-out: an extension that says nothing about itself gets probed.
Related
PortfolioOptimisers.LinearDecay — Type
struct LinearDecay <: AbstractSeparationDecayAlgorithmSeparation decay falling off linearly to the edge of the budget.
Mathematical definition
\[\begin{align} f(d) &= d_{\mathrm{max}} + 1 - d\,, \end{align}\]
Where:
- $d$: Separation between two assets.
- $d_{\mathrm{max}}$: Separation budget in scope.
The default, and the only member that reads the budget. It is the fall-off the graded neighbourhood hardcoded before the family existed, so it reproduces those values exactly: a direct neighbour scores $d_{\mathrm{max}}$, the asset itself $d_{\mathrm{max}} + 1$.
Because truncation lives with the budget rather than in the decay, no max(0, ⋅) floor is needed — on the kept range 0 <= d <= dmax the expression is strictly positive, bottoming out at 1.
Examples
julia> separation_decay.(Ref(LinearDecay()), 0:3, 3)4-element Vector{Int64}: 4 3 2 1Related
PortfolioOptimisers.ExponentialDecay — Type
struct ExponentialDecay{__T_rate} <: AbstractSeparationDecayAlgorithmSeparation decay falling off exponentially.
Mathematical definition
\[\begin{align} f(d) &= e^{-\lambda d}\,, \end{align}\]
Where:
- $d$: Separation between two assets.
- $\lambda$: Rate of the fall-off,
rate.
Pins f(0) = 1 and lets rate set the self-versus-neighbour contrast independently of the budget, which is what a caller wanting relatedness to drop sharply needs — the budget only says how far to look.
Parameterised by rate rather than by per-step retention. $\rho^d$ and $e^{-\lambda d}$ are the same function ($\lambda = -\log\rho$), but "retention per step" is a statement about integers, and the family's argument is a real separation. The rate form also matches ExponentialSimilarity and ExpGerberIQDecay, and needs only a one-sided bound to stay monotone.
Fields
rate: Rate of the exponential fall-off,exp(-rate * d). Larger values decay faster. The per-step retention form,ratio^d, israte = -log(ratio).
Constructors
ExponentialDecay(; rate::Number = 1.0) -> ExponentialDecayKeywords correspond to the struct's fields.
Validation
rate > 0.
Examples
julia> ExponentialDecay()ExponentialDecay rate ┴ Float64: 1.0julia> separation_decay.(Ref(ExponentialDecay()), 0:3, 3)4-element Vector{Float64}: 1.0 0.36787944117144233 0.1353352832366127 0.049787068367863944Related
PortfolioOptimisers.ReciprocalDecay — Type
struct ReciprocalDecay{__T_power} <: AbstractSeparationDecayAlgorithmSeparation decay falling off as a power of the separation.
Mathematical definition
\[\begin{align} f(d) &= \left(1 + d\right)^{-p}\,, \end{align}\]
Where:
- $d$: Separation between two assets.
- $p$: Exponent of the fall-off,
power.
The middle ground between LinearDecay and ExponentialDecay: heavier-tailed than the exponential, so distant assets keep a small but non-negligible score.
The 1 + is what makes classical inverse-distance weighting finite at $d = 0$; it also pins f(0) = 1, matching ExponentialDecay's scale for free. The alternative spelling $(1 + d^p)^{-1}$ is not used: it pins $f(1) = 1/2$ for every p and is non-monotone in p at fixed d, so raising the exponent would score near neighbours lower and far ones higher.
Fields
power: Exponent of the reciprocal fall-off,inv((1 + d)^power). Larger values decay faster.
Constructors
ReciprocalDecay(; power::Number = 1.0) -> ReciprocalDecayKeywords correspond to the struct's fields.
Validation
power > 0.
Examples
julia> ReciprocalDecay()ReciprocalDecay power ┴ Float64: 1.0julia> separation_decay.(Ref(ReciprocalDecay()), 0:3, 3)4-element Vector{Float64}: 1.0 0.5 0.3333333333333333 0.25Related
PortfolioOptimisers.NoDecay — Type
struct NoDecay <: AbstractSeparationDecayAlgorithmSeparation decay that does not fall off at all.
Mathematical definition
\[\begin{align} f(d) &= 1\,, \end{align}\]
Where:
- $d$: Separation between two assets.
No decay is not no truncation
The name is about the fall-off and nothing else. The budget still cuts: a pair outside it scores 0, because truncation was never the decay's job — see AbstractSeparationDecayAlgorithm's "The budget is an argument, not a field". What comes out is therefore an indicator of the neighbourhood the budget selects, not a matrix of ones.
That is exactly what makes it useful. Under HopCount it turns Proximity into the n-hop neighbourhood indicator, which is what the retired BinaryNeighbourhood produced; under PathLength it is an ε-ball, and neither needs a type of its own once the fall-off is a knob.
It is the flat end of the family, so it is the only member that is not strictly decreasing. The contract asks for monotone non-increasing, which a constant satisfies.
Examples
julia> separation_decay.(Ref(NoDecay()), 0:3, 3)4-element Vector{Int64}: 1 1 1 1Related
PortfolioOptimisers.separation_decay — Function
separation_decay(dk::LinearDecay, d::Number, dmax::Number)
separation_decay(dk::ExponentialDecay, d::Number, dmax::Number)
separation_decay(dk::ReciprocalDecay, d::Number, dmax::Number)
separation_decay(dk::NoDecay, d::Number, dmax::Number)Score a separation under a decay algorithm.
The whole extension contract of AbstractSeparationDecayAlgorithm: a new member is a struct and one method of this function.
Arguments
dk: Separation decay algorithm.d: Separation between two assets,d >= 0. Real rather than integral, so a weighted path length is as admissible as a hop count.dmax: Separation budget in scope. Inert for members that do not need it — onlyLinearDecayreads it. Inert arguments have precedent here:phylogeny_featuresignores itsalgentirely for a partition source.
Validation
The contract is not checked here — this runs inside an assets × assets loop. Callers probe once up front with assert_separation_decay instead.
Returns
f::Number: Score for the separation. Non-negative for0 <= d <= dmax; above the budget the sign is unconstrained andLinearDecaydoes go negative, which is harmless because the consumer's budget test short-circuits before the call.
Examples
julia> separation_decay(LinearDecay(), 2, 3)2julia> separation_decay(ExponentialDecay(; rate = 2.0), 2, 3)0.01831563888873418julia> separation_decay(ReciprocalDecay(; power = 2.0), 2, 3)0.1111111111111111julia> separation_decay(NoDecay(), 2, 3)1Related
PortfolioOptimisers.assert_separation_decay — Function
assert_separation_decay(dk::AbstractSeparationDecayAlgorithm, ds, dmax::Number)
assert_separation_decay(dk::Union{<:LinearDecay, <:ExponentialDecay, <:ReciprocalDecay,
<:NoDecay}, ds, dmax::Number)Check that a separation decay honours its contract over the separations it will be asked about.
The fallback probes: it evaluates separation_decay over ds and checks the result is finite, that f(0) is strictly positive and maximal, that the values are monotone non-increasing, and that none of them is negative. The four shipped members satisfy the contract by construction and override this to a no-op, so the probe costs nothing for what ships and is fail-safe for extensions.
Probing is cheap where it is used because ds is small and the loop it guards is not: Proximity passes 0:dmax, which under HopCount is exhaustive — every separation the assets × assets loop can ever ask about, in dmax + 1 evaluations. Under a separation whose budget is not an integer the same range is a unit-spaced sample, which is all a continuum admits and all the clauses below need.
Non-negativity gets one extra evaluation at d = dmax, whether or not dmax appears in ds, mirroring the out-of-loop evaluation of f(0). That endpoint is what closes the clause over a continuum: monotonicity is already promised, so f(dmax) >= 0 implies f(d) >= 0 for every d in [0, dmax], and a ds that can only ever be a sample — as it must be once separations are weighted path lengths — costs this clause nothing. Monotonicity itself gains nothing from the endpoint and remains genuinely sampled.
Arguments
dk: Separation decay algorithm.ds: Separations to probe. Need not be sorted. Precondition:ds ⊆ [0, dmax]—dsis what the guarded loop will ask about, and the loop never asks outside the budget.dmax: Separation budget in scope, forwarded toseparation_decayand probed as an endpoint in its own right.
Validation
- Every probed value is finite.
f(0) > 0.f(0) >= f(d)for every probedd.- The probed values are monotone non-increasing in
d. f(d) >= 0for every probedd, and atd = dmaxwhether or not it was probed.
Returns
nothing.
Related
References
- [4]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).