Net returns and drawdowns

Net returns and drawdowns are two of the performance metrics of a portfolio. Here we define functions used to compute portfolio returns and related quantities.

PortfolioOptimisers.AbstractPreviousWeightsSourceType
AbstractPreviousWeightsSource <: AbstractAlgorithm

Abstract supertype of the Previous-Weights Source family.

A Previous-Weights Source names the weights fold_loop threads from a fold into the fold that follows it. nothing threads the target weights of the previous fold, which is the library's original behaviour, and DriftedWeights is the family's one leaf.

The two walk-forward schemes carry this family in their pws field, bound to Option{<:AbstractPreviousWeightsSource}. A scheme whose folds are not a timeline carries no such field: no fold of it has a fold behind it, so it has no previous weights of any kind to thread.

Interfaces

In order to implement a new previous-weights source which will work seamlessly with the library, subtype AbstractPreviousWeightsSource with a wd::AbstractWeightDrift field, the Weight Drift held_weights_drift runs when the scheme sets no drift of its own, and implement the following method:

  • previous_weights(pws::AbstractPreviousWeightsSource, prev::PredictionResult) -> VecNum_VecVecNum: Read the weights the previous fold threads into the fold that follows it. A fallback method reads the held weights of the previous fold, prev.hw.w, so it is only needed when the source threads something else.

Related

source
PortfolioOptimisers.SelfFinancingDriftType
struct SelfFinancingDrift <: AbstractWeightDrift

Grows each position at its own asset return and holds no trade in between, so the weights drift and the series is the wealth ratio of the drifted holdings.

The portfolio is self-financing over the window: no capital enters it and none leaves it, and the implicit cash position $1 - \sum_j w_j$ earns zero. The recursion therefore holds for a long-short book and for a partly invested one.

Examples

julia> SelfFinancingDrift()SelfFinancingDrift()

Related

source
PortfolioOptimisers.DriftedWeightsType
struct DriftedWeights{__T_wd} <: AbstractPreviousWeightsSource

Thread the weights a fold held after its last observation into the fold that follows it.

The fold loop threads the target weights of the previous fold by default. Those are the weights the optimiser chose, so a turnover, a tracking or a fee estimator then measures the change in the decision. This source threads the weights the portfolio held after the last observation of that fold instead, so the same estimators measure the trades a fund places.

wd names the Weight Drift the held weights are computed under, and it is read only when the scheme's own wd is nothing. A scheme that drifts its return series drifts its held weights the same way, because one drift runs per fold and HeldWeightsResult records the form that ran.

Fields

  • wd: Weight drift the held weights are computed under when the return series carries no drift of its own.

Constructors

DriftedWeights(; wd::AbstractWeightDrift = SelfFinancingDrift()) -> DriftedWeights

Keyword arguments correspond to the struct's fields.

Examples

julia> DriftedWeights()DriftedWeights  wd ┴ SelfFinancingDrift()

Related

source
PortfolioOptimisers.HeldWeightsResultType
struct HeldWeightsResult{__T_X, __T_U, __T_w0, __T_w, __T_wd} <: AbstractResult

Records what a fold actually held, so a reader can recover the weight path of that fold.

The record is present on a PredictionResult only when a drift ran, and a reader dispatches on its absence rather than testing for it. It stores the asset returns the fold was scored over, the weights the drift started from, the weights held after the last observation, and the Weight Drift that produced them. It stores the weight path itself only when the scheme's store_weight_path is true; otherwise weight_path rebuilds the path from the stored members on demand.

w0 is the first row of the path. On a solved fold it is the fold's target weights; on a failed fold under a drift it is the previous weights the fold was handed, because a fold that could not rebalance holds what it held. The record carries it so a rebuild never reads the fold's res.w, which is NaN on a failed fold.

wd records the form that ran, not the setting that asked for it. A rebuild that guesses the form is a second answer rather than a rebuild, so the form travels with the numbers it made.

Under a population result the weights are one vector per member, so w is a vector of vectors and U, when it is stored, is one matrix per member. A member whose drifted wealth is not positive carries NaN in both, and its entry in the fold's return code is an OptimisationFailure.

