PortfolioOptimisers pipeline: private API

PortfolioOptimisers.first_duplicateFunction
first_duplicate(xs) -> Any

Return the first element that repeats an earlier one, for a name-uniqueness error that names the offending token without dumping the whole collection. Only ever called on the failing path.

Arguments

  • xs: A collection of names.

Returns

  • The first element that repeats an earlier one, or nothing when every element is unique.

Related

source

Injection

The pipeline resolves its computed slots into routing targets and hands each one to the optimiser, which owns the decision of where it lands. See pipe_route for the optimiser-owned half of the seam.

PortfolioOptimisers.inject_contextFunction
inject_context(
    opt::OptimisationEstimator,
    ctx::PipelineContext
) -> Any

Override an optimisation step's internal configuration with the computed slots of the pipeline context, immediately before the step runs.

This is the pipeline-owned half of the injection seam. It resolves everything that depends on the slots — which halves of the uncertainty pair are populated, which result types the constraints slot holds, how many of each — into a flat sequence of routing targets, then hands each one to pipe_route without knowing where it lands. Which optimiser field receives a target is the optimiser's business, so a field rename is a local edit rather than a break here.

Targets an optimiser has no home for are handled by unroutable_target: :pe and :cle pass by, everything else throws rather than being silently dropped. This is why a naive or meta-optimiser accepts a computed prior it can use while still rejecting an uncertainty set it cannot.

Arguments

  • opt: The optimisation step estimator.
  • ctx: The pipeline context.

Returns

  • opt′: The (possibly rebuilt) estimator actually run.

Related

source
PortfolioOptimisers.constraint_target_ofFunction
constraint_target_of(c) -> Symbol

The routing target one element of the constraints slot lands in.

An element a constraint step could not place by type carries its target — run_constraint_step paired the two — and it is read straight off. Everything else is placed by implicit_constraint_target.

Two cases throw. A Threshold names six optimiser fields, so its type cannot place it; the error names the declaration that would. A result of any other unplaceable type has no target at all, and is rejected here rather than at an optimiser.

Arguments

  • c: One element of the constraints slot.

Returns

Related

source
PortfolioOptimisers.accumulate_constraint_valuesFunction
accumulate_constraint_values(
    _::Val,
    vals
) -> LinearConstraint

Combine the several values that reached one accumulating routing target.

The default packs them into a vector in write order, which is the shape every field holding one result per estimator expects.

:cte is the exception, and it is what this seam exists for. Its field takes a vector of CentralityConstraint estimators, and centrality_constraints appends every row of every estimator into one LinearConstraint. Separate steps therefore merge rather than pack, so n centrality steps in a Pipeline reach the optimiser with the value one cte field holding n estimators would have produced.

Only ever called with more than one value; a single value is unwrapped by constraint_targets before it gets here.

Arguments

  • ::Val{target}: The routing target the values reached.
  • vals: The values, in write order.

Returns

  • The combined value.

Related

source
PortfolioOptimisers.constraint_targetsFunction
constraint_targets(cs) -> Vector{Pair{Symbol, Any}}

Fan the constraints slot out into routing targets.

Each element is placed by constraint_target_of and unwrapped by constraint_value_of. Several results reaching one accumulating target are combined by accumulate_constraint_values — packed into a vector in write order, or, for :cte, merged into the one constraint that holds all their rows. A group of one is unwrapped, matching the scalar-or-vector shape those fields accept everywhere else. A second result reaching any other target is refused, because that field holds one value and the second would silently replace the first.

Arguments

  • cs: The constraints slot.

Returns

  • A vector of target => value pairs, in the order the results were written.

Related

source
PortfolioOptimisers.maybe_inject_stepFunction
maybe_inject_step(est, ::PipelineContext) = est
maybe_inject_step(opt::OptimisationEstimator, ctx::PipelineContext)
maybe_inject_step(ps::PipelineStep, ctx::PipelineContext)

Either return the step estimator unchanged, inject the context into the optimiser, or inject the context into the optimiser and create a pipeline step.

Arguments

  • est: A step estimator.
  • opt: An optimisation step estimator.
  • ps: A PipelineStep wrapping an optimisation step estimator.
  • ctx: The pipeline context.

Returns

  • est′: The step estimator to run.
  • opt: The optimiser with its configuration overridden by the context.
  • ps: The pipeline step with its optimiser overridden by the context.
source
maybe_inject_step(
    res::NonFiniteAllocationOptimisationResult,
    ctx::PipelineContext
) -> NonFiniteAllocationOptimisationResult

Injection rules for a precomputed optimisation result standing in the optimisation step — the predict-only fold of a mixed TimeDependent schedule.

A result is already solved, so it has no configuration to override; this reuses the non-injectable pattern of inject_context: computed prior and phylogeny slots pass by (the result was fitted with its own), but populated uncertainty or constraints slots throw an ArgumentError rather than being silently dropped — a computed constraint that never reaches a solve is a fail-closed error, not a no-op.

Related

source
PortfolioOptimisers.pipe_required_targetsFunction
pipe_required_targets(
    ps::PipelineStep
) -> Union{Tuple{}, Tuple{Symbol}, Tuple{Symbol, Symbol}}

The routing targets a step is known at construction to produce.

An uncertainty-set step qualifies. It must declare which parameters it bounds through its PipelineStep wrapper, and that declaration is a field of the step rather than a property of a computed result, so the targets it will write are known before anything runs.

