Net returns and drawdowns: private API

PortfolioOptimisers.AbstractWeightDriftType
abstract type AbstractWeightDrift <: AbstractAlgorithm

Supertype for the algorithms that let a portfolio's weights drift with its own returns over the observations it is scored on.

A cross-validation scheme carries this family in its wd field. nothing scores every observation against the same weight vector, which is the library's original behaviour and stays its default. SelfFinancingDrift is the family's one leaf.

Related

source
PortfolioOptimisers.drift_position_valuesFunction
drift_position_values(wd::AbstractWeightDrift, w::VecNum, X::MatNum)

Compute the value of each position at each observation of a drifted window.

Each position starts at its weight and compounds at its own asset return, so row t holds what one unit of initial capital has become in each asset by the end of observation t. The matrix is the quantity every other drift verb reads, and it is a cumulative product, not a per step renormalisation.

Mathematical definition

\[\begin{align} P_{t j} &= w_{j} \prod\limits_{s=1}^{t} \left(1 + X_{s j}\right) \end{align}\]

Where:

  • $P_{t j}$: Value of the position in asset $j$ at the end of observation $t$, per unit of initial capital.
  • $X_{s j}$: Return of asset $j$ at observation $s$.
  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $T$: Number of observations.
  • $N$: Number of assets.

Algorithm

  1. Compound the asset returns with relative_cumulative_returns, giving the T × N matrix of growth factors of each asset up to each observation.
  2. Scale column j of that matrix by w[j], giving the position values.

Arguments

  • wd: Weight drift algorithm.
  • w: Portfolio weights.
  • X: Asset return matrix (observations × assets).

Returns

  • P::MatNum: T × N matrix of position values.

Related

source
PortfolioOptimisers.drift_wealthFunction
drift_wealth(P::MatNum, w::VecNum)

Compute the wealth of each observation of a drifted window from its position values.

The cash position is what the weights leave uninvested, 1 - sum(w), and it earns zero, so it enters every observation's wealth unchanged. A fully invested book holds none of it, and a levered or short book holds a negative one.

Mathematical definition

\[\begin{align} V_{t} &= \sum\limits_{j=1}^{N} P_{t j} + \left(1 - \sum\limits_{j=1}^{N} w_{j}\right) \end{align}\]

Where:

  • $V_{t}$: Wealth at the end of observation $t$, per unit of initial capital.
  • $P_{t j}$: Value of the position in asset $j$ at the end of observation $t$.
  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $N$: Number of assets.

Algorithm

  1. Sum P along its asset axis, giving the invested wealth of each observation.
  2. Add the cash position 1 - sum(w) to every entry.

Arguments

Returns

  • V::VecNum: T × 1 vector of wealth, one entry per observation.

Related

source
PortfolioOptimisers.drift_returnsFunction
drift_returns(V::VecNum)

Read a wealth vector as the return series of the drifted window.

The return of an observation is the ratio of its wealth to the wealth before it, and the wealth before the first observation is the initial capital of one. The series is therefore the wealth ratio of the drifted holdings, and it is not the dot product of the held weights with the asset returns: the two differ by up to 809 ulp, and the wealth ratio is what the reference arithmetic computes.

Mathematical definition

\[\begin{align} R_{t} &= \frac{V_{t}}{V_{t-1}} - 1\,, \qquad V_{0} = 1 \end{align}\]

Where:

  • $R_{t}$: Return of observation $t$.
  • $V_{t}$: Wealth at the end of observation $t$, per unit of initial capital.
  • $T$: Number of observations.

Algorithm

  1. Prepend the initial capital of one to V, and drop its last entry, giving the wealth before each observation.
  2. Divide V by that vector elementwise, and subtract one.

Arguments

Returns

  • R::VecNum: T × 1 vector of drifted returns.

Related

source
PortfolioOptimisers.non_positive_wealth_indexFunction
non_positive_wealth_index(V::VecNum)

Find the first observation whose wealth is not positive.

A NaN wealth fails > 0 and is found, so an exactly zero wealth and the non-finite values it makes downstream are both caught here.

Algorithm

  1. Return the index of the first entry of V that does not satisfy x > 0, and nothing when every entry does.

Arguments

Returns

  • i::Option{<:Integer}: Index of the first non-positive wealth, or nothing.

Related

source
PortfolioOptimisers.assert_positive_wealthFunction
assert_positive_wealth(V::VecNum, obs = nothing, member = nothing)

Check that every observation of a drifted window has a positive wealth.

The check runs before any return is formed, so a ruined window gives no partial series. obs names the failing observation in the message: a vector of labels names it by its label, a vector of integers names it by its panel row, and nothing names it by its row inside the window.

Algorithm

  1. Find the first non-positive wealth with non_positive_wealth_index. Return nothing when there is none.
  2. Build the phrase that names the failing observation, reading obs.
  3. Raise a NonPositiveWealthError that states the condition, the failing wealth, that phrase, and the member when one is named.

Arguments

  • V: T × 1 vector of wealth, from drift_wealth.
  • obs: Observation labels of the window, panel rows of the window, or nothing.
  • member: Index of the population member the wealth belongs to, or nothing for a single weight vector.

Validation

Returns

  • nothing.

Related