Fields

  • X: Asset returns of the fold, as the view the fold was scored over.
  • U: Weight path, observations × assets, or nothing when it was not stored and is rebuilt on demand.
  • w0: Weights the drift started from: the first row of the path.
  • w: Held weights after the last observation of the fold.
  • wd: Weight drift that produced the path and the held weights.

Constructors

HeldWeightsResult(;    X::MatNum,    U::Option{<:MatNum_VecMatNum},    w0::VecNum_VecVecNum,    w::VecNum_VecVecNum,    wd::AbstractWeightDrift) -> HeldWeightsResult

Keywords correspond to the struct's fields.

Validation

  • !isempty(X).
  • A stored U has the size of X, one matrix per member under a population.
  • w0 and w each have one entry per column of X, one vector per member under a population, and w0 has the shape of w.

Related

source
PortfolioOptimisers.calc_net_returnsMethod
calc_net_returns(w::VecNum, X::MatNum, args...)
calc_net_returns(w::VecNum, X::MatNum, fees::Fees)
calc_net_returns(w::VecVecNum, X::MatNum, args...)

Compute the net portfolio returns. If fees is not nothing, it deducts the computed fees from the gross returns.

The fee is two scalars on two clocks. calc_fees contracts the whole weight vector into a per period number and a one-time number: the first is subtracted from every row of $\mathbf{X}\boldsymbol{w}$, so a T-row matrix charges it T times, and the second lands on the clock fees.fa names, the first row by default. That is why the Fees rates must be stated per period of X, and the two fixed amounts for the whole holding period.

The per asset returns sum to this series. vec(sum(calc_net_asset_returns(w, X, fees); dims = 2)) reproduces calc_net_returns(w, X, fees), because calc_asset_fees splits over the assets what calc_fees contracts into a scalar. The two sides add in a different order, so the identity holds to rounding and not to ==.

This verb is the plain product, and a non-finite entry poisons its whole observation. 0 * NaN is NaN, so a zero weight does not save the row: one NaN in X[t, i] makes val[t] non-finite whatever w[i] holds. The verb takes no finiteness check, because it is the funnel of the library and a scan here is paid at each of its call sites on every evaluation. A gapped panel is scored through predict(res::NonFiniteAllocationOptimisationResult, rd::ReturnsResult), which reduces the window to the Investable Mask and filters the Held Gaps once with filter_held_gaps before it reaches this verb.

Mathematical definition

\[\begin{align} \boldsymbol{R}(\mathbf{X},\, \boldsymbol{w}) &= \mathbf{X} \boldsymbol{w} \ominus F_{\text{t}}(\boldsymbol{w}) \end{align}\]

Where:

  • $\boldsymbol{R}(\mathbf{X},\, \boldsymbol{w})$: T × 1 vector of portfolio net returns.
  • $\mathbf{X}$: T × N matrix of asset returns (observations × assets).
  • $\boldsymbol{w}$: N × 1 vector of portfolio weights.
  • $F_{\text{t}}(\boldsymbol{w})$: Total fees computed using calc_fees.
  • $\ominus$: Elementwise (Hadamard) subtraction.

Algorithm

  1. Contract X with w, giving X * w, the T × 1 gross portfolio return series.
  2. On the args... method, return that series unchanged. The method reads none of its trailing arguments, so a nothing fees reaches it and charges nothing rather than charging a zero fee.
  3. On the fees::Fees method, hand the series to charge_fees, which subtracts the per period charge from every entry, and the one-off charge on the clock fees.fa names.
  4. On the w::VecVecNum method, apply steps 1 to 3 to each weight vector wi of w, and collect one return series per weight vector.

Arguments

  • w: Portfolio weights, or a vector of portfolio weight vectors.
  • X: Asset return matrix (observations × assets).
  • fees: Fees structure.
  • args...: Additional arguments (ignored).

