The online portfolio selection head

OnlinePortfolioSelection runs an online portfolio selection rule. It is a naive optimiser, so it needs a solver only when its set of allowed weights does. optimise(opt, rd) runs the rule from the start weights over every row of the returns in order, and returns the weights for the next period. partial_fit! updates the rule with a block of new rows, one row at a time. optimise(opt) with no data returns the current weights of the rule, and runs no batch fit.

The pages of this group list the rules in the order the library added them. Li and Hoi (2014) sort the rules into five families: benchmarks, follow the winner, follow the loser, pattern matching and meta-learning. The capability catalogue and the roster by group of the user guide list the rules by these families. The user guide also runs the rules on two synthetic markets and prints a table of their regret.

PortfolioOptimisers.OnlinePortfolioSelection — Type
struct OnlinePortfolioSelection{__T_alg, __T_set, __T_w0, __T_fees, __T_fb, __T_strict, __T_cache} <: NaiveOptimisationEstimator

Updates its allocation from each realised price relative through the Online Selection Rule on alg.

The head is a naive optimiser. It fits no moment and solves no programme of its own. Every rule of the family is one recursion, which rebalances the allocation to its target at the start of every period, and the head does all the work that the rules share. # Mathematical definition states the recursion and the wealth it earns.

The batch verb is the Causal Pass. optimise(opt, rd) starts from the Start Allocation w0, applies the Online Update to every row of rd in order, and returns the Next-Period Allocation. After rows 1:T that is the portfolio for period T + 1, so the pass takes one more update than a backtest driver that stops at the last row it holds. The pass rebalances at every row and pays no fee. predict(res, rd) on its Result charges the head's fees, with no drift.

The online verbs run the same recursion. partial_fit!(opt, rd) folds each row into the state as the Block Step. A fold of k rows is k single-row updates, so a walk-forward with test_size = k holds the Next-Period Allocation of the end of the block for k periods. optimise(opt) with no data is the Recursion Read-out. It wraps the allocation of the state in a Result and runs no batch path. So optimise(opt) after a fold of rows 1:t equals optimise(opt, rd[1:t]) exactly, at every test_size. The held path of a walk-forward differs from the batch pass in two ways. The target set at the start of a block drifts over the block, and the Block Step sets the cadence of the trades.

The Start Allocation is w0, nothing by default. When it is absent, the recursion starts at 1/N over the whole pinned universe, and the unlisted names are included. If k of N assets are unlisted at the first row, the recursion puts k/N of its weight on legs that see x = 1, as cash does, until the rule moves that weight. The read-out renormalises over the listed legs, so the fund holds the listed legs alone. But the fund is not always the recursion that starts over the listed names alone, because the parked legs enter the gross return ⟨w, x⟩ that a rule reads. Buy-and-hold scales each leg by its own price relative, so its fund is the same. The exponentiated-gradient step divides by the gross return, so its fund is different. A caller who wants the recursion to be the fund from row one gives w0 over the listed assets, with zeros elsewhere. The rule's step then decides what a zero does. The exponentiated-gradient step keeps a zero at zero, so that asset never gets weight. A Euclidean step that moves no weight onto the asset keeps the zero exact too, because project_simplex only renormalises a raw step that is on the simplex up to rounding. Buy-and-hold is such a step. A Euclidean step that moves weight onto the asset, as the passive-aggressive step does, gives it weight, and a gap at that asset is then a Held Gap.

A given w0 is over the pinned names, and the head pins and views it with them. The first step projects it once onto the Allocation Set in the rule's geometry, as it projects the uniform start, so a start outside the set becomes feasible and is never refused. A set that reads the head's rows has no constraints before the first row, so it holds the start as given until the first update. The start reaches the first Online Update as w. A rule that steps from w continues from it. A rule whose next allocation is not a step from w replaces it at that update. The constant rebalanced portfolio returns its own w, an expert mixture returns the mix of its experts, and the Newton step solves its Gram. Such a rule can still carry w0 forward. The gradient of the Newton step reads w, and an expert that steps from w starts at w0. Under every rule, the fund holds w0 for exactly one period.