source
PortfolioOptimisers.weight_pathFunction
weight_path(wd::AbstractWeightDrift, w::VecNum, X::MatNum, obs = nothing)

Compute the weights held at each observation of a drifted window.

Row t holds the weights the book carries through observation t, so the first row is the target weights and every later row is the previous observation's position values deflated by its wealth. The rows and the deflated cash position sum to one at every observation.

Mathematical definition

\[\begin{align} U_{1 j} &= w_{j}\\ U_{t j} &= \frac{P_{t-1,\, j}}{V_{t-1}}\,, \qquad t > 1 \end{align}\]

The equivalent closed form is the self-financing recursion, which a reader may recognise from the literature:

\[\begin{align} \boldsymbol{u}_{t+1} &= \frac{\boldsymbol{u}_{t} \odot \left(1 + \boldsymbol{x}_{t}\right)}{1 + \boldsymbol{u}_{t} \cdot \boldsymbol{x}_{t}} \end{align}\]

The two forms agree in exact arithmetic and differ in floating point: the cumulative product is what the code computes, and the recursion was measured up to 824 ulp away from it.

Where:

  • $U_{t j}$: Weight held in asset $j$ through observation $t$.
  • $P_{t j}$: Value of the position in asset $j$ at the end of observation $t$.
  • $V_{t}$: Wealth at the end of observation $t$.
  • $\boldsymbol{u}_{t}$: N × 1 vector of weights held through observation $t$.
  • $\boldsymbol{x}_t$: Asset returns for observation $t$, the $t$-th row of the returns matrix.
  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $T$: Number of observations.
  • $N$: Number of assets.
  • $\odot$: Elementwise (Hadamard) multiplication.

Algorithm

  1. Compute the position values with drift_position_values and the wealth with drift_wealth.
  2. Check the wealth with assert_positive_wealth.
  3. Write the target weights into the first row.
  4. Write each earlier observation's position values, deflated by that observation's wealth, into the row after it.

Arguments

  • wd: Weight drift algorithm.
  • w: Portfolio weights.
  • X: Asset return matrix (observations × assets).
  • obs: Observation labels of the window, panel rows of the window, or nothing. Read only by the message of step 2.

Validation

Returns

  • U::MatNum: T × N matrix of held weights, one row per observation.

Examples

julia> PortfolioOptimisers.weight_path(SelfFinancingDrift(), [0.5, 0.5], [0.1 -0.1; 0.2 0.0])2×2 Matrix{Float64}: 0.5   0.5 0.55  0.45

Related

source
weight_path(hw::Nothing, w::VecNum, X::MatNum)
weight_path(hw::Nothing, w::VecVecNum, X::MatNum)
weight_path(hw::HeldWeightsResult, w::VecNum_VecVecNum, args...)

Read the weight path of a fold, from the record the fold carries.

A fold that carries no record held its target weights on every one of its observations, so its path is the constant path of w. A fold that carries one gives the stored path when store_weight_path asked for it, and rebuilds the path from the record's own start weights, asset returns and Weight Drift when it did not. The rebuild is bit-identical to the store, because the record carries the form that ran and the weights it started from.

Algorithm

  1. On nothing, repeat w over the rows of X.
  2. On a record, read rebuild_weight_path, which gives the stored path or rebuilds it.

Arguments

  • hw: Held-weights record of the fold, or nothing.
  • w: Target weights of the fold, which are the first row of the path when there is no record. A record carries its own start weights w0, so a record ignores this argument: on a failed fold the target is NaN and the path is not.
  • X: Asset returns of the fold. A record carries its own, so a record ignores this argument.

Returns

  • MatNum_VecMatNum: The weight path, observations × assets, one matrix per member under a population.

Related

source
PortfolioOptimisers.held_weightsFunction
held_weights(wd::AbstractWeightDrift, w::VecNum, X::MatNum, obs = nothing)

Compute the weights held after the last observation of a drifted window.

These are the weights a fund carries into the next rebalance, so they are the base a trade is measured against. They are one step beyond the last row of weight_path, which holds the weights carried through the last observation.

Mathematical definition

\[\begin{align} h_{j} &= \frac{P_{T j}}{V_{T}} \end{align}\]

Where:

  • $h_{j}$: Weight held in asset $j$ after the last observation.
  • $P_{t j}$: Value of the position in asset $j$ at the end of observation $t$.
  • $V_{t}$: Wealth at the end of observation $t$.
  • $T$: Number of observations.
  • $N$: Number of assets.

Algorithm

  1. Compute the position values with drift_position_values and the wealth with drift_wealth.
  2. Check the wealth with assert_positive_wealth.
  3. Divide the last row of the position values by the last wealth.

Arguments

  • wd: Weight drift algorithm.
  • w: Portfolio weights.
  • X: Asset return matrix (observations × assets).
  • obs: Observation labels of the window, panel rows of the window, or nothing. Read only by the message of step 2.

Validation

Returns

  • h::VecNum: N × 1 vector of held weights.

Examples

julia> PortfolioOptimisers.held_weights(SelfFinancingDrift(), [0.5, 0.5], [0.1 -0.1; 0.2 0.0])2-element Vector{Float64}: 0.5945945945945945 0.4054054054054054