Returns

  • val::VecNum: Portfolio net returns, for a w::VecNum.
  • val::Vector{<:VecNum}: One net return series per weight vector, for a w::VecVecNum.

Examples

julia> calc_net_returns([0.5, 0.5], [0.01 0.02; 0.03 0.04])2-element Vector{Float64}: 0.015 0.035

Related

source
PortfolioOptimisers.calc_net_returnsMethod
calc_net_returns(w::MatNum, X::MatNum, args...)

Compute the net portfolio returns of a weight path.

w is a T × N weight path: row t holds the weights the portfolio carried through observation t. weight_path is the verb that makes one, under a Weight Drift or at constant weights, so this method reads a drifted window the way the VecNum methods above read a constant one.

The weight argument's type is the picker. A VecNum is one target weight vector, and it weighs every observation. A MatNum is a path, and it weighs each observation by the row that belongs to it. A scorer that forwards its weights to this verb therefore reads a path with no change of its own: performance_summary is bound ArrNum and admits both today.

This method states no arithmetic of its own. It sums the per asset split of the same path along the asset axis, so the two cannot drift apart, and the fee it charges is the one calc_net_asset_returns charges. At constant weights every row of the path is the same vector, so the answer is the one the VecNum methods give. The two sides add in a different order, so that identity holds to rounding and not to ==.

Mathematical definition

\[\begin{align} \boldsymbol{R}(\mathbf{X},\, \mathbf{U}) &= \left(\mathbf{X} \odot \mathbf{U} \ominus \boldsymbol{F}_{\text{t}}(\mathbf{U}_{1,\cdot})^{\intercal}\right) \boldsymbol{1} \end{align}\]

Where:

  • $\boldsymbol{R}(\mathbf{X},\, \mathbf{U})$: T × 1 vector of portfolio net returns.
  • $\mathbf{X}$: T × N matrix of asset returns (observations × assets).
  • $\mathbf{U}$: T × N weight path, whose row $t$ holds the weights carried through observation $t$.
  • $\mathbf{U}_{1,\cdot}$: First row of the path, which is the target weights.
  • $\boldsymbol{F}_{\text{t}}(\boldsymbol{w})$: N × 1 per asset vector of total portfolio fees computed using calc_fees.
  • $\boldsymbol{1}$: N × 1 vector of ones, which sums along the asset axis.
  • $\odot$: Elementwise (Hadamard) multiplication.
  • $\ominus$: Elementwise (Hadamard) subtraction.

Algorithm

  1. Split the path over the assets with calc_net_asset_returns, which scales each observation by the weights held through it and charges the fee from the path's first row.
  2. Sum that T × N matrix along its asset axis, giving the T × 1 portfolio net return series.

Validation

Arguments

  • w: Weight path (observations × assets).
  • X: Asset return matrix (observations × assets).
  • fees: Fees structure.
  • args...: Additional arguments (ignored).

Returns

  • val::VecNum: Portfolio net returns.

Examples

julia> calc_net_returns([0.5 0.5; 0.6 0.4], [0.01 0.02; 0.03 0.04])2-element Vector{Float64}: 0.015 0.034

Related

source
PortfolioOptimisers.calc_net_returnsFunction
calc_net_returns(w::VecVecNum, X::MatNum, fees, wd::AbstractWeightDrift, obs = nothing)
calc_net_returns(w::VecNum, X::MatNum, fees, wd::AbstractWeightDrift, obs = nothing)
calc_net_returns(w::VecNum, X::MatNum, fees, wd::Nothing, args...)
calc_net_returns(w::VecVecNum, X::MatNum, fees, wd::Nothing, args...)

Compute the net portfolio returns of a window, reading the weight drift wd.

A nothing wd scores every observation against the same weight vector and reproduces calc_net_returns(w::VecNum, X::MatNum, args...) exactly. An AbstractWeightDrift lets each position grow at its own return, and the series becomes the wealth ratio of the drifted holdings. The fee is charged as it is on the undrifted series, through charge_fees: calc_fees contracts the target weights into a per period scalar, subtracted from every observation, and a one-time scalar, which lands on the clock fees.fa names.