The Online Update reads the recursion's own allocation, and no flag changes that. The previous weights of the loop reach the head through factory alone. They go into fees and fb, and never into the recursion. The fund's held book is the one base against which a turnover fee on this family measures a trade. So the online arm of the fold loop refuses a head whose fees carry a tn term when the Previous-Weights Source of the walk-forward is nothing. It refuses by name and before any fold, and pws = DriftedWeights() is the whole configuration. A head with no tn fee is not checked.

A non-finite return is kept, and read in two ways. The rule's step reads x = 1 there, as if the leg held cash. At an asset that the panel marks inactive, or at one to which the recursion gives no weight, this is silent. At an active asset with a non-zero weight it is a Held Gap, which warns by default and refuses under strict. The rows buffer keeps the cell as NaN, with the row's active mask beside it. Every statistic over the rows reads them as the batch verb reads a carrier, and reduces to its own Coverage Universe. Such a statistic is the mean of a forecaster, the prior of a Risk Loss or of a programme set, or the re-solve of a leader. A plain estimator drops an asset with a gap anywhere in its window. A mask-aware estimator answers the asset from the rows it has. A kernel over price relatives reads the gap as one.

The head never forces a zero into the recursion's allocation, and a relisted asset comes back at the recursion's own weight. A programme set that fits on the rows writes a zero at a leg that its prior cannot price, as a batch head does. It admits the leg again when the prior can price it. Under a time-varying panel, the Investable Mask of the read-out is the active mask of the last folded row. The read-out slices the full allocation to that mask and renormalises it, and the Result expands it back with a zero at every non-investable asset.

The Allocation Set on set is the one the Constrained Update projects onto. project projects the raw step of every rule onto it, in the rule's own Projection Geometry. The default BoundedAllocationSet is the simplex, and every projection onto it is closed form, so the default configuration solves nothing. A ProgrammeAllocationSet admits the full constraint vocabulary, and its projection is a programme. When a programme fails, the step is a Held Step. The projection returns the Price-Adjusted Allocation it was given, so the fund trades nothing that period, and the rule's carrier still absorbs the row. The head warns once, with the row's timestamp. The retcode of the Recursion Read-out is an OptimisationSuccess that carries the HeldStep record of the last folded row, so a fallback chain never runs on a hold. The constructor refuses a negative lower bound under an entropic, Tsallis or log-barrier rule.

merge_states on the state and Online(head) are refused by name. An update depends on the order of the rows, and the family never refits from a buffer.

Mathematical definition

\[\begin{align} \boldsymbol{w}_1 &= \underset{\boldsymbol{w} \in \mathcal{W}}{\arg\min} \; D_\Psi(\boldsymbol{w}, \boldsymbol{w}_0)\,, \\ \boldsymbol{w}_{t+1} &= \underset{\boldsymbol{w} \in \mathcal{W}}{\arg\min} \; D_\Psi(\boldsymbol{w}, \boldsymbol{q})\,, \\ x_{tj} &= \begin{cases} 1 + r_{tj} & \text{if } r_{tj} \text{ is finite}\,, \\ 1 & \text{otherwise}\,, \end{cases} \\ S_T &= \prod_{t=1}^{T} \langle \boldsymbol{w}_t, \boldsymbol{x}_t \rangle\,. \end{align}\]

Where:

  • $\boldsymbol{w}_1$: Start Allocation, the allocation that the recursion starts from.
  • $\boldsymbol{w}_0$: Given start, or $\boldsymbol{1}/N$ when none is given.
  • $\boldsymbol{w}_t$: Iterate of the rule at period $t$, the allocation its update reads.
  • $\boldsymbol{q}$: Raw step, the vector the rule's unconstrained step gives before the projection onto the Allocation Set. The rule makes it from $\boldsymbol{w}_t$ and $\boldsymbol{x}_t$.
  • $\mathcal{W}$: Allocation Set.
  • $D_\Psi$: Bregman divergence of $\Psi$.
  • $\Psi$: Potential of the Projection Geometry.
  • $\boldsymbol{x}_t$: Price relative vector of period $t$, each asset's price over its price one period earlier.
  • $r_{tj}$: Return of asset $j$ at time $t$.
  • $S_T$: Wealth of the fund after $T$ periods, from a wealth of one.
  • $t$: Period index, counted from one.
  • $T$: Number of observations.
  • $N$: Number of assets.