Related

source
PortfolioOptimisers.drifted_weight_pathFunction
drifted_weight_path(P::MatNum, V::VecNum, w::VecNum)

Read the weights held through each observation of a drifted window, from one pass of the drift.

The weights held through observation t are the position values after observation t - 1, quoted against the wealth after observation t - 1. The first row is the target weights, because nothing has drifted yet when the window opens. weight_path is the verb that makes P and V itself; this one reads a pass a caller already holds, so a caller that needs the path and the held weights pays one pass rather than two.

Mathematical definition

\[\begin{align} u_{1 j} &= w_{j}\,,\\ u_{t j} &= \frac{P_{t-1, j}}{V_{t-1}}\,, \qquad t > 1\,. \end{align}\]

Where:

  • $P$ is the matrix of position values.
  • $V$ is the wealth of each observation.
  • $w$ is the vector of target weights.

Algorithm

  1. Write the target weights into the first row.
  2. Write each later row as the position values of the observation before it, divided by the wealth of that observation.

Arguments

Returns

  • MatNum: The weight path, observations × assets.

Related

source
PortfolioOptimisers.drifted_held_weightsFunction
drifted_held_weights(P::MatNum, V::VecNum)

Read the weights held after the last observation of a drifted window, from one pass of the drift.

These are the weights a chain carries forward. They are one step beyond the last row of the weight path: that row holds the weights the window opened its last observation with, and these hold the weights it closed that observation with.

Mathematical definition

\[\begin{align} u_{T+1, j} &= \frac{P_{T j}}{V_{T}}\,. \end{align}\]

Where:

  • $P$ is the matrix of position values.
  • $V$ is the wealth of each observation.
  • $T$ is the number of observations of the window.

Algorithm

  1. Divide the last row of the position values by the last wealth.

Arguments

Returns

  • VecNum: The weights held after the last observation.

Related

source
PortfolioOptimisers.assert_held_weights_shapeFunction
assert_held_weights_shape(U::Nothing, X::MatNum, w::VecNum)
assert_held_weights_shape(U::Nothing, X::MatNum, w::VecVecNum)
assert_held_weights_shape(U::MatNum, X::MatNum, w::VecNum)
assert_held_weights_shape(U::VecMatNum, X::MatNum, w::VecVecNum)

Check that the members of a HeldWeightsResult agree on their shape.

The method is chosen by the pair (U, w), so a stored path under a single weight vector and a stored path under a population are separate checks, and an absent path checks only the weights. A pair that does not match at all, such as a matrix path beside a population's weights, reaches no method and raises a MethodError, which names both shapes.

Algorithm

  1. Check that w holds one weight for each column of X, member by member under a population.
  2. Check that a stored U has the size of X, member by member under a population.

Arguments

  • U: Weight path, or nothing.
  • X: Asset returns of the fold.
  • w: Held weights after the last observation.

Returns

  • nothing.

Related

source
PortfolioOptimisers.assert_held_start_shapeFunction
assert_held_start_shape(w0::VecNum, w::VecNum)
assert_held_start_shape(w0::VecVecNum, w::VecVecNum)

Check that the start weights of a HeldWeightsResult have the shape of its held weights.

A single weight vector starts from a single vector, and a population starts from one vector per member. A pair that mixes the two reaches no method and raises a MethodError, which names both shapes.

Algorithm

  1. Under a single weight vector, nothing further: assert_held_weights_shape already checked each against the columns of X.
  2. Under a population, check that w0 has one vector per member of w.

Arguments

  • w0: Weights the drift started from.
  • w: Held weights after the last observation.

Returns

  • nothing.

Related

source
PortfolioOptimisers.rebuild_weight_pathFunction
rebuild_weight_path(U::MatNum_VecMatNum, wd::AbstractWeightDrift, w::VecNum_VecVecNum, X::MatNum)
rebuild_weight_path(U::Nothing, wd::AbstractWeightDrift, w::VecNum, X::MatNum)
rebuild_weight_path(U::Nothing, wd::AbstractWeightDrift, w::VecVecNum, X::MatNum)

Give a stored weight path, or rebuild one that was not stored.

The stored path is read by dispatch on U, so weight_path tests nothing. A rebuild reruns the drift that made the record over the record's own asset returns, which is what makes it bit-identical to the store.

Algorithm

  1. On a stored U, return it.
  2. On nothing under a single weight vector, run the drift over X and give its path. A non-finite start vector, which is a failed fold with nothing to hold, gives a NaN path, as the store would.
  3. On nothing under a population, run the drift member by member, and fill a member whose wealth is not positive, or whose start vector is not finite, with NaN. That is what the store holds for such a member, so the rebuild stays bit-identical to it. held_weights_result already raised on a population every member of which is ruined, so no record reaching this verb holds one.

Arguments

  • U: Stored weight path, or nothing.
  • wd: Weight drift that made the record.
  • w: Weights the drift started from, w0 of the record.
  • X: Asset returns of the fold.

Returns

  • MatNum_VecMatNum: The weight path.

Related

source
PortfolioOptimisers.held_weights_driftFunction
held_weights_drift(wd::Nothing, pws::Nothing)
held_weights_drift(wd::AbstractWeightDrift, pws::Any)
held_weights_drift(wd::Nothing, pws::AbstractPreviousWeightsSource)