A vector of weight vectors is a population, and a ruined member does not stop the run. The member's series is filled with NaN, one warning names every member that fell, and the run raises only when no member survives. A single weight vector is a population of one, so it raises.

Mathematical definition

\[\begin{align} R_{t} &= \frac{V_{t}}{V_{t-1}} - 1 - F_{\text{t}}(\boldsymbol{w})\,, \qquad V_{0} = 1\\ V_{t} &= \sum\limits_{j=1}^{N} w_{j} \prod\limits_{s=1}^{t} \left(1 + X_{s j}\right) + \left(1 - \sum\limits_{j=1}^{N} w_{j}\right) \end{align}\]

Where:

  • $R_{t}$: Net return of observation $t$.
  • $V_{t}$: Wealth at the end of observation $t$, per unit of initial capital.
  • $X_{s j}$: Return of asset $j$ at observation $s$.
  • $F_{\text{t}}(\boldsymbol{w})$: Total fees computed using calc_fees.
  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $T$: Number of observations.
  • $N$: Number of assets.

Algorithm

  1. On a nothing wd, delegate to calc_net_returns(w::VecNum, X::MatNum, args...), which charges the fee and reads no drift.
  2. On an AbstractWeightDrift, give a NaN series when w is not finite: a failed solve has no weights to drift, and its series is NaN as the undrifted one is, with no fee charged.
  3. Otherwise compute the position values with drift_position_values and the wealth with drift_wealth.
  4. Check the wealth with assert_positive_wealth.
  5. Read the wealth as a return series with drift_returns, and charge the fee through charge_fees, on the target weights.
  6. On a population, run steps 2 to 5 for each member. A non-finite member's series is NaN and the member is not ruined. Fill a ruined member's series with NaN, warn once naming every member that fell, and raise when every member fell.

Arguments

  • w: Portfolio weights, or a vector of portfolio weight vectors.
  • X: Asset return matrix (observations × assets).
  • fees: Fees structure, or nothing.
  • wd: Weight drift algorithm, or nothing.
  • obs: Observation labels of the window, panel rows of the window, or nothing. Read only by the message of step 3.
  • args...: Additional arguments (ignored).

Validation

  • The wealth of every observation is positive, else a NonPositiveWealthError is raised. On a population, the raise happens only when no member survives. A non-finite w is not checked: it is a failed solve, and its series is NaN.

Returns

  • val::VecNum: Portfolio net returns, for a w::VecNum.
  • val::Vector{<:VecNum}: One net return series per weight vector, for a w::VecVecNum.

Examples

julia> calc_net_returns([0.5, 0.5], [0.1 -0.1; 0.2 0.0], nothing, SelfFinancingDrift())2-element Vector{Float64}: 0.0 0.1100000000000001

Related

source
PortfolioOptimisers.calc_net_asset_returnsFunction
calc_net_asset_returns(w::VecNum, X::MatNum, args...)
calc_net_asset_returns(w::VecNum, X::MatNum, fees::Fees, imsk::Option{<:BitVector} = nothing)

Compute the per asset net portfolio returns. If fees is not nothing, it deducts the computed fees from the gross returns.

The rows sum to the portfolio series. vec(sum(calc_net_asset_returns(w, X, fees); dims = 2)) reproduces calc_net_returns(w, X, fees), because calc_asset_fees splits over the assets what calc_fees contracts into a scalar. The two sides add in a different order, so the identity holds to rounding and not to ==: on $\boldsymbol{w} = [0.6,\, -0.4,\, 0,\, 0.25]$ with all four rate fields set, the largest difference measured 6.9e-18.

Each per asset fee is two vectors on two clocks, as it is for calc_net_returns. The N × 1 per period vector is subtracted from every row of $\mathbf{X} \odot \boldsymbol{w}^{\intercal}$, so a T-row matrix charges it T times, and the N × 1 one-time vector lands on the clock fees.fa names, the first row by default.

These are the constant-weight methods: the one vector w weighs every observation. The w::MatNum methods below read a weight path instead, one row of weights per observation, which is what a fold scored under a Weight Drift held.