The Next-Period Allocation after $T$ rows is $\boldsymbol{w}_{T+1}$. Both projections are one call of project, so a ProgrammeAllocationSet adds its Objective Penalty to both objectives.

Fields

  • alg: The Online Selection Rule: the update, its parameters and its Projection Geometry.
  • set: The Allocation Set every allocation of the recursion lies in, or a TimeDependent schedule of one set per fold. Entry i is the complete set of fold i. The fold loop puts it in place before it folds the rows of fold i, so those rows step inside entry i, and the read-out reports it.
  • w0: The Start Allocation over the pinned universe, or nothing for 1/N over every pinned name.
  • fees: Fees estimator or result.
  • fb: Fallback result or estimator.
  • strict: Whether a constraint that names an asset or a group that sets does not hold raises an error. When it is false, the constraint generator warns and skips the entry. The flag reaches every constraint of the set, the fee and the read-out, and the constraints that a programme set resolves on the rows at each step. It also governs a Held Gap, which warns when it is false and raises an error when it is true.
  • cache: The Partial Fit State of the recursion, nothing until partial_fit! writes one. factory carries it unchanged and port_opt_view slices it to the selected assets.

Constructors

OnlinePortfolioSelection(;    alg::AbstractOnlinePortfolioSelectionAlgorithm = ExponentiatedGradient(),    set::TD{<:AbstractAllocationSet} = BoundedAllocationSet(),    w0::Option{<:AbstractVector} = nothing,    fees::TD_Option{<:FeesE_Fees} = nothing,    fb::TDO_Option{<:OptE_Opt} = nothing,    strict::Bool = false,    cache::Option{<:OnlinePortfolioSelectionState} = nothing) -> OnlinePortfolioSelection

Keywords correspond to the struct's fields. set, fees and fb can hold a TimeDependent per-fold schedule. alg, w0 and strict are static.

Validation

  • w0: non-empty and finite, when given. It need not lie in the set, because the first step projects it onto the set.
  • If fees is a FeesEstimator: !isnothing(set.sets). A schedule on set is held to it per entry.
  • Everything assert_geometry_admits_set refuses: a negative lower bound under an entropic, Tsallis or log-barrier rule. A schedule on set is held to it per entry.
  • Everything assert_rule_admits_set refuses: a solver-free leader under a programme set. A schedule on set is held to it per entry.
  • fb schedules: bind !== :nearest.

Propagated parameters

When factory is called on this type, the following fields are propagated:

  • fees: Recursively updated via factory, which threads the previous weights into a turnover fee.
  • fb: Recursively updated via factory.

View parameters

When port_opt_view is called on this type, the fields with a per-asset axis are subset to the selected indices: alg and set recursively, w0 sliced and renormalised, fees viewed at the indices, and cache through the state's own view.

Examples

julia> OnlinePortfolioSelection()OnlinePortfolioSelection     alg ┼ MirrorDescent         │     eta ┼ Float64: 0.05         │    proj ┼ EntropicProjection()         │   alpha ┼ Int64: 0         │     obj ┼ LogWealth()         │    grad ┴ PlainGradient()     set ┼ BoundedAllocationSet         │     wb ┼ WeightBounds         │        │   lb ┼ Float64: 0.0         │        │   ub ┴ Float64: 1.0         │   sets ┴ nothing      w0 ┼ nothing    fees ┼ nothing      fb ┼ nothing  strict ┴ Bool: false

Related

References

  • [151] B. Li and S. C. H. Hoi. Online portfolio selection: A survey. ACM Computing Surveys 46(3), 1–36 (2014).
source
PortfolioOptimisers.optimise — Method
optimise(opt::OnlinePortfolioSelection{<:Any, <:Any, <:Any, <:Any, Nothing}, rd::ReturnsResult; kwargs...) -> NaiveOptimisationResult

Runs the Causal Pass over rd, and returns the Next-Period Allocation.

Arguments

  • opt: The head.
  • rd: The returns result to use. Every row is one Online Update, in order.
  • kwargs: Additional keyword arguments, ignored.

Validation

  • No field in the tree of opt holds an Online. An ArgumentError naming the field is thrown otherwise, through assert_batch_entry.