Resolve the one Weight Drift a fold runs, from the two switches of its scheme.

The two switches are independent, and either one alone asks for a drift. A scheme that drifts its return series drifts its held weights the same way. A scheme that drifts nothing but threads drifted weights runs the form the DriftedWeights source carries, because the series stays at the target weights and the holdings still move.

Algorithm

  1. Neither switch is set: no drift runs, and the fold carries no HeldWeightsResult.
  2. wd is set: it is the form that runs, whatever pws is.
  3. Only pws is set: the form it carries is the form that runs.

Arguments

  • wd: Weight drift of the scheme, or nothing.
  • pws: Previous-weights source of the scheme, or nothing.

Returns

  • Option{<:AbstractWeightDrift}: The form that runs, or nothing.

Related

source
PortfolioOptimisers.nan_held_memberFunction
nan_held_member(X::MatNum, store_weight_path::Bool, ::Type{Tw})

The NaN held weights and, under the flag, the NaN weight path of a member that drifted nothing.

A member that failed with nothing to hold, or whose drifted wealth is not positive, records NaN at every asset and every observation. The two builders of a HeldWeightsResult read this one place for that shape, so the store and the rebuild stay bit-identical.

Arguments

  • X: Asset returns of the fold.
  • store_weight_path: If true, give the NaN path as well; else nothing in its place.
  • Tw: Element type of the record.

Returns

  • (w, U): The NaN held weights, and the NaN path or nothing.

Related

source
PortfolioOptimisers.held_weights_memberFunction
held_weights_member(wd::AbstractWeightDrift, wi::VecNum, X::MatNum, store_weight_path::Bool, ::Type{Tw})

Drift one member of a population, and say whether the drift ruined it.

Algorithm

  1. A non-finite start vector drifts nothing: give nan_held_member's shape and false, because a failure is not a ruin.
  2. Otherwise drift it. A wealth that is not positive gives the same shape and true.
  3. A surviving member gives its held weights, its path under the flag, and false.

Arguments

  • wd: Weight drift that runs.
  • wi: Start weights of the member.
  • X: Asset returns of the fold.
  • store_weight_path: If true, give the path as well; else nothing in its place.
  • Tw: Element type of the record.

Returns

  • (w, U, ruined): The held weights, the path or nothing, and whether the member is ruined.

Related

source
PortfolioOptimisers.held_weights_resultFunction
held_weights_result(wd::Nothing, w::VecNum_VecVecNum, X::MatNum, store_weight_path::Bool, obs = nothing)
held_weights_result(wd::AbstractWeightDrift, w::VecNum, X::MatNum, store_weight_path::Bool, obs = nothing)
held_weights_result(wd::AbstractWeightDrift, w::VecVecNum, X::MatNum, store_weight_path::Bool, obs = nothing)

Build the HeldWeightsResult of a fold, and name the members the drift ruined.

The verb runs the drift once and reads three things off that one pass: the weights held after the last observation, the weight path when the caller asked for it, and the members whose wealth is not positive. A single weight vector is a population of one, so a ruined single vector raises rather than reporting a ruined member.

A non-finite start vector is a fold, or a member, that failed with no previous weights to hold, and it is not ruined: nothing drifts, the record carries NaN, and the member keeps the failure code its own optimisation gave it. held_start_weights is what hands a failed fold its previous weights instead, when it has any.

Algorithm

  1. With no drift, give (nothing, nothing). The fold held its target weights, so there is nothing to record.
  2. Over one weight vector, drift it, check the wealth with assert_positive_wealth, and record the held weights and, under the flag, the path. A non-finite vector drifts nothing and records NaN.
  3. Over a population, drift each member. A ruined member records NaN and is named in the second value; a non-finite member records NaN and is not named. Raise when every member is ruined.

Arguments

  • wd: Weight drift that runs, from held_weights_drift, or nothing.
  • w: Weights the drift starts from, w0 of the record: the fold's target weights, or the previous weights a failed fold holds.
  • X: Asset returns of the fold.
  • store_weight_path: If true, store the weight path on the record.
  • obs: Observation labels the wealth message names, see assert_positive_wealth.

Returns

  • (hw, ruined): The record, or nothing; and the indices of the ruined members, or nothing.

Related

source
PortfolioOptimisers.drift_observationsFunction
drift_observations(ts::Nothing, test_idx)
drift_observations(ts::Any, test_idx)

Name the observations of a fold, for the message a non-positive wealth raises.

A fold that carries timestamps names its observations by their labels. A fold that carries none names them by their absolute rows of the panel, which the fold's own test indices are.

Algorithm

  1. Give the timestamps when the fold carries them.
  2. Give the test indices when it does not.

Arguments

  • ts: Timestamps of the fold, or nothing.
  • test_idx: Absolute panel rows of the fold.

Returns

Related

source
PortfolioOptimisers.investable_reductionMethod
investable_reduction(X::MatNum, w, fees::Option{<:Fees}, strict::Bool)
investable_reduction(rd::AbstractReturnsResult, w, fees::Option{<:Fees}, strict::Bool)
investable_reduction(pr::AbstractPriorResult, w, fees::Option{<:Fees}, strict::Bool)
investable_reduction(imsk::Nothing, pr::AbstractPriorResult, w, fees::Option{<:Fees}, strict::Bool)
investable_reduction(imsk::BitVector, pr::AbstractPriorResult, w, fees::Option{<:Fees}, strict::Bool)