This verb is the plain product, and a non-finite entry poisons its own cell. 0 * NaN is NaN, so a zero weight does not save the entry, and the row it sits in no longer sums to a finite number. The verb takes no finiteness check, for the reason calc_net_returns takes none. A gapped panel is scored through predict(res::NonFiniteAllocationOptimisationResult, rd::ReturnsResult), which filters the Held Gaps once with filter_held_gaps before it reaches this verb.

Mathematical definition

\[\begin{align} \mathbf{R}(\mathbf{X},\, \boldsymbol{w}) &= \mathbf{X} \odot \boldsymbol{w}^{\intercal} \ominus \boldsymbol{F}_{\text{t}}(\boldsymbol{w})^{\intercal} \end{align}\]

Where:

  • $\mathbf{R}(\mathbf{X},\, \boldsymbol{w})$: T × N matrix of per asset portfolio net returns.
  • $\mathbf{X}$: T × N matrix of asset returns (observations × assets).
  • $\boldsymbol{w}$: N × 1 vector of portfolio weights.
  • $\boldsymbol{F}_{\text{t}}(\boldsymbol{w})$: N × 1 per asset vector of total portfolio fees computed using calc_fees.
  • $\odot$: Elementwise (Hadamard) multiplication.
  • $\ominus$: Elementwise (Hadamard) subtraction.

Algorithm

  1. Scale each column of X by its weight, giving X ⊙ transpose(w), the T × N matrix of gross per asset contributions.
  2. On the args... method, return that matrix unchanged. The method reads none of its trailing arguments, so a nothing fees reaches it and charges nothing rather than charging a zero fee.
  3. On the fees::Fees method, hand the matrix to charge_asset_fees, which subtracts the per asset per period charge from every row, and the one-off charge on the clock fees.fa names. imsk says which columns the five per asset fields were priced on and which the two liquidation carriers were, so each charge lands in the columns it was priced on; a nothing imsk charges the investable axis alone and refuses a fee that carries a liquidation.

Arguments

  • w: Portfolio weights.
  • X: Asset return matrix (observations × assets).
  • fees: Fees structure.
  • imsk: The Investable Mask, or nothing when no asset left.
  • args...: Additional arguments (ignored).

Returns

  • ret::MatNum: Per asset portfolio net returns, on the caller's universe.

Examples

julia> calc_net_asset_returns([0.5, 0.5], [0.01 0.02; 0.03 0.04])2×2 Matrix{Float64}: 0.005  0.01 0.015  0.02

Related

source
calc_net_asset_returns(w::MatNum, X::MatNum, args...)
calc_net_asset_returns(w::MatNum, X::MatNum, fees::Fees, imsk::Option{<:BitVector} = nothing)

Compute the per asset net portfolio returns of a weight path.

w is a T × N weight path: row t holds the weights the portfolio carried through observation t. weight_path is the verb that makes one, under a Weight Drift or at constant weights, so this method reads a drifted window the way the VecNum methods above read a constant one.

The rows sum to the portfolio series of that same path. The wealth ratio of an observation is the weights held through it contracted with that observation's asset returns, so vec(sum(calc_net_asset_returns(U, X, fees); dims = 2)) reproduces the series calc_net_returns forms from the same fold under the same drift. The two sides add in a different order, so the identity holds to rounding and not to ==.

The fee is charged from the first row of the path. That row is the target weights, because nothing has drifted when the window opens, and it is the vector the portfolio series charges its own fee from. The same two N × 1 vectors are therefore subtracted here as there: the per period one from every row, and the one-time one on the clock fees.fa names.

Mathematical definition

\[\begin{align} \mathbf{R}(\mathbf{X},\, \mathbf{U}) &= \mathbf{X} \odot \mathbf{U} \ominus \boldsymbol{F}_{\text{t}}(\mathbf{U}_{1,\cdot})^{\intercal} \end{align}\]