A constraint step qualifies for the same reason, one step removed: its target is declared by its family through pipe_constraint_targets, and where the family names several, by the step's own target field. Both are known before anything runs, and run_constraint_step resolves the destination from the same declaration, so the target checked here is the target the step will write.

Everything else returns an empty tuple. A callable step writing :constraints declares no family, and a precomputed result carried in by the pipeline input names its target only by its type.

Arguments

  • est: A step estimator.

Returns

  • A tuple of routing targets, empty when nothing is statically known.

Related

source
PortfolioOptimisers.assert_routableFunction
assert_routable(ests)

Reject at construction a pipeline whose terminal optimiser cannot receive a target an earlier step will write.

Without this, an unroutable uncertainty set is discovered by inject_context at injection time — which, under cross_val_predict, is after the fold loop has already fitted every earlier step of the first fold. The check asks the optimiser directly via pipe_accepts, so it stays honest as optimisers gain or lose fields.

It is deliberately structural: it establishes that the optimiser family can receive the target at all, not that this particular configuration will accept the value. A JuMPOptimiser always accepts :mu_ucs, but one carrying a non-ArithmeticReturn estimator still fails at injection — that condition belongs to pipe_route and is not duplicated here.

Skipped when the terminal step is a TimeDependent schedule or a precomputed result, since the optimiser is then not known until the fold loop resolves it.

Arguments

Returns

  • nothing.

Related

source
PortfolioOptimisers.assert_constraint_targetsFunction
assert_constraint_targets(ests)

Validate that every constraint step of a Pipeline resolves to exactly one routing target.

Runs resolve_constraint_target on each constraint step, which is the same call run_constraint_step makes when the step runs. Doing it here moves three failures from the fold loop to the constructor: a family that computes nothing for the constraints slot and is therefore not a step, a family that names several targets and was not told which, and a declared target that belongs to another family.

Validation

Arguments

  • ests: The step estimators.

Returns

  • nothing.

Related

source

Prediction

Predicting with a fitted pipeline replays the fitted preprocessing steps — the training universe, the training imputation parameters, the returns conversion — on an unseen data window, then delegates to the existing weights-level prediction machinery. Cross-validation folds can be computed directly on price-level data (Prices_RR), so the whole workflow is fitted per fold with no test-window leakage into stateful preprocessing.

PortfolioOptimisers.apply_fitted_stepFunction
apply_fitted_step(fitted, data) -> data′

Replay one fitted pipeline step on a data window during prediction.

Preprocessing steps transform the window at the data level they apply to: price-level fitted objects (AbstractPricesPreprocessingResult, AbstractPricesPreprocessingEstimator) transform price-level windows, returns-level ones transform returns-level windows, and PricesToReturns converts the window from prices to returns. A fitted object whose data level does not match the current window passes it through unchanged — mirroring fit, where such a step cannot affect the data that reaches the optimiser. Non-preprocessing fitted results (priors, phylogeny, uncertainty, constraints, optimisation) pass the window through untouched, and a nested PipelineResult replays its own steps recursively.

Arguments

Returns

  • data′: The transformed (or untouched) data window.

Related

source
PortfolioOptimisers.apply_fitted_stepsFunction
apply_fitted_steps(
    results::Tuple,
    data::Union{AbstractPricesResult, AbstractReturnsResult}
) -> Any

Replay the fitted preprocessing steps of a pipeline on a data window, in step order.

Arguments

  • results: The fitted per-step results of a PipelineResult.
  • data: The data window to transform.

Returns

  • data′: The transformed data window (returns-level when the steps include a PricesToReturns conversion).

Related

source
PortfolioOptimisers.assert_universe_alignedFunction
assert_universe_aligned(
    res::PipelineResult,
    rd::AbstractReturnsResult
)

Assert that a test window came from the same ingestion as the training window.

This is a provenance check. The terminal weights are indexed by the training universe, so a test window carrying a different asset set — or the same set described by a different universe statement — would silently misalign weights and returns, and it is reported here by name rather than surfacing as a dimension mismatch inside the risk calculation.

It reads two things. nx equality answers the asset axis, and check_asset_panel binds an AssetPanel's asset axis to its carrier's at construction, so nx equality compares the panel's axis transitively and no separate assertion is owed. Panel-presence parity answers where the universe was stated: a window with a panel and a fitted context without one, or the reverse, did not come from one ingestion.

The alignment guarantee it once carried alone has since split, and both halves are now held elsewhere. The axis half is structural. The ingestion layer fixes the asset axis before the split and port_opt_view slices it, so every window of every fold carries every asset, reduce-and-expand always expands onto a fixed axis, and no policy of the layer's own drops a row or a column. The semantic half was never this check's: an asset present in both windows and non-investable in one is a Held Gap, which filter_held_gaps reads off the weights and the returns under the strictness policy.

What is left is the population that can still break the invariant, and its message names both: a carrier built outside the layer, and a third-party step that changes the asset set. On the layer's path neither can arise, so no remedy is prescribed here.

Arguments

  • res: The fitted PipelineResult.
  • rd: The transformed test-window returns.

Validation

  • rd.nx == train.nx. Raises an ArgumentError.
  • The window and the fitted context either both carry an AssetPanel or neither does. Raises an ArgumentError.

Returns

  • nothing.

Related

source