Reduce a prior result, the weights scored against it and the fees charged on them to the Investable Mask.

An optimiser reduces once at its entry, so no optimiser meets a gap. A caller who scores a weight vector against a prior result by hand does meet one: a non-investable asset carries NaN in mu, on the diagonal of sigma and down its column of pr.X, so dot(w, pr.sigma, w) and pr.X * w are NaN at any weight, the optimiser's own zero included. expected_risk, expected_return, risk_contribution and factor_risk_contribution reduce here instead, and a per-asset answer expands back through expand_investable_weights.

Every block of the prior is reduced together by the port_opt_view method the prior's owner already writes, so a new block cannot be forgotten, and the reduced pr.X carries no dead column. The fees travel with the weights, because a Fees whose rates are one number per asset is indexed by the same axis and would otherwise meet a shorter weight vector.

The fee takes the same door the fit sites take, investable_fees_view, under every carrier. A caller states the two liquidation carriers of a Fees over the full universe, so on a prior with no mask nothing left and the carriers are dropped; a result's fee is marked with the mask it was reduced on, so it passes the door untouched and the exit it carries is charged. Without the door, an all-investable prior charged a caller's full-universe carrier as a forced exit of the whole book on every period, which is the defect of #1067.

A held non-investable asset is a holding the prior cannot value. It takes the library's strictness policy through strict_diagnostic: a warning names the assets and their weights are dropped, or an ArgumentError names them under strict.

A bare returns matrix and a ReturnsResult carry no moments, so no mask exists to derive and they pass through, the fee alone taking its door under a nothing mask. That is what lets one door state the reduction once and dispatch decide whether it happens.

Algorithm

  1. Return nothing, the carrier and the weights unchanged, and the fee through investable_fees_view under a nothing mask, when the carrier is a matrix or a returns result.
  2. Derive the Investable Mask once with investable_mask.
  3. Return the same when every asset is investable.
  4. Otherwise report the held non-investable assets through strict_diagnostic.
  5. Return the mask, a port_opt_view of the prior at findall(imsk), the view of the weights at the mask, and the fee through investable_fees_view at the mask and the prior's unreduced pr.X.

Arguments

  • pr: Prior result, ReturnsResult, or asset returns matrix.
  • w: Portfolio weights, a population of them, or a weight path (observations × assets).
  • fees: Fees the figure is charged against, or nothing.
  • strict: Whether a held non-investable asset raises rather than warns.

Validation

  • Every asset the weights hold is investable, else a warning naming the assets is emitted, or an ArgumentError naming them is raised under strict.

Returns

  • (imsk, pr, w, fees): The Investable Mask and the three reduced to it, or nothing, the carrier and the weights unchanged, and the fee on the axes a nothing mask leaves.

Related

source
PortfolioOptimisers.investable_returns_viewFunction
investable_returns_view(imsk::Nothing, rd::AbstractReturnsResult)
investable_returns_view(imsk::BitVector, rd::AbstractReturnsResult)

Take the view of a returns result at the Investable Mask.

The pairing of investable_reduction with the returns data a value-level verb takes beside the prior, so a factor regression that fits its own loadings fits them over the live assets alone.

Arguments

  • imsk: The Investable Mask, or nothing.
  • rd: The returns result to use.

Returns

  • rd::AbstractReturnsResult: The returns result reduced to the investable assets, or unchanged.

Related

source
PortfolioOptimisers.held_gap_pairsFunction
held_gap_pairs(w::VecNum, X::MatNum)
held_gap_pairs(w::VecVecNum, X::MatNum)
held_gap_pairs(W::MatNum, X::MatNum)

Find the Held Gaps of a window.

A Held Gap is an (observation, asset) pair at which the portfolio's weight is non-zero and the asset's return is missing. It arises where the universe changes after the fit: an asset that was investable on the training window and delists inside the test window. A pair with a zero weight contributes nothing whatever it holds, and is never a Held Gap.

The weight argument's type is the picker, as it is for calc_net_returns. A VecNum is one target weight vector, and it weighs every observation, so a whole column is held or none of it is. A MatNum is a weight path, and each pair is judged by the weight held through its own observation. A VecVecNum is a population, and a column is held when any member holds it.

Arguments

  • w: Portfolio weights, a population of them, or a weight path (observations × assets).
  • X: Asset returns, observations × assets.

Returns

  • held::Vector{Tuple{Int, Int}}: The (observation, asset) pairs, empty when there are none.

Related

source
PortfolioOptimisers.filter_held_gapsFunction
filter_held_gaps(w::Union{<:VecNum, <:VecVecNum, <:MatNum}, X::MatNum, strict::Bool;
                 nx::Option{<:VecStr} = nothing)

Return the window with every non-finite entry replaced by zero, and name the Held Gaps.