Where:

  • $\mathbf{R}(\mathbf{X},\, \mathbf{U})$: T × N matrix of per asset portfolio net returns.
  • $\mathbf{X}$: T × N matrix of asset returns (observations × assets).
  • $\mathbf{U}$: T × N weight path, whose row $t$ holds the weights carried through observation $t$.
  • $\mathbf{U}_{1,\cdot}$: First row of the path, which is the target weights.
  • $\boldsymbol{F}_{\text{t}}(\boldsymbol{w})$: N × 1 per asset vector of total portfolio fees computed using calc_fees.
  • $\odot$: Elementwise (Hadamard) multiplication.
  • $\ominus$: Elementwise (Hadamard) subtraction.

Algorithm

  1. Scale each observation of X by the weights held through it, giving X ⊙ w, the T × N matrix of gross per asset contributions.
  2. On the args... method, return that matrix unchanged. The method reads none of its trailing arguments, so a nothing fees reaches it and charges nothing rather than charging a zero fee.
  3. On the fees::Fees method, hand the matrix and the path's first row to charge_asset_fees, which subtracts the per asset per period charge from every row, and the one-off charge on the clock fees.fa names. imsk says which columns each axis of the fee was priced on, so each charge lands in its own; a nothing imsk charges the investable axis alone and refuses a fee that carries a liquidation.

Validation

  • size(w) == size(X), else the broadcast raises a DimensionMismatch. A fold's path has the size of the fold's asset returns by construction, and assert_held_weights_shape checks a stored one.

Arguments

  • w: Weight path (observations × assets).
  • X: Asset return matrix (observations × assets).
  • fees: Fees structure.
  • imsk: The Investable Mask, or nothing when no asset left.
  • args...: Additional arguments (ignored).

Returns

  • ret::MatNum: Per asset portfolio net returns, on the caller's universe.

Examples

julia> calc_net_asset_returns([0.5 0.5; 0.6 0.4], [0.01 0.02; 0.03 0.04])2×2 Matrix{Float64}: 0.005  0.01 0.018  0.016

Related

source
calc_net_asset_returns(pred::PredictionResult{<:Any, <:Any, <:HeldWeightsResult}, fees = nothing)
calc_net_asset_returns(pred::PredictionResult{<:Any, <:Any, Nothing}, args...)

Split a fold's net return series over the assets that produced it.

The fold-taking method of calc_net_asset_returns, and the mirror of calc_net_returns(res::OptimisationResult, X, fees): it resolves the fold's asset returns, its weight path and its fee, so a caller holding a fold reaches the split in one call. The fee is settled against the fold's own length exactly as predict settled it, so the rows of the result sum to the series the fold stored, to rounding.

A fold that carries no Held Weights record raises. pred.rd.X is the portfolio series, not the asset returns, so a fold whose scheme ran neither switch keeps no matrix to split.

Arguments

  • pred: Single-fold prediction result.
  • fees: A caller's Fees on the caller's universe, which takes precedence over the result's own and is viewed at the result's Investable Mask through fold_fees.
  • args...: Additional arguments (ignored by the refusing method).

Validation

Returns

  • ret::MatNum: Per asset net returns of the fold, one row per observation.

Related

source
PortfolioOptimisers.calc_turnoverFunction
calc_turnover(w::AbstractMatrix{<:Number})
calc_turnover(w::AbstractVector{<:AbstractVector{<:Number}})

Compute the one-way turnover of a weight path.

The turnover of an observation is the sum of the absolute weight changes the rebalance into it costs. It is the value-level reading of the quantity Turnover bounds and set_turnover_fees! charges, and it is what plot_turnover draws.

The first observation has no predecessor, so its turnover is NaN rather than zero: a path that opens a book from cash trades its whole gross, and a path that inherits one trades nothing, and the series cannot tell the two apart. The answer therefore has one entry per observation of w, and the caller drops or keeps the leading gap.

Mathematical definition

\[\begin{align} \mathrm{TO}_{t} &= \sum_{i=1}^{N} \left\lvert w_{t,i} - w_{t-1,i} \right\rvert\,, \qquad t = 2,\, \dots,\, T\,. \end{align}\]

