Pipeline cross-validation
The price-level restriction (the rolling-window rule)
Combinatorial and multiple-randomised cross-validation recombine non-contiguous groups / resampled paths. A pipeline that starts from prices runs a rolling, order-dependent transform (a PricesToReturns, or any windowed preprocessing) that needs contiguous input rows, so those schemes are rejected at the price-level split. They are supported for a returns-level pipeline (below), which has no such transform.
PortfolioOptimisers.port_opt_view — Method
port_opt_view(x, i, args...; kwargs...) -> nothing_scalar_array_view(x, i)Sub-select an estimator, result, or algorithm to the asset/observation index i.
port_opt_view is the index-selection counterpart of factory: where factory threads runtime values down a composed struct tree, port_opt_view threads an index selection — restricting every data-bearing field and composed child to the subset i. It is the mechanism that makes meta-optimisers (NestedClustered, SubsetResampling) and cross-validation variants operate on subproblems with identical struct shapes.
Callers do not normally call port_opt_view directly; it is driven by meta-optimisers and cross-validation internals. It is public (not exported) because extension authors who implement a new composed estimator may need to define a method. Use @vprop on data-bearing fields to have the method generated automatically.
This universal fallback handles leaf values: arrays are sliced via nothing_scalar_array_view; scalars, nothing, estimators without data fields, and algorithms pass through unchanged. Composed structs that recurse into children define their own (more specific) method — emitted by @vprop or hand-written.
The threaded tail args... (typically the returns matrix X for the JuMP families) and any kwargs are accepted and dropped here, so a macro-threaded port_opt_view(child, i, X) never MethodErrors on a leaf field.
Algorithm
- Drop
args...andkwargs.... This method is the leaf of the recursion, so it threads nothing further. - Return
nothing_scalar_array_viewofxati, whose own algorithm names the rule for each leaf type.
Related
port_opt_view(pipe::Pipeline, i, args...; kwargs...)Deliberately unsupported: a Pipeline cannot be sub-selected by asset view.
Meta-optimisers (NestedClustered, Stacking, SubsetResampling) build asset sub-portfolios by taking a port_opt_view of their inner estimator. A pipeline's asset universe is fitted state — the missing-data filter decides it from the training window — so an asset view taken before fitting is not well defined. Wrapping a Pipeline in a meta-optimiser is therefore unsupported for now; a meta-optimiser may still be the optimisation step of a pipeline.
Related
PortfolioOptimisers.needs_previous_weights — Method
needs_previous_weights(p::Pipeline) -> Any
Return true if any step of the Pipeline requires the previous fold's portfolio weights, forcing sequential fold execution and a populated w_prev in the TimeDependentContext. Recurses through PipelineStep wrappers and nested pipelines; schedule entries are inspected per the needs_previous_weights conventions (entries yes, default no).
Related
The pipeline fold loop
cross_val_predict over a Pipeline fits the whole workflow per fold and predicts on each test window. It is also the fold loop that consumes TimeDependent schedules in a pipeline (ADR 0030, "swap, then inject"): schedules are swapped for their per-fold values before fit runs, so injection never sees a schedule and fit/run_step never learn about folds.
A scheme that declares a Fold Fit sends the loop down its online arm, where the pipeline is warmed up once, folded fold by fold through partial_fit!, and read out through fit(pipe); Online(pipe) takes the same doors as the declared refit. See the Pipeline's online step.
PortfolioOptimisers.cross_val_predict — Method
cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CVER = KFold(); ex = FLoops.ThreadedEx(), id = nothing)Run cross-validated prediction over an entire Pipeline workflow and return a MultiPeriodPredictionResult.
Return type by cross-validation scheme
cross_val_predict is the single entry point for every scheme, but the result type depends on cv: single-path schemes return one series, multi-path schemes return a per-path collection. The type flips with the scheme, so result-navigation code written against one shape (e.g. a KFold run) breaks when the scheme is swapped — branch on the scheme, not on the run.
cv scheme | Return type | .pred holds |
|---|---|---|
KFold / walk-forward (CVER) | MultiPeriodPredictionResult | one series — one prediction per fold |
CombinatorialCrossValidation | PopulationPredictionResult | a per-path collection |
MultipleRandomised | PopulationPredictionResult | a per-path collection |
The combinatorial and asset-resampling schemes are dispatched by their own methods (see Related); the rest of this docstring describes the contiguous, single-path (CVER) method.
The input is split at its own level — price-level data by the prices-aware split methods (contiguous windows, so stateful preprocessing stays inside the fold), returns-level data as usual — and for each fold the whole workflow is fitted on the training window and predicts on the test window, exactly as fit/predict do for a holdout. This method covers the contiguous, single-path schemes (KFold and the walk-forwards). Combinatorial and asset-resampling schemes have their own methods for a returns-level pipeline (see cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::CombinatorialCrossValidation) and cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::MultipleRandomised)); for a price-starting pipeline they are rejected at split by the rolling-window rule.
This is the fold loop that consumes TimeDependent schedules in a pipeline: when the pipeline is time-dependent, fold i builds a TimeDependentContext — with rd the raw, pre-preprocessing input data, so pipeline-level callables see the fold's data before any step has transformed it — and swaps every schedule for its fold-i value via update_time_dependent_estimator before fit runs. A schedule step may resolve to an estimator (the fold optimises) or a precomputed result (the fold predicts only); injection never sees a schedule. The loop is fold_loop, shared with the optimiser-level schemes. The scheme states whether its folds are a timeline through folds_are_time_ordered. A walk-forward answers true, so a pipeline that needs_previous_weights runs sequentially and threads the previous fold's weights into the context's w_prev and, post-swap, into the optimisation steps via factory. A KFold answers false, because its folds are independent of each other. Its folds run in parallel, w_prev is nothing, and no factory pass runs — the same behaviour the optimiser-level KFold path already has.
A walk-forward that declares a Fold Fit (ff = OnlineStep()) sends the loop down its online arm: the pipeline is warmed up once on the first training window, each fold's new rows are folded through its steps into the row owner by partial_fit!, and the fold reads the pipeline out through fit(pipe) where a refit would have run, through pipeline_fold_fit. The run reaches the weights of the batch expanding walk-forward fold for fold, and Online(pipe) takes the same door as the declared refit from an input-carrier buffer.
Arguments
pipe: The pipeline.data: Price- or returns-level input data (Prices_RR).cv::CVER: Cross-validation scheme with contiguous, non-combinatorial folds. Defaults toKFold().folds_are_time_ordereddecides whether its folds thread the previous fold's weights, andfold_fitwhether the folds refit or fold.ex: FLoops executor controlling parallelism. Defaults toFLoops.ThreadedEx().id: Identifier stored on the result.
Returns
MultiPeriodPredictionResult: One prediction per fold, in fold order.
Related
PipelinefitTimeDependentfolds_are_time_orderedsearch_cross_validationMultiPeriodPredictionResult/PopulationPredictionResult(the two return shapes)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CombinatorialCrossValidation)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::MultipleRandomised)
Combinatorial and asset-resampling over a returns-level pipeline
A returns-level pipeline runs the multi-path schemes like the plain-optimiser loops: combinatorial fits each split on its (possibly non-contiguous) training rows and predicts its test groups; multiple-randomised runs each path's inner walk-forward over an asset-subset view of the input, so the pipeline fits fresh on the sub-universe and never sub-selects fitted state.
PortfolioOptimisers.cross_val_predict — Method
cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CombinatorialCrossValidation; ex = FLoops.ThreadedEx(), kwargs...) -> PopulationPredictionResultRun combinatorial cross-validation over a price- or returns-level Pipeline.
Each split fits the whole workflow on its (possibly non-contiguous) training rows and predicts each of the split's disjoint test groups; sort_predictions! then recombines the per-split test-group predictions into the scheme's paths, exactly like the plain-optimiser combinatorial loop. Time-dependent steps resolve per split against the fold's TimeDependentContext before fit.
At the returns level the training rows are order-independent for moment-style fitted steps, so this is exact. At the price level a split's training rows are non-contiguous — there are gaps where the held-out test groups sit — so the fold's rolling transform (PricesToReturns) produces one spurious return per gap boundary (a boundary return spanning a gap). That is the rolling-window approximation: combinatorial paths at the price level cost a few boundary returns in each fold's training window. Test groups are contiguous, so predictions are unaffected. Use MultipleRandomised if you need contiguous training rows at the price level.
Related
cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CVER = KFold(); ex = FLoops.ThreadedEx(), id = nothing)Run cross-validated prediction over an entire Pipeline workflow and return a MultiPeriodPredictionResult.
Return type by cross-validation scheme
cross_val_predict is the single entry point for every scheme, but the result type depends on cv: single-path schemes return one series, multi-path schemes return a per-path collection. The type flips with the scheme, so result-navigation code written against one shape (e.g. a KFold run) breaks when the scheme is swapped — branch on the scheme, not on the run.
cv scheme | Return type | .pred holds |
|---|---|---|
KFold / walk-forward (CVER) | MultiPeriodPredictionResult | one series — one prediction per fold |
CombinatorialCrossValidation | PopulationPredictionResult | a per-path collection |
MultipleRandomised | PopulationPredictionResult | a per-path collection |
The combinatorial and asset-resampling schemes are dispatched by their own methods (see Related); the rest of this docstring describes the contiguous, single-path (CVER) method.
The input is split at its own level — price-level data by the prices-aware split methods (contiguous windows, so stateful preprocessing stays inside the fold), returns-level data as usual — and for each fold the whole workflow is fitted on the training window and predicts on the test window, exactly as fit/predict do for a holdout. This method covers the contiguous, single-path schemes (KFold and the walk-forwards). Combinatorial and asset-resampling schemes have their own methods for a returns-level pipeline (see cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::CombinatorialCrossValidation) and cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::MultipleRandomised)); for a price-starting pipeline they are rejected at split by the rolling-window rule.
This is the fold loop that consumes TimeDependent schedules in a pipeline: when the pipeline is time-dependent, fold i builds a TimeDependentContext — with rd the raw, pre-preprocessing input data, so pipeline-level callables see the fold's data before any step has transformed it — and swaps every schedule for its fold-i value via update_time_dependent_estimator before fit runs. A schedule step may resolve to an estimator (the fold optimises) or a precomputed result (the fold predicts only); injection never sees a schedule. The loop is fold_loop, shared with the optimiser-level schemes. The scheme states whether its folds are a timeline through folds_are_time_ordered. A walk-forward answers true, so a pipeline that needs_previous_weights runs sequentially and threads the previous fold's weights into the context's w_prev and, post-swap, into the optimisation steps via factory. A KFold answers false, because its folds are independent of each other. Its folds run in parallel, w_prev is nothing, and no factory pass runs — the same behaviour the optimiser-level KFold path already has.
A walk-forward that declares a Fold Fit (ff = OnlineStep()) sends the loop down its online arm: the pipeline is warmed up once on the first training window, each fold's new rows are folded through its steps into the row owner by partial_fit!, and the fold reads the pipeline out through fit(pipe) where a refit would have run, through pipeline_fold_fit. The run reaches the weights of the batch expanding walk-forward fold for fold, and Online(pipe) takes the same door as the declared refit from an input-carrier buffer.
Arguments
pipe: The pipeline.data: Price- or returns-level input data (Prices_RR).cv::CVER: Cross-validation scheme with contiguous, non-combinatorial folds. Defaults toKFold().folds_are_time_ordereddecides whether its folds thread the previous fold's weights, andfold_fitwhether the folds refit or fold.ex: FLoops executor controlling parallelism. Defaults toFLoops.ThreadedEx().id: Identifier stored on the result.
Returns
MultiPeriodPredictionResult: One prediction per fold, in fold order.
Related
PipelinefitTimeDependentfolds_are_time_orderedsearch_cross_validationMultiPeriodPredictionResult/PopulationPredictionResult(the two return shapes)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CombinatorialCrossValidation)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::MultipleRandomised)
PortfolioOptimisers.cross_val_predict — Method
cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::MultipleRandomised; ex = FLoops.ThreadedEx(), kwargs...) -> PopulationPredictionResultRun asset-resampling (multiple-randomised) cross-validation over a price- or returns-level Pipeline.
Each resampled path is an inner walk-forward over a random asset subset; the subset is applied to the input data (an asset view via pipeline_asset_view), and the pipeline is fitted fresh on the sub-universe — so the pipeline never needs to sub-select its fitted universe. Paths are run by pipeline_path_fit_and_predict and returned as a PopulationPredictionResult. Asset resampling keeps every observation window contiguous (it draws over assets, not rows), so — unlike combinatorial cross-validation — multiple-randomised is admissible at the price level too: a price-starting pipeline fits fresh on the asset-subset prices per fold, with no rolling-window violation.
Related
cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CVER = KFold(); ex = FLoops.ThreadedEx(), id = nothing)Run cross-validated prediction over an entire Pipeline workflow and return a MultiPeriodPredictionResult.
Return type by cross-validation scheme
cross_val_predict is the single entry point for every scheme, but the result type depends on cv: single-path schemes return one series, multi-path schemes return a per-path collection. The type flips with the scheme, so result-navigation code written against one shape (e.g. a KFold run) breaks when the scheme is swapped — branch on the scheme, not on the run.
cv scheme | Return type | .pred holds |
|---|---|---|
KFold / walk-forward (CVER) | MultiPeriodPredictionResult | one series — one prediction per fold |
CombinatorialCrossValidation | PopulationPredictionResult | a per-path collection |
MultipleRandomised | PopulationPredictionResult | a per-path collection |
The combinatorial and asset-resampling schemes are dispatched by their own methods (see Related); the rest of this docstring describes the contiguous, single-path (CVER) method.
The input is split at its own level — price-level data by the prices-aware split methods (contiguous windows, so stateful preprocessing stays inside the fold), returns-level data as usual — and for each fold the whole workflow is fitted on the training window and predicts on the test window, exactly as fit/predict do for a holdout. This method covers the contiguous, single-path schemes (KFold and the walk-forwards). Combinatorial and asset-resampling schemes have their own methods for a returns-level pipeline (see cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::CombinatorialCrossValidation) and cross_val_predict(pipe::Pipeline, data::AbstractReturnsResult, cv::MultipleRandomised)); for a price-starting pipeline they are rejected at split by the rolling-window rule.
This is the fold loop that consumes TimeDependent schedules in a pipeline: when the pipeline is time-dependent, fold i builds a TimeDependentContext — with rd the raw, pre-preprocessing input data, so pipeline-level callables see the fold's data before any step has transformed it — and swaps every schedule for its fold-i value via update_time_dependent_estimator before fit runs. A schedule step may resolve to an estimator (the fold optimises) or a precomputed result (the fold predicts only); injection never sees a schedule. The loop is fold_loop, shared with the optimiser-level schemes. The scheme states whether its folds are a timeline through folds_are_time_ordered. A walk-forward answers true, so a pipeline that needs_previous_weights runs sequentially and threads the previous fold's weights into the context's w_prev and, post-swap, into the optimisation steps via factory. A KFold answers false, because its folds are independent of each other. Its folds run in parallel, w_prev is nothing, and no factory pass runs — the same behaviour the optimiser-level KFold path already has.
A walk-forward that declares a Fold Fit (ff = OnlineStep()) sends the loop down its online arm: the pipeline is warmed up once on the first training window, each fold's new rows are folded through its steps into the row owner by partial_fit!, and the fold reads the pipeline out through fit(pipe) where a refit would have run, through pipeline_fold_fit. The run reaches the weights of the batch expanding walk-forward fold for fold, and Online(pipe) takes the same door as the declared refit from an input-carrier buffer.
Arguments
pipe: The pipeline.data: Price- or returns-level input data (Prices_RR).cv::CVER: Cross-validation scheme with contiguous, non-combinatorial folds. Defaults toKFold().folds_are_time_ordereddecides whether its folds thread the previous fold's weights, andfold_fitwhether the folds refit or fold.ex: FLoops executor controlling parallelism. Defaults toFLoops.ThreadedEx().id: Identifier stored on the result.
Returns
MultiPeriodPredictionResult: One prediction per fold, in fold order.
Related
PipelinefitTimeDependentfolds_are_time_orderedsearch_cross_validationMultiPeriodPredictionResult/PopulationPredictionResult(the two return shapes)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::CombinatorialCrossValidation)cross_val_predict(pipe::Pipeline, data::Prices_RR, cv::MultipleRandomised)
Time-dependent traits and the swap over steps
The per-step legs of the time-dependent machinery: the traits recurse over a pipeline's steps, the swap maps over them (unwrapping PipelineStep-wrapped schedules), the fold-less reset resolves schedule steps to their explicit default, and the previous-weights factory delivers w_prev to the optimisation steps after the swap.
PortfolioOptimisers.factory — Method
factory(
p::Pipeline,
w::AbstractVector{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}
) -> Pipeline
Rebuild a Pipeline with the previous fold's weights delivered to every optimisation step (see pipeline_step_factory). Applied by the fold loop after the swap, so freshly swapped-in per-fold optimisers receive the previous weights too.
Related