A point-in-time panel carries a NaN at every (observation, asset) pair where the asset is inactive: before it lists, and after it delists. 0 * NaN is NaN, so a zero weight does not save the product X * w, and one delisting poisons every observation after it. This is the one place a fold cleans the window it is about to score, and it runs once, over the investable columns alone, before the series is formed and before a Weight Drift compounds.

A Held Gap takes the library's strictness policy through strict_diagnostic: a warning names the pairs and the pair contributes zero, or an ArgumentError names them under strict. A zero weight at a gap is silent. Nothing is renormalised, so the missing weight sits in cash on that observation, which is the one reading that invents no trade the weights never stated.

The funnel calc_net_returns stays the plain product. A scan there is paid at each of its call sites on every evaluation; a scan here is paid once, on a window the fold already multiplies once.

Algorithm

  1. Return X itself when every entry of X is finite.
  2. Otherwise find the Held Gaps with held_gap_pairs and report them through strict_diagnostic, naming the assets by nx when it is given.
  3. Return a copy of X with every non-finite entry replaced by zero.

A fold hands this verb the window viewed at the Investable Mask, so a column index of X is a reduced one and names nothing to the caller. The fold therefore passes the view's nx, and the message names the asset rather than a position the caller cannot look up.

Arguments

  • w: Portfolio weights, a population of them, or a weight path (observations × assets).
  • X: Asset returns, observations × assets.
  • strict: Whether a non-finite return at a held pair raises rather than warns.
  • nx: Optional asset names of the columns of X. When given, the message names the held assets by them; otherwise it reports their column indices.

Validation

  • Every held pair of X is finite, else a warning naming the pairs is emitted, or an ArgumentError naming them is raised under strict.

Returns

  • X::MatNum: The window with no non-finite entry.

Related

source
PortfolioOptimisers.held_gap_msgFunction
held_gap_msg(held::AbstractVector{<:Tuple{Integer, Integer}}, nx::Option{<:VecStr} = nothing)

Write the message a Held Gap raises.

The message names the assets, the count of pairs and the first observation, so a caller can find the delisting that made them, and it states the consequence: the pair contributes zero and the missing weight sits in cash. The assets are named by nx when it is given, because the window a fold filters is viewed at the Investable Mask and a column index of it is a reduced one; with no names the column indices are reported.

Arguments

  • held: The (observation, asset) pairs held_gap_pairs found.
  • nx: Optional asset names of the window's columns.

Returns

  • msg::String: The message.

Related

source
PortfolioOptimisers.expand_investable_columnsFunction
expand_investable_columns(imsk::BitVector, A::MatNum)

Expand a per asset history of the investable universe back onto the full asset universe.

The column of a non-investable asset is filled with zero, which is what filter_held_gaps writes over its whole NaN column anyway. A drift over a zero column at a zero weight moves no wealth, so the expanded history rebuilds the same weight path the reduced one did.

Arguments

  • imsk: The Investable Mask, true at every asset whose prior moments were finite.
  • A: A history over the investable assets, observations × investable assets.

Returns

  • A::MatNum: The history over the full asset universe.

Related

source
PortfolioOptimisers.expand_held_weightsFunction
expand_held_weights(imsk::Nothing, hw)
expand_held_weights(imsk::BitVector, hw::Nothing)
expand_held_weights(imsk::BitVector, hw::HeldWeightsResult)

Expand a fold's Held Weights record back onto the full asset universe.

A fold reduces its test window to the Investable Mask before it reads it, so the record it builds lives on the investable assets alone. The next fold's turnover reads hw.w, and it states the previous weights over the caller's own universe, so the record is expanded once here, on the way out of predict(res::NonFiniteAllocationOptimisationResult, rd::ReturnsResult). Every member of the record is expanded together, so a reader that rebuilds the weight path from hw.X gets the path the fold ran.

Algorithm

  1. Return the record unchanged when the mask is nothing, so a universe with nothing to exclude keeps the path it took before the mask existed.
  2. Return nothing when the fold built no record.
  3. Otherwise expand the asset returns, the stored weight path and the held weights, and rebuild the record around them.

Arguments

  • imsk: The Investable Mask, or nothing.
  • hw: The fold's Held Weights record, or nothing.

Returns

  • hw::Option{<:HeldWeightsResult}: The record over the full asset universe.

Related

source
PortfolioOptimisers.expand_held_memberFunction
expand_held_member(imsk::BitVector, x::Nothing)
expand_held_member(imsk::BitVector, x::VecNum)
expand_held_member(imsk::BitVector, x::MatNum)
expand_held_member(imsk::BitVector, x::VecVecNum)
expand_held_member(imsk::BitVector, x::VecMatNum)

Expand one member of a Held Weights record back onto the full asset universe.

The member's type is the picker, so expand_held_weights states no branch of its own. A weight vector expands through expand_investable_weights and a weight path through expand_investable_columns; a population expands member by member; an absent path stays absent.

Arguments

  • imsk: The Investable Mask, true at every asset whose prior moments were finite.
  • x: Held weights, a weight path, a population of either, or nothing.

Returns

  • The member over the full asset universe.

Related

source
PortfolioOptimisers.charge_feesFunction
charge_fees(r::VecNum, w::VecNum, fees::Option{<:Fees})

Subtract a fee from a portfolio return series, on the clock the fee states.