Where:

  • $\mathrm{TO}_{t}$: Turnover of observation $t$.
  • $w_{t,i}$: Weight of asset $i$ at observation $t$.
  • $T$: Number of observations.
  • $N$: Number of assets.

Algorithm

  1. Allocate tn, one entry per observation of w, and write NaN into every entry.
  2. For each observation t after the first, sum the absolute differences of the two consecutive weight vectors, giving tn[t].

Arguments

  • w: Weight path. A matrix carries one observation per row, and a vector of vectors carries one observation per element.

Returns

  • tn::VecNum: Turnover, one entry per observation of w, NaN at the first.

Examples

julia> calc_turnover([0.5 0.5; 0.25 0.75])2-element Vector{Float64}: NaN   0.5

Related

source
PortfolioOptimisers.cumulative_returnsFunction
cumulative_returns(X::ArrNum, compound::Bool = false; dims::Int = 1)

Compute simple or compounded cumulative returns along a specified dimension.

cumulative_returns computes the cumulative returns for an array of asset or portfolio returns. By default, it computes simple cumulative returns using cumsum. If compound is true, it computes compounded cumulative returns using cumprod(one(eltype(X)) .+ X).

The Precomputed-returns contract: the series the caller hands this verb must be finite. It takes no finiteness check, because every internal caller hands it the output of calc_net_returns and a scan on a long series would be paid by all of them. One non-finite entry poisons every entry after it, because both accumulations carry it forward. A caller who holds a gapped series drops the gaps first with x[isfinite.(x)], and a caller who holds a gapped panel scores it through predict(res::NonFiniteAllocationOptimisationResult, rd::ReturnsResult) instead.

Mathematical definition

Portfolio cumulative returns

\[\begin{align} \boldsymbol{CR}_{a}(\boldsymbol{X}) &= \left\{j \in [1,\,T]\, |\, \boldsymbol{CR}_{a,\, j}\right\} \\ CR_{a,\, j}(\boldsymbol{X}) &= \sum\limits_{i=1}^{j} X_{i} \\ \boldsymbol{CR}_{r}(\boldsymbol{X}) &= \left\{j \in [1,\,T]\, |\, \boldsymbol{CR}_{r,\, j}\right\} \\ CR_{r,\, j}(\boldsymbol{X}) &= \prod\limits_{i=1}^{j} (1 + X_{i}) \end{align}\]

Where:

  • $\boldsymbol{X}$: T × 1 vector of portfolio returns.
  • $CR_{a,\, j}(\boldsymbol{X})$: Simple cumulative portfolio returns at period j.
  • $CR_{r,\, j}(\boldsymbol{X})$: Compound cumulative portfolio returns at period j.
  • $\boldsymbol{CR}_{a}$: T × 1 vector of simple cumulative portfolio returns.
  • $\boldsymbol{CR}_{r}$: T × 1 vector of compound cumulative portfolio returns.

Per asset cumulative returns

The same definitions apply as above, but for each individual asset in the returns matrix $\mathbf{X}$ instead of the portfolio return $\mathbf{X} \boldsymbol{w}$.

Algorithm

  1. Read compound, which selects one of two helpers.
  2. When compound is false, reach absolute_cumulative_returns, giving the running sum of X along dims.
  3. When compound is true, reach relative_cumulative_returns, giving the running product of one(eltype(X)) .+ X along dims.

Arguments

  • X: Array of asset or portfolio returns (vector or matrix).
  • compound: If true, computes compounded cumulative returns; otherwise, computes simple cumulative returns.
  • dims: Dimension along which to perform the computation.

Returns

  • ret::ArrNum: Array of cumulative returns, same shape as X.

Examples

julia> cumulative_returns([0.01, 0.02, -0.01])3-element Vector{Float64}: 0.01 0.03 0.02julia> cumulative_returns([0.01, 0.02, -0.01], true)3-element Vector{Float64}: 1.01 1.0302 1.019898

Related

