Net returns and drawdowns: private API
PortfolioOptimisers.AbstractWeightDrift — Type
abstract type AbstractWeightDrift <: AbstractAlgorithmSupertype 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
PortfolioOptimisers.drift_position_values — Function
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
- Compound the asset returns with
relative_cumulative_returns, giving theT × Nmatrix of growth factors of each asset up to each observation. - Scale column
jof that matrix byw[j], giving the position values.
Arguments
wd: Weight drift algorithm.w: Portfolio weights.X: Asset return matrix (observations × assets).
Returns
P::MatNum:T × Nmatrix of position values.
Related
AbstractWeightDriftSelfFinancingDriftdrift_wealth: Contracts this matrix into the wealth of each observation.relative_cumulative_returns: The helper that step 1 reaches.weight_path
PortfolioOptimisers.drift_wealth — Function
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
- Sum
Palong its asset axis, giving the invested wealth of each observation. - Add the cash position
1 - sum(w)to every entry.
Arguments
P:T × Nmatrix of position values, fromdrift_position_values.w: Portfolio weights.
Returns
V::VecNum:T × 1vector of wealth, one entry per observation.
Related
drift_position_values: The matrix this verb contracts.drift_returns: Reads this vector as a series of returns.assert_positive_wealth: The domain check this vector must pass.weight_path
PortfolioOptimisers.drift_returns — Function
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
- Prepend the initial capital of one to
V, and drop its last entry, giving the wealth before each observation. - Divide
Vby that vector elementwise, and subtract one.
Arguments
V:T × 1vector of wealth, fromdrift_wealth.
Returns
R::VecNum:T × 1vector of drifted returns.
Related
drift_wealth: The vector this verb reads.calc_net_returns(w::VecVecNum, X::MatNum, fees, wd::AbstractWeightDrift, obs): Charges the fee against this series.cumulative_returns
PortfolioOptimisers.non_positive_wealth_index — Function
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
- Return the index of the first entry of
Vthat does not satisfyx > 0, andnothingwhen every entry does.
Arguments
V:T × 1vector of wealth, fromdrift_wealth.
Returns
i::Option{<:Integer}: Index of the first non-positive wealth, ornothing.
Related
drift_wealthassert_positive_wealth: The check that raises on this index.NonPositiveWealthErrorOption
PortfolioOptimisers.assert_positive_wealth — Function
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
- Find the first non-positive wealth with
non_positive_wealth_index. Returnnothingwhen there is none. - Build the phrase that names the failing observation, reading
obs. - Raise a
NonPositiveWealthErrorthat states the condition, the failing wealth, that phrase, and the member when one is named.
Arguments
V:T × 1vector of wealth, fromdrift_wealth.obs: Observation labels of the window, panel rows of the window, ornothing.member: Index of the population member the wealth belongs to, ornothingfor a single weight vector.
Validation
V:all(>(0), V), else aNonPositiveWealthErroris raised.
Returns
nothing.
Related
non_positive_wealth_index: The finder step 1 reaches.NonPositiveWealthErrordrift_wealthweight_path
PortfolioOptimisers.weight_path — Function
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 × 1vector 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
- Compute the position values with
drift_position_valuesand the wealth withdrift_wealth. - Check the wealth with
assert_positive_wealth. - Write the target weights into the first row.
- 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, ornothing. Read only by the message of step 2.
Validation
- The wealth of every observation is positive, else a
NonPositiveWealthErroris raised.
Returns
U::MatNum:T × Nmatrix 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.45Related
AbstractWeightDriftSelfFinancingDriftheld_weights: The weights held after the last observation.drift_position_valuesdrift_wealthassert_positive_wealth
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
- On
nothing, repeatwover the rows ofX. - On a record, read
rebuild_weight_path, which gives the stored path or rebuilds it.
Arguments
hw: Held-weights record of the fold, ornothing.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 weightsw0, so a record ignores this argument: on a failed fold the target isNaNand 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
PortfolioOptimisers.held_weights — Function
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
- Compute the position values with
drift_position_valuesand the wealth withdrift_wealth. - Check the wealth with
assert_positive_wealth. - 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, ornothing. Read only by the message of step 2.
Validation
- The wealth of every observation is positive, else a
NonPositiveWealthErroris raised.
Returns
h::VecNum:N × 1vector 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.4054054054054054Related
AbstractWeightDriftSelfFinancingDriftweight_path: The weights held through each observation.drift_position_valuesdrift_wealthassert_positive_wealth
PortfolioOptimisers.drifted_weight_path — Function
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
- Write the target weights into the first row.
- Write each later row as the position values of the observation before it, divided by the wealth of that observation.
Arguments
P: Position values of the window, fromdrift_position_values.V: Wealth of each observation, fromdrift_wealth.w: Target weights of the window.
Returns
MatNum: The weight path,observations × assets.
Related
PortfolioOptimisers.drifted_held_weights — Function
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
- Divide the last row of the position values by the last wealth.
Arguments
P: Position values of the window, fromdrift_position_values.V: Wealth of each observation, fromdrift_wealth.
Returns
VecNum: The weights held after the last observation.
Related
PortfolioOptimisers.assert_held_weights_shape — Function
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
- Check that
wholds one weight for each column ofX, member by member under a population. - Check that a stored
Uhas the size ofX, member by member under a population.
Arguments
U: Weight path, ornothing.X: Asset returns of the fold.w: Held weights after the last observation.
Returns
nothing.
Related
PortfolioOptimisers.assert_held_start_shape — Function
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
- Under a single weight vector, nothing further:
assert_held_weights_shapealready checked each against the columns ofX. - Under a population, check that
w0has one vector per member ofw.
Arguments
w0: Weights the drift started from.w: Held weights after the last observation.
Returns
nothing.
Related
PortfolioOptimisers.rebuild_weight_path — Function
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
- On a stored
U, return it. - On
nothingunder a single weight vector, run the drift overXand give its path. A non-finite start vector, which is a failed fold with nothing to hold, gives aNaNpath, as the store would. - On
nothingunder a population, run the drift member by member, and fill a member whose wealth is not positive, or whose start vector is not finite, withNaN. That is what the store holds for such a member, so the rebuild stays bit-identical to it.held_weights_resultalready raised on a population every member of which is ruined, so no record reaching this verb holds one.
Arguments
U: Stored weight path, ornothing.wd: Weight drift that made the record.w: Weights the drift started from,w0of the record.X: Asset returns of the fold.
Returns
MatNum_VecMatNum: The weight path.
Related
PortfolioOptimisers.held_weights_drift — Function
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
- Neither switch is set: no drift runs, and the fold carries no
HeldWeightsResult. wdis set: it is the form that runs, whateverpwsis.- Only
pwsis set: the form it carries is the form that runs.
Arguments
wd: Weight drift of the scheme, ornothing.pws: Previous-weights source of the scheme, ornothing.
Returns
Option{<:AbstractWeightDrift}: The form that runs, ornothing.
Related
PortfolioOptimisers.nan_held_member — Function
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: Iftrue, give theNaNpath as well; elsenothingin its place.Tw: Element type of the record.
Returns
(w, U): TheNaNheld weights, and theNaNpath ornothing.
Related
PortfolioOptimisers.held_weights_member — Function
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
- A non-finite start vector drifts nothing: give
nan_held_member's shape andfalse, because a failure is not a ruin. - Otherwise drift it. A wealth that is not positive gives the same shape and
true. - 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: Iftrue, give the path as well; elsenothingin its place.Tw: Element type of the record.
Returns
(w, U, ruined): The held weights, the path ornothing, and whether the member is ruined.
Related
PortfolioOptimisers.held_weights_result — Function
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
- With no drift, give
(nothing, nothing). The fold held its target weights, so there is nothing to record. - 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 recordsNaN. - Over a population, drift each member. A ruined member records
NaNand is named in the second value; a non-finite member recordsNaNand is not named. Raise when every member is ruined.
Arguments
wd: Weight drift that runs, fromheld_weights_drift, ornothing.w: Weights the drift starts from,w0of the record: the fold's target weights, or the previous weights a failed fold holds.X: Asset returns of the fold.store_weight_path: Iftrue, store the weight path on the record.obs: Observation labels the wealth message names, seeassert_positive_wealth.
Returns
(hw, ruined): The record, ornothing; and the indices of the ruined members, ornothing.
Related
PortfolioOptimisers.drift_observations — Function
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
- Give the timestamps when the fold carries them.
- Give the test indices when it does not.
Arguments
ts: Timestamps of the fold, ornothing.test_idx: Absolute panel rows of the fold.
Returns
- The labels
assert_positive_wealthreads.
Related
PortfolioOptimisers.investable_reduction — Method
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
- Return
nothing, the carrier and the weights unchanged, and the fee throughinvestable_fees_viewunder anothingmask, when the carrier is a matrix or a returns result. - Derive the Investable Mask once with
investable_mask. - Return the same when every asset is investable.
- Otherwise report the held non-investable assets through
strict_diagnostic. - Return the mask, a
port_opt_viewof the prior atfindall(imsk), the view of the weights at the mask, and the fee throughinvestable_fees_viewat the mask and the prior's unreducedpr.X.
Arguments
pr: Prior result,ReturnsResult, or asset returns matrix.w: Portfolio weights, a population of them, or a weight path (observations × assets).fees:Feesthe figure is charged against, ornothing.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
ArgumentErrornaming them is raised understrict.
Returns
(imsk, pr, w, fees): The Investable Mask and the three reduced to it, ornothing, the carrier and the weights unchanged, and the fee on the axes anothingmask leaves.
Related
PortfolioOptimisers.investable_returns_view — Function
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, ornothing.rd: The returns result to use.
Returns
rd::AbstractReturnsResult: The returns result reduced to the investable assets, or unchanged.
Related
PortfolioOptimisers.held_gap_pairs — Function
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
PortfolioOptimisers.filter_held_gaps — Function
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
- Return
Xitself when every entry ofXis finite. - Otherwise find the Held Gaps with
held_gap_pairsand report them throughstrict_diagnostic, naming the assets bynxwhen it is given. - Return a copy of
Xwith 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 ofX. When given, the message names the held assets by them; otherwise it reports their column indices.
Validation
- Every held pair of
Xis finite, else a warning naming the pairs is emitted, or anArgumentErrornaming them is raised understrict.
Returns
X::MatNum: The window with no non-finite entry.
Related
PortfolioOptimisers.held_gap_msg — Function
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)pairsheld_gap_pairsfound.nx: Optional asset names of the window's columns.
Returns
msg::String: The message.
Related
PortfolioOptimisers.expand_investable_columns — Function
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,trueat 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
PortfolioOptimisers.expand_held_weights — Function
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
- 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. - Return
nothingwhen the fold built no record. - Otherwise expand the asset returns, the stored weight path and the held weights, and rebuild the record around them.
Arguments
imsk: The Investable Mask, ornothing.hw: The fold's Held Weights record, ornothing.
Returns
hw::Option{<:HeldWeightsResult}: The record over the full asset universe.
Related
PortfolioOptimisers.expand_held_member — Function
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,trueat every asset whose prior moments were finite.x: Held weights, a weight path, a population of either, ornothing.
Returns
- The member over the full asset universe.
Related
PortfolioOptimisers.charge_fees — Function
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
- A
nothingfeesreturnsrunchanged. It charges no fee rather than a zero fee. - Read the pair
(amortised, one_time)ofcalc_fees, over the length ofr. This site knows the series it charges, so it hands that length in. - Subtract
amortisedfrom every observation. - Subtract
one_timefrom the first observation alone. Under anAmortisedFeesit is zero, because step 2 spread that whole cost intoamortised, andiszerogates 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:Feesstructure, ornothing.
Returns
val::VecNum: The net return series.
Related
PortfolioOptimisers.charge_asset_fees — Function
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
- A
nothingfeesreturnsRunchanged. It charges no fee rather than a zero fee. - Read the pair of pairs
((am_i, am_l), (ot_i, ot_l))ofcalc_asset_fees, over the row count ofR. The weights it is handed are the investable ones, because that is the axis the five per asset fields were sliced to; the carriers readwfor its element type alone. - On a
nothingimsk, charge the investable axis over the whole matrix, and refuse afeesthat carries a liquidation: no mask says where the exits are, so the charge has nowhere to land, and dropping it would understate the return. - On a
BitVectorimsk, charge the investable axis in theimskcolumns 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 anAmortisedFeesthe 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 asR.fees:Feesstructure, ornothing.imsk: The Investable Mask, ornothingwhen no asset left.
Validation
imskspans the columns ofR, else aDimensionMismatchnaming both widths.- Each charge spans the axis it is charged on, through
assert_fee_axis_width. - A
nothingimskmeets no liquidation carrier, else anArgumentErrornaming the mask.
Returns
val::MatNum: The net per asset return matrix, on the caller's universe.
Related
PortfolioOptimisers.charges_nothing — Function
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 ornothing.
Returns
val::Bool:truewhen the term charges nothing.
Related
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, ornothing.ot: One-off charge of the axis, one entry per column. Zero under anAmortisedFees, which spread it intoam.
Returns
A::AbstractMatrix: The same matrix, charged.
Related
PortfolioOptimisers.assert_fee_axis_width — Function
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, ornothing.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
amandotis empty or spansn, else aDimensionMismatchnaming both widths.
Related
PortfolioOptimisers.absolute_drawdown_arr — Function
absolute_drawdown_arr(X::ArrNum; cX::Bool = false, dims::Int = 1) -> ArrNumCompute 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
- Read
cX, which says whetherXis already cumulative. - When
cXisfalse, reachabsolute_cumulative_returnsalongdims, giving the cumulative seriescX. WhencXistrue, takeXas that series unchanged. - Take the running maximum of
cXalongdimswithinit = zero(eltype(X)), giving the running peak. Theinitstarts the peak at the initial capital of zero. - Subtract the running peak from
cX, giving the drawdown array.
Arguments
X::ArrNum: Returns array (or cumulative-returns array ifcX = true).cX::Bool = false: Iftrue, treatXas already cumulative returns.dims: Dimension along which to perform the computation.
Returns
dd::ArrNum: Drawdown array of the same shape asX. Every entry is zero or negative.
Related
ArrNumabsolute_drawdown_vecrelative_drawdown_arr: The compound sibling, seeded withinit = one(eltype(X)).drawdowns: The public entry point that reaches this function whencompoundisfalse.absolute_cumulative_returns: The helper that step 2 reaches.cumulative_returns
PortfolioOptimisers.relative_drawdown_arr — Function
relative_drawdown_arr(X::ArrNum; cX::Bool = false, dims::Int = 1) -> ArrNumCompute 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
- Read
cX, which says whetherXis already cumulative. - When
cXisfalse, reachrelative_cumulative_returnsalongdims, giving the cumulative seriescX. WhencXistrue, takeXas that series unchanged. - Take the running maximum of
cXalongdimswithinit = one(eltype(X)), giving the running peak. Theinitstarts the peak at the initial capital of one. - Divide
cXby the running peak and subtract one, giving the relative drawdown array.
Arguments
X::ArrNum: Returns array (or cumulative-returns array ifcX = true).cX::Bool = false: Iftrue, treatXas already cumulative compounded returns.dims: Dimension along which to perform the computation.
Returns
dd::ArrNum: Relative drawdown array of the same shape asX. Every entry is zero or negative.
Related
ArrNumrelative_drawdown_vecabsolute_drawdown_arr: The additive sibling, seeded withinit = zero(eltype(X)).drawdowns: The public entry point that reaches this function whencompoundistrue.relative_cumulative_returns: The helper that step 2 reaches.cumulative_returns
PortfolioOptimisers.relative_cumulative_returns — Function
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
- Add one to every entry of
X, giving the per period growth factors. - 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 asX.
Related
ArrNumabsolute_cumulative_returns: The additive sibling, whose series starts at zero.cumulative_returns: The public entry point that reaches this helper whencompoundistrue.relative_drawdown_arr
PortfolioOptimisers.absolute_cumulative_returns — Function
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
- Take the running sum of
Xalongdims, 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 asX.
Related
ArrNumrelative_cumulative_returns: The multiplicative sibling, whose series starts at one.cumulative_returns: The public entry point that reaches this helper whencompoundisfalse.absolute_drawdown_arr