l, s and tn are rates per period, so they charge on every observation. fl and fs are currency amounts charged one time for the whole holding period, so the clock decides where in the series they land, and fees.fa names that clock. The length of the holding period is the length of the series, which this verb hands to calc_fees.

Algorithm

  1. A nothing fees returns r unchanged. It charges no fee rather than a zero fee.
  2. Read the pair (amortised, one_time) of calc_fees, over the length of r. This site knows the series it charges, so it hands that length in.
  3. Subtract amortised from every observation.
  4. Subtract one_time from the first observation alone. Under an AmortisedFees it is zero, because step 2 spread that whole cost into amortised, and iszero gates the pass away.

Both clocks charge the same total when the horizon is the length of r. They give a different drawdown, because the first charges the whole cost on one observation and the second charges a fraction of it on each.

Arguments

  • r: Gross portfolio return series.
  • w: Portfolio weights.
  • fees: Fees structure, or nothing.

Returns

  • val::VecNum: The net return series.

Related

source
PortfolioOptimisers.charge_asset_feesFunction
charge_asset_fees(R::MatNum, w::VecNum, fees::Option{<:Fees}, imsk::Option{<:BitVector} = nothing)

Subtract a per asset fee from a per asset return matrix, on the clock the fee states and in the columns it was priced on.

The per asset twin of charge_fees. Its row sums are the series that verb returns, up to the order of summation.

A reduced Fees lives on two axes: the five per asset fields were sliced to the Investable Mask at the door, and the two liquidation carriers to its complement. R and w span the caller's own universe, so imsk is what reunites them, and each axis is charged in the columns it owns through charge_fee_axis!. A liquidated asset earns no return, so its column of R is zero and holds its charge alone: the charge is neither smeared over the assets that stayed nor carried in a matrix of its own.

Algorithm

  1. A nothing fees returns R unchanged. It charges no fee rather than a zero fee.
  2. Read the pair of pairs ((am_i, am_l), (ot_i, ot_l)) of calc_asset_fees, over the row count of R. The weights it is handed are the investable ones, because that is the axis the five per asset fields were sliced to; the carriers read w for its element type alone.
  3. On a nothing imsk, charge the investable axis over the whole matrix, and refuse a fees that carries a liquidation: no mask says where the exits are, so the charge has nowhere to land, and dropping it would understate the return.
  4. On a BitVector imsk, charge the investable axis in the imsk columns and the liquidation axis in the complement's, each by the same two steps: the per period vector on every observation, and the one-off vector on the first alone. Under an AmortisedFees the one-off vector is zero, because step 2 spread that cost into the per period one.

Arguments

  • R: Gross per asset return matrix (observations × assets), on the caller's universe.
  • w: Portfolio weights, on the same universe as R.
  • fees: Fees structure, or nothing.
  • imsk: The Investable Mask, or nothing when no asset left.

Validation

  • imsk spans the columns of R, else a DimensionMismatch naming both widths.
  • Each charge spans the axis it is charged on, through assert_fee_axis_width.
  • A nothing imsk meets no liquidation carrier, else an ArgumentError naming the mask.

Returns

  • val::MatNum: The net per asset return matrix, on the caller's universe.

Related

source
PortfolioOptimisers.charges_nothingFunction
charges_nothing(v)

Report whether a fee term charges nothing at all.

The verb that prices an unset liquidation carrier returns an empty vector rather than a vector of zeros, because it holds no length to build one from: the axis is the complement of the Investable Mask, and a Fees does not carry the mask. A nothing reaches the same sites from the caller that has already taken a step. Both mean one thing, so the sites that ask reach one predicate rather than repeating the pair.

Arguments

  • v: A fee term, a vector or nothing.

Returns

  • val::Bool: true when the term charges nothing.

Related

source
PortfolioOptimisers.charge_fee_axis!Function
charge_fee_axis!(A::AbstractMatrix, am, ot)

Charge one axis of a per asset return matrix on the two clocks a fee states, in place.

The step charge_asset_fees takes once per axis: the per period vector am is subtracted from every row, and the one-off vector ot from the first row alone. A is a view of the columns the axis owns, so the verb never learns which axis it is charging, and the investable fields and the two liquidation carriers take the same two steps.

The one-off charge is one vector written into one row, never an array the size of the matrix: a charge made once is stored once, and under an AmortisedFees it is zero and no row is written at all.

Each step is skipped by its own vector, not by the other's. An empty vector is what an unset term returns, and the two terms of an axis are set independently: a Fees may carry flq and no lq, so a one-off charge is owed on an axis whose per period charge is empty. A step gated on the wrong vector would drop that charge in silence. A nothing am skips the per period step alone, for the caller that has already taken it.

Arguments

  • A: The columns of the matrix this axis owns, charged in place.
  • am: Per period charge of the axis, one entry per column, or nothing.
  • ot: One-off charge of the axis, one entry per column. Zero under an AmortisedFees, which spread it into am.

Returns

  • A::AbstractMatrix: The same matrix, charged.

Related

source
PortfolioOptimisers.assert_fee_axis_widthFunction
assert_fee_axis_width(am, ot, n::Integer, axis::String)

Refuse a per asset charge whose width is not the width of the axis it is charged on.