source
PortfolioOptimisers.drawdownsFunction
drawdowns(X::ArrNum, compound::Bool = false; cX::Bool = false, dims::Int = 1)

Compute simple or compounded drawdowns along a specified dimension.

The running peak starts at the initial capital, not at the first observation, so a series that is under water from the first period reports a negative drawdown there. drawdowns([-0.1, 0.05]) returns [-0.1, -0.05] and not [0.0, 0.0]. drawdowns dispatches to absolute_drawdown_arr or to relative_drawdown_arr, which hold the single definition of the peak: the init of their accumulate(max, ...) is the initial capital, zero(eltype(X)) for the additive path and one(eltype(X)) for the compound one.

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

The Precomputed-returns contract: the series the caller hands this verb must be finite. It takes no finiteness check, for the reason cumulative_returns takes none. One non-finite entry poisons the running peak and every drawdown after it. A caller who holds a gapped series drops the gaps first with x[isfinite.(x)], and a caller who holds a gapped panel scores it through predict(res::NonFiniteAllocationOptimisationResult, rd::ReturnsResult) instead.

Mathematical definition

Portfolio drawdowns

\[\begin{align} \boldsymbol{DD}_{a}(\boldsymbol{X}) &= \left\{j \in [1,\,T] \, |\, \mathrm{DD_{a}}(\boldsymbol{X},\, j)\right\}\\ DD_{a}(\boldsymbol{X},\, j) &= \sum\limits_{i=1}^{j} X_{i} - \underset{t \in [1,\, j]}{\max}\left( \sum\limits_{i=1}^{t} X_{i} \right)\\ \boldsymbol{DD}_{r}(\boldsymbol{X}) &= \left\{j \in [1,\,T] \, |\, \mathrm{DD_{r}}(\boldsymbol{X},\, j)\right\}\\ DD_{r}(\boldsymbol{X},\, j) &= \dfrac{\prod\limits_{i=1}^{j}\left(1 + X_{i}\right)}{\underset{t \in [1,\, j]}{\max}\left( \prod\limits_{i=1}^{t}\left(1 + X_{i}\right) \right)} - 1 \end{align}\]

Where:

  • $\boldsymbol{DD}_{a}(\boldsymbol{X})$: T × 1 vector of simple drawdowns.
  • $DD_{a}(\boldsymbol{X},\, j)$: Simple drawdown at period j.
  • $\boldsymbol{DD}_{r}(\boldsymbol{X})$: T × 1 vector of compound drawdowns.
  • $DD_{r}(\boldsymbol{X},\, j)$: Compound drawdown at period j.
  • $\boldsymbol{X}$: T × 1 vector of portfolio returns.

Per asset portfolio drawdowns

The same definitions apply as above, but for each individual asset in the returns matrix $\mathbf{X}$ instead of the portfolio return $\mathbf{X} \boldsymbol{w}$.

Algorithm

  1. Read compound, which selects one of two builders.
  2. When compound is false, reach absolute_drawdown_arr, forwarding cX and dims.
  3. When compound is true, reach relative_drawdown_arr, forwarding cX and dims.
  4. The builder that step 2 or step 3 reaches cumulates X along dims first, unless cX is true, in which case it takes X as the cumulative series it already is.
  5. That builder then subtracts the running peak, seeded at the initial capital.

Arguments

  • X: Array of asset or portfolio returns (vector or matrix).
  • compound: If true, computes compounded drawdowns; otherwise, computes simple drawdowns.
  • cX: If true, treats X as cumulative returns; otherwise, computes cumulative returns first.
  • dims: Dimension along which to perform the computation.

Returns

  • dd::ArrNum: Array of drawdowns, same shape as X. Every entry is zero or negative.

Examples

julia> drawdowns([0.01, 0.02, -0.01])3-element Vector{Float64}:  0.0  0.0 -0.009999999999999998julia> drawdowns([0.01, 0.02, -0.01], true)3-element Vector{Float64}:  0.0  0.0 -0.010000000000000009julia> drawdowns([-0.1, 0.05])2-element Vector{Float64}: -0.1 -0.05

Related

source