Returns

  • res::NaiveOptimisationResult: The Next-Period Allocation, with pr = nothing.

Related

source
PortfolioOptimisers.optimise — Method
optimise(opt::OnlinePortfolioSelection; kwargs...) -> OptimisationResult

Returns the Recursion Read-out, the allocation of the state as a Result, and runs no batch path.

The method refuses by name a head that has taken no step. The read-out fails in one way, when the allocation has no mass on the assets active at the last row. The fallback chain then runs as the batch verb runs it, and each fallback reads out through its own optimise(fb). A fallback that needs rows has folded none and refuses. So the fallback that serves here is a head that needs no rows, such as PreviousWeights.

Algorithm

  1. Refuse a head whose cache is nothing.
  2. Reset every schedule of the head to its fold-less value through reset_time_dependent_estimator.
  3. Read the state out through online_selection_readout, giving res.
  4. Return res when it succeeded or when the head has no fallback.
  5. Otherwise warn, read the fallback out with optimise(opt.fb), and return its Result through factory, which records the failed pair of opt and res.

Arguments

  • opt: The stepped head.
  • kwargs: Ignored.

Validation

  • opt.cache is not nothing. An ArgumentError is thrown otherwise.

Returns

  • res::OptimisationResult: The Next-Period Allocation, or the Result of the fallback chain when the read-out fails.

Related

source
PortfolioOptimisers.partial_fit! — Method
partial_fit!(opt::OnlinePortfolioSelection, rd::ReturnsResult)

Folds the rows of a carrier into the head's recursion as the Block Step, and reads nothing out.

Each row of rd is one Online Update, in order, and the state never learns the cadence of the loop. A fold of k rows is k updates and no read-out. After a fold of rows 1:t, optimise(opt) equals optimise(opt, rd[1:t]) exactly, because both take the same t single-row updates.

Arguments

  • opt: The head.
  • rd: The carrier holding one row or a block of them, observations × assets.

Validation

Returns

  • opt: The head, with its cache rebound to the state after the last row.

Related

source
PortfolioOptimisers.Online — Method
Online(::OnlinePortfolioSelection, args...; kwargs...)

Refuses to construct Online(head), and names the three routes to the two answers that the wrapper stands for.

Online declares a refit from a buffer, and for a head each of its two settings is a batch walk-forward that the library already runs. Uncapped, the refit is the expanding walk-forward. Its allocation equals the allocation of the online arm exactly, because both take the same single-row updates from w0. But it costs a pass over every row folded so far, where the online arm costs one row. Capped, the refit is the rolling walk-forward, whose allocation is the recursion restarted from w0 inside each window. So the wrapper adds no answer, and the refusal costs the caller nothing.

Related

source
PortfolioOptimisers._optimise — Method
_optimise(
    opt::OnlinePortfolioSelection,
    rd::ReturnsResult;
    kwargs...
) -> NaiveOptimisationResult{Nothing, __T_wb, __T_fees, __T_retcode, __T_w, __T_imsk, Nothing} where {__T_wb, __T_fees, __T_retcode, __T_w, __T_imsk}

Runs the Causal Pass.

optimise calls this internal dispatch. It reads and writes no state that the head carries.

Algorithm

  1. Reset every schedule of the head to its fold-less value through reset_time_dependent_estimator.
  2. Fold every row of rd from the Start Allocation, with no carried state, through fold_online_selection, giving state.
  3. Read the Next-Period Allocation out of state through online_selection_readout.

Related

source
PortfolioOptimisers.rows_needed — Method
rows_needed(td::TimeDependent)

Returns the number of rows that a per-fold schedule of Allocation Sets needs.

The count is the maximum over every entry of a vector schedule and over its explicit default. A callable schedule answers nothing, which is unbounded. The seed sizes the rows buffer once and never resizes it, so the cap must answer for every fold, and not only for the fold in which the seed runs. The entries of a callable do not exist before their fold, so the cap keeps every row folded so far, and no set of a fold can read a row that the cap dropped.

Related

source

References

[151]
B. Li and S. C. Hoi. Online portfolio selection: A survey. ACM Computing Surveys 46, 1–36 (2014).