charge_asset_fees splits a matrix by the Investable Mask and charges each half with the vector priced for it. A vector that does not span its half would broadcast into the wrong columns, or raise a DimensionMismatch naming neither the mask nor the fee, so both vectors of both axes are measured here first. An empty vector is the answer when a term was never set, so it passes: charge_fee_axis! skips the step it belongs to.

Arguments

  • am: Per period charge of the axis, or nothing.
  • ot: One-off charge of the axis.
  • n: Number of columns the axis owns.
  • axis: Name of the axis, for the message.

Validation

  • Each of am and ot is empty or spans n, else a DimensionMismatch naming both widths.

Related

source
PortfolioOptimisers.absolute_drawdown_arrFunction
absolute_drawdown_arr(X::ArrNum; cX::Bool = false, dims::Int = 1) -> ArrNum

Compute the absolute drawdown array for a matrix of cumulative (or raw) returns.

Each element represents the drawdown from the running peak along the specified dimension.

The running peak starts at the initial capital, not at the first observation. accumulate(max, ...) is seeded with init = zero(eltype(X)), which is the value an additive cumulative series starts from, so a series that is under water from the first period reports a negative drawdown there. absolute_drawdown_arr([-0.1, 0.05]) returns [-0.1, -0.05] and not [0.0, 0.0]. That init is the whole convention, and this function holds one of its two definitions.

Algorithm

  1. Read cX, which says whether X is already cumulative.
  2. When cX is false, reach absolute_cumulative_returns along dims, giving the cumulative series cX. When cX is true, take X as that series unchanged.
  3. Take the running maximum of cX along dims with init = zero(eltype(X)), giving the running peak. The init starts the peak at the initial capital of zero.
  4. Subtract the running peak from cX, giving the drawdown array.

Arguments

  • X::ArrNum: Returns array (or cumulative-returns array if cX = true).
  • cX::Bool = false: If true, treat X as already cumulative returns.
  • dims: Dimension along which to perform the computation.

Returns

  • dd::ArrNum: Drawdown array of the same shape as X. Every entry is zero or negative.

Related

source
PortfolioOptimisers.relative_drawdown_arrFunction
relative_drawdown_arr(X::ArrNum; cX::Bool = false, dims::Int = 1) -> ArrNum

Compute the relative drawdown array for a matrix of cumulative (or raw) compounded returns.

Each element represents the relative drawdown from the running peak along the specified dimension.

The running peak starts at the initial capital, not at the first observation. accumulate(max, ...) is seeded with init = one(eltype(X)), which is the value a compound cumulative series starts from, so a series that is under water from the first period reports a negative drawdown there. relative_drawdown_arr([-0.1, 0.05]) returns [-0.09999999999999998, -0.05499999999999994] and not [0.0, 0.0]. That init is the whole convention, and this function holds one of its two definitions.

The two conventions agree to first order on a small return. On [-1e-6, 5e-7] the additive and the compound answers differ by 5.0e-13.

Algorithm

  1. Read cX, which says whether X is already cumulative.
  2. When cX is false, reach relative_cumulative_returns along dims, giving the cumulative series cX. When cX is true, take X as that series unchanged.
  3. Take the running maximum of cX along dims with init = one(eltype(X)), giving the running peak. The init starts the peak at the initial capital of one.
  4. Divide cX by the running peak and subtract one, giving the relative drawdown array.

Arguments

  • X::ArrNum: Returns array (or cumulative-returns array if cX = true).
  • cX::Bool = false: If true, treat X as already cumulative compounded returns.
  • dims: Dimension along which to perform the computation.

Returns

  • dd::ArrNum: Relative drawdown array of the same shape as X. Every entry is zero or negative.

Related

source
PortfolioOptimisers.relative_cumulative_returnsFunction
relative_cumulative_returns(X; dims = 1)

Compute the relative cumulative returns from a return matrix.

Internal helper that computes cumulative returns as cumprod(1 .+ X; dims=dims), returning the cumulative portfolio value relative to the starting value.

The series is quoted against an initial capital of one, so an entry below one is a loss and an entry above one is a gain. relative_drawdown_arr reads that convention through its init = one(eltype(X)).

Algorithm

  1. Add one to every entry of X, giving the per period growth factors.
  2. Take the running product of those factors along dims, giving the cumulative portfolio value relative to the starting value.

Arguments

  • X: Return matrix.
  • dims: Dimension along which to perform the computation.

Returns

  • ret::ArrNum: Relative cumulative return matrix, same shape as X.

Related

source
PortfolioOptimisers.absolute_cumulative_returnsFunction
absolute_cumulative_returns(X; dims = 1)

Compute the absolute cumulative returns from a return matrix.

Internal helper that computes cumsum(X; dims=dims), returning the cumulative sum of portfolio returns.

The series is quoted against an initial capital of zero, so an entry below zero is a loss and an entry above zero is a gain. absolute_drawdown_arr reads that convention through its init = zero(eltype(X)).

Algorithm

  1. Take the running sum of X along dims, giving the cumulative portfolio return.

Arguments

  • X: Return matrix.
  • dims: Dimension along which to perform the computation.

Returns

  • ret::ArrNum: Cumulative return matrix, same shape as X.

Related

source