The online step
An estimator with no exact incremental fold keeps the observations it has seen in a PortfolioOptimisers.SampleBufferState, and Online is the configuration that seeds one. The wrapper is transient: PortfolioOptimisers.update_online_estimator resolves it at warm-up, so no wrapper survives into the run. The buffer carries the per-observation masks of a CoveragePolicy and the factor observations of a factor prior beside its rows, each fixed by the first append, so one buffer serves every estimator that refits.
PortfolioOptimisers.Online — Type
struct Online{T1, T2} <: AbstractEstimatorDeclares that an estimator takes the online step from a buffer of the observations it has seen.
An Online is stored directly in the estimator field it wraps — e.g. HighOrderPriorEstimator(; pe = Online(EmpiricalPrior())), or the pe of an optimiser, JuMPOptimiser(; pe = Online(EmpiricalPrior()), slv = …) — and it is transient, in the sense TimeDependent is: update_online_estimator walks the fields that hold one, seeds each wrapped estimator's cache with a SampleBufferState, and rebuilds the host through its keyword constructor. What comes out is an ordinary estimator carrying a state, and no Online exists from that point on, so every verb downstream meets a plain estimator.
Only a field whose bound is Onl admits a wrapper: the pe slot of a wrapping prior and of an optimiser. A moment slot inside a prior — ce, me, ske, kte — takes none, so EmpiricalPrior(; ce = Online(…)) is refused by the field's bound before any warm-up runs. The prior is the row owner: it holds its observations once, at the bottom of its chain, and refits every member that does not fold from those rows, so a buffer under one of its moments would hold the same rows a second time. Wrap the prior instead. A moment estimator is wrapped on its own only when it is itself the estimator under study, update_online_estimator(Online(Coskewness())).
It differs from TimeDependent in when it resolves, and the difference is deliberate. A schedule re-resolves every fold, because its value changes every fold. An Online resolves once, at warm-up, because after warm-up the state is what is threaded from step to step and a second resolution would throw the buffer away.
A wrapper replaces an exact fold; it does not add to one. Every family that folds exactly narrows the cache type parameter of its own partial_fit! methods to the state that fold reads, so a wrapped estimator never meets them: it buffers its observations and answers every read-out verb by running the batch verb over the rows the buffer holds. The state's type is the whole route, and it is decided by whether the caller wrapped the estimator.
So wrap an estimator whose estimate has no exact incremental fold of its own, one that folds exactly and carries its observations for a consumer downstream, or one whose estimate you want fitted over a window rather than over every observation. An estimator that folds exactly and carries nothing needs no wrapper: unwrapped it already answers partial_fit!, seeding its own state on the first call, and it folds in the memory one state costs rather than the memory a buffer costs.
A wrapper and a TimeDependent schedule do not wrap each other, and the difference in when they resolve is the whole reason. Neither Online(TimeDependent(…)) nor a schedule whose entry or default is an Online is admissible: a wrapper reached through a schedule entry would be resolved at no fold at all, or re-seeded at every fold, throwing away the buffer the step threads. They compose the other way round, and both ways are ordinary. An estimator an Online wraps may hold schedules of its own, which survive the seeding untouched and resolve per fold afterwards; and one host may hold a wrapper in one field and a schedule in another, each resolving at its own time. The two field scans are disjoint by construction — a field holding one is invisible to the other's candidate list — so neither resolution can reach the other's wrapper.
max_history caps that buffer, and the cap is the window. An uncapped buffer answers exactly what a batch fit over every observation folded so far answers, and a capped one answers exactly what a batch fit over the last max_history observations answers — for the estimate itself and for every consumer that reads the observations, the scenario risk measures among them, so CVaR, EVaR and CDaR. There is one rule and no special case: a buffer means the batch verb over the buffer's rows. An estimator left unwrapped is unaffected, folds exactly, and stays fitted over every observation.
Fields
est: Estimator the buffer is seeded on. It is the value the field takes once the wrapper has resolved.
max_history: Optional cap on the number of observations the carry buffer keeps.nothingkeeps every observation folded so far. A cap bounds memory and nothing else: it does not make the estimator windowed, because an estimate that folds exactly is fitted over every observation whatever the buffer holds.
Constructors
Online(est::Union{<:AbstractEstimator, <:StatsBase.CovarianceEstimator}; max_history::Option{<:Integer} = nothing)Online(; est::Union{<:AbstractEstimator, <:StatsBase.CovarianceEstimator}, max_history::Option{<:Integer} = nothing)Validation
estis not anOnline. AnArgumentErroris thrown otherwise.esthas acachefield. AnArgumentErroris thrown otherwise.max_history > 0when it is notnothing. ADomainErroris thrown otherwise.
Examples
julia> Online(Covariance(; alg = SemiMoment()); max_history = 250)Online est ┼ Covariance │ me ┼ SimpleExpectedReturns │ │ w ┴ nothing │ ce ┼ GeneralCovariance │ │ ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true) │ │ w ┴ nothing │ alg ┼ SemiMoment() │ w ┴ nothing max_history ┴ Int64: 250Related
PortfolioOptimisers.partial_fit! — Function
partial_fit!(
state::SampleBufferState,
X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
...
) -> Any
partial_fit!(
state::SampleBufferState,
X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
F::Union{Nothing, AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}};
dims,
active_mask,
estimation_mask
) -> Any
Folds every observation of a block into a SampleBufferState.
The block arm of the partial_fit! interface, and the whole implementation of the buffer's fold: the single-observation arm reshapes its argument and calls this one. A cap truncates the incoming rows before they are copied, so a block longer than the cap never allocates the whole of it. reserve_sample_buffer carries the growth, and its docstring states what makes an append amortised O(1).
The masks and the factor rows travel with the rows they explain, row for row, through every step: the orientation, the truncation, the growth and the copy. Whether the buffer records a mask, and whether it records factor rows, is fixed by its first append, exactly as the width and the element type are, and a later fold that disagrees is refused by assert_buffer_presence_agreement — a buffer whose activity is known for some of its rows and not for others answers neither question, and a buffer whose factor rows exist for some observations and not for others describes no regression. A buffer holding no observations has nothing to disagree with, so reset_empty_buffer empties it and the fold seeds it afresh.
Algorithm
- Orient
X, the masks andFtoobservations × assetsandobservations × factors, transposing them whendims == 2, refuse a mask that is not of the shape of the block and a factor block whose rows are not the block's, and return the state unchanged when the block is empty. - Truncate the incoming rows, their masks and their factor rows to the last
max_historyof them when a cap is set. - Empty a buffer that holds no observations and disagrees with the fold about the masks or the factor rows, so that the seed below fixes them afresh.
- Seed the buffer with
seed_sample_bufferwhen it is the empty seed, which fixes the width, the element type, the masks and the factor rows; and refuse a block whose width is not the width already fixed, whose masks disagree with the ones the buffer holds, or whose factor rows are absent where the buffer records them, present where it does not, or of a width the buffer did not fix. - Make room for the block with
reserve_sample_buffer, which drops what the cap pushes out and grows the backing matrices. - Copy the block, its masks and its factor rows in after the valid region, and rebind
nwithAccessors.@reset.
Arguments
state: The buffer to fold into.X: Data matrixobservations × assetsif thedimskeyword does not exist ordims = 1,assets × observationswhendims = 2.F: Factor observations to fold, oriented asXis, ornothing.dims: Dimension along which to perform the computation.active_mask: The active mask of the block, of the shape ofX, ornothing. A buffer records it for every observation it holds or for none of them.estimation_mask: The estimation mask of the block, of the shape ofX, ornothing. It is carried on the same terms asactive_mask.
Validation
dims in (1, 2).Xhas the width the first append fixed. ADimensionMismatchis thrown otherwise.active_maskandestimation_mask, when they are notnothing, have the shape ofX. ADimensionMismatchis thrown otherwise.F, when it is notnothing, has as many rows asX, and the width the first append that carried one fixed. ADimensionMismatchis thrown otherwise.- A buffer holding observations is given the masks it already records, and factor rows exactly when it already records them. An
ArgumentErroris thrown otherwise.
Returns
state::SampleBufferState: The buffer after the last row.
Related
PortfolioOptimisers.partial_fit! — Function
partial_fit!(
state::SampleBufferState,
x::AbstractVector{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
...
) -> Any
partial_fit!(
state::SampleBufferState,
x::AbstractVector{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
f::Union{Nothing, AbstractVector{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}};
active_mask,
estimation_mask
) -> Any
Folds one observation into a SampleBufferState.
The single-observation arm of the partial_fit! interface. It reshapes the observation into a one-row block, which costs no copy, and folds it through the block arm. The masks and the factor observation are reshaped on the same terms, so one observation under a policy, or beside its factors, folds through the same code the block does.
Arguments
state: The buffer to fold into.x: One observation, whose entries are the assets.f: The contemporaneous factor observation, whose entries are the factors, ornothing.active_mask: The active mask of the observation, one entry per asset, ornothing.estimation_mask: The estimation mask of the observation, one entry per asset, ornothing.
Validation
xhas the width the first append fixed. ADimensionMismatchis thrown otherwise.active_maskandestimation_mask, when they are notnothing, have one entry per asset. ADimensionMismatchis thrown otherwise.f, when it is notnothing, has the width the first factor append fixed, and is given exactly when the buffer records factor rows. ADimensionMismatchor anArgumentErroris thrown otherwise.
Returns
state::SampleBufferState: The buffer after the observation.
Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(
x::SampleBufferState,
i,
args...
) -> SampleBufferState
Slices a SampleBufferState to the selected assets.
A buffer holds its observations verbatim, so the slice of the buffer is the buffer of the sliced universe, column for column, and the observation axis passes through. The masks are per cell, so they slice on the same axis and by the same indices, and the viewed buffer answers the same question over the selected assets that the whole one answers over all of them. The factor rows are copied, never sliced: the selection indexes assets, a factor is not an asset, and a fit over a subset of the universe reads the same factors. The slice copies by index and does not view: a later partial_fit! on the viewed estimator would otherwise write through into the backing matrices of the estimator the view was taken from.
Arguments
x: The buffer to slice.i: Index or indices of the assets to keep.args...: Additional positional arguments (ignored).
Returns
state::SampleBufferState: The buffer of the same observations over the selected assets.
Related
PortfolioOptimisers.partial_fit! — Method
partial_fit!(
est::Union{AbstractEstimator, CovarianceEstimator},
X::Union{AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}, AbstractVector{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}};
dims,
active_mask,
estimation_mask
) -> Any
Folds observations into the sample buffer an estimator carries.
The buffering arm of partial_fit!, and the method every estimator carrying a SampleBufferState reaches. A family that folds exactly writes methods of its own, and each of them narrows the cache type parameter of its own estimator to the state that fold reads, so a buffer never meets them and this method is what remains. The state's type is therefore the whole route, and nothing refuses the step.
It is one method over both arms of the interface rather than two, because the families that refuse the step declare one method over both arms too, and a pair of narrower methods here would be ambiguous against each of them. So the arm is chosen by the type of X inside the body, which is statically resolved at every call site.
A buffer carries the per-observation masks beside the observations, so a CoveragePolicy mask threads through the wrapper as it does through an estimator's own accumulator, and the read-out hands it back to the batch verb. A wrapped estimator folded under a policy therefore answers what a batch fit over the same window under the same policy answers, and the unwrapped and wrapped paths agree.
Algorithm
- Read the buffer out of the
cachefield withassert_sample_buffer, which refuses an estimator that was never wrapped inOnline. - Fold a matrix and its masks through the block arm of
partial_fit!, and a vector and its masks through the single-observation arm. - Rebind
est.cachewithAccessors.@reset, and return the estimator.
Arguments
est: Estimator whose buffer is folded forward.X: Observations to fold. A matrix holds one observation per row whendims == 1, and one per column whendims == 2. A vector is a single observation across the assets, anddimsis ignored.dims: Dimension along which to perform the computation.active_mask: The active mask of the block, of the shape ofX, or of one entry per asset whenXis one observation, ornothing.estimation_mask: The estimation mask, on the same terms asactive_mask.
Validation
estcarries aSampleBufferState. AnArgumentErroris thrown otherwise.- The masks, when they are not
nothing, have the shape ofX. ADimensionMismatchis thrown otherwise. - A buffer holding observations is given the masks it already records. An
ArgumentErroris thrown otherwise. dims in (1, 2).
Returns
est: The estimator, with itscachefield rebound to the buffer after the last observation.
Related
PortfolioOptimisers.merge_states — Method
merge_states(
a::SampleBufferState,
b::SampleBufferState
) -> SampleBufferState{_A, Int64} where _A
Folds two SampleBufferState fitted on disjoint blocks into the buffer of the concatenated block.
Concatenation, which is exact: a buffer holds its observations verbatim, so the buffer of two blocks is the buffer of the rows of one followed by the rows of the other. It is the one buffer state that merges. A cap is applied to the result, which keeps the last max_history rows of the concatenation. The masks and the factor rows concatenate with the rows they explain, and two buffers that disagree about which of them they record are refused, on the reasoning assert_buffer_presence_agreement states for a fold.
assert_mergeable_states is deliberately not called. Its array-shape rule reads every array field on every axis, and a buffer's backing matrix carries the observation axis as well as the asset axis, so two buffers over the same assets and different numbers of observations would be refused by it. The width, the cap and the masks are checked here instead.
Algorithm
- Refuse two buffers of different widths, and two buffers of different caps.
- Refuse two buffers that do not record the same masks, or that do not agree on recording factor rows, or whose factor rows are of different widths.
- Concatenate the valid region of the first with the valid region of the second, and each mask and the factor rows with their own.
- Keep the last
max_historyrows when a cap is set.
Arguments
a: The buffer of the first block of observations.b: The buffer of the second block of observations.
Validation
aandbdescribe the same number of assets. ADimensionMismatchis thrown otherwise.aandbcarry the same cap. AnArgumentErroris thrown otherwise.aandbrecord the same masks, and both record factor rows or neither does. AnArgumentErroris thrown otherwise.aandbdescribe the same number of factors, when they record factor rows. ADimensionMismatchis thrown otherwise.
Returns
state::SampleBufferState: The buffer the two blocks give when they are folded as one block.
Related