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.AbstractPreviousWeightsSource — Type
AbstractPreviousWeightsSource <: AbstractAlgorithmAbstract 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
PortfolioOptimisers.SelfFinancingDrift — Type
struct SelfFinancingDrift <: AbstractWeightDriftGrows 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
PortfolioOptimisers.DriftedWeights — Type
struct DriftedWeights{__T_wd} <: AbstractPreviousWeightsSourceThread 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()) -> DriftedWeightsKeyword arguments correspond to the struct's fields.
Examples
julia> DriftedWeights()DriftedWeights wd ┴ SelfFinancingDrift()Related
PortfolioOptimisers.HeldWeightsResult — Type
struct HeldWeightsResult{__T_X, __T_U, __T_w0, __T_w, __T_wd} <: AbstractResultRecords 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, ornothingwhen 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) -> HeldWeightsResultKeywords correspond to the struct's fields.
Validation
!isempty(X).- A stored
Uhas the size ofX, one matrix per member under a population. w0andweach have one entry per column ofX, one vector per member under a population, andw0has the shape ofw.
Related
PortfolioOptimisers.calc_net_returns — Method
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 × 1vector of portfolio net returns. - $\mathbf{X}$:
T × Nmatrix of asset returns (observations × assets). - $\boldsymbol{w}$:
N × 1vector of portfolio weights. - $F_{\text{t}}(\boldsymbol{w})$: Total fees computed using
calc_fees. - $\ominus$: Elementwise (Hadamard) subtraction.
Algorithm
- Contract
Xwithw, givingX * w, theT × 1gross portfolio return series. - On the
args...method, return that series unchanged. The method reads none of its trailing arguments, so anothingfeesreaches it and charges nothing rather than charging a zero fee. - On the
fees::Feesmethod, hand the series tocharge_fees, which subtracts the per period charge from every entry, and the one-off charge on the clockfees.fanames. - On the
w::VecVecNummethod, apply steps 1 to 3 to each weight vectorwiofw, 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:Feesstructure.args...: Additional arguments (ignored).
Returns
val::VecNum: Portfolio net returns, for aw::VecNum.val::Vector{<:VecNum}: One net return series per weight vector, for aw::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.035Related
PortfolioOptimisers.calc_net_returns — Method
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 × 1vector of portfolio net returns. - $\mathbf{X}$:
T × Nmatrix of asset returns (observations × assets). - $\mathbf{U}$:
T × Nweight 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 × 1per asset vector of total portfolio fees computed usingcalc_fees. - $\boldsymbol{1}$:
N × 1vector of ones, which sums along the asset axis. - $\odot$: Elementwise (Hadamard) multiplication.
- $\ominus$: Elementwise (Hadamard) subtraction.
Algorithm
- 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. - Sum that
T × Nmatrix along its asset axis, giving theT × 1portfolio net return series.
Validation
size(w) == size(X), else the broadcast insidecalc_net_asset_returnsraises aDimensionMismatch. A fold's path has the size of the fold's asset returns by construction, andassert_held_weights_shapechecks a stored one.
Arguments
w: Weight path (observations × assets).X: Asset return matrix (observations × assets).fees:Feesstructure.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.034Related
MatNumArrNumweight_path: Makes the path this method reads.SelfFinancingDriftcalc_net_asset_returns: The per asset split this method sums.performance_summary: Scores the series this method forms.expected_risk: Scores a path through this method on aNetReturnsInputmeasure.Fees
PortfolioOptimisers.calc_net_returns — Function
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
- On a
nothingwd, delegate tocalc_net_returns(w::VecNum, X::MatNum, args...), which charges the fee and reads no drift. - On an
AbstractWeightDrift, give aNaNseries whenwis not finite: a failed solve has no weights to drift, and its series isNaNas the undrifted one is, with no fee charged. - Otherwise compute the position values with
drift_position_valuesand the wealth withdrift_wealth. - Check the wealth with
assert_positive_wealth. - Read the wealth as a return series with
drift_returns, and charge the fee throughcharge_fees, on the target weights. - On a population, run steps 2 to 5 for each member. A non-finite member's series is
NaNand the member is not ruined. Fill a ruined member's series withNaN, 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:Feesstructure, ornothing.wd: Weight drift algorithm, ornothing.obs: Observation labels of the window, panel rows of the window, ornothing. Read only by the message of step 3.args...: Additional arguments (ignored).
Validation
- The wealth of every observation is positive, else a
NonPositiveWealthErroris raised. On a population, the raise happens only when no member survives. A non-finitewis not checked: it is a failed solve, and its series isNaN.
Returns
val::VecNum: Portfolio net returns, for aw::VecNum.val::Vector{<:VecNum}: One net return series per weight vector, for aw::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.1100000000000001Related
AbstractWeightDriftSelfFinancingDriftcalc_net_returns(w::VecNum, X::MatNum, args...): The undrifted series anothingwdreaches.drift_returnsassert_positive_wealthNonPositiveWealthErrorVecVecNumFees
PortfolioOptimisers.calc_net_asset_returns — Function
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 × Nmatrix of per asset portfolio net returns. - $\mathbf{X}$:
T × Nmatrix of asset returns (observations × assets). - $\boldsymbol{w}$:
N × 1vector of portfolio weights. - $\boldsymbol{F}_{\text{t}}(\boldsymbol{w})$:
N × 1per asset vector of total portfolio fees computed usingcalc_fees. - $\odot$: Elementwise (Hadamard) multiplication.
- $\ominus$: Elementwise (Hadamard) subtraction.
Algorithm
- Scale each column of
Xby its weight, givingX ⊙ transpose(w), theT × Nmatrix of gross per asset contributions. - On the
args...method, return that matrix unchanged. The method reads none of its trailing arguments, so anothingfeesreaches it and charges nothing rather than charging a zero fee. - On the
fees::Feesmethod, hand the matrix tocharge_asset_fees, which subtracts the per asset per period charge from every row, and the one-off charge on the clockfees.fanames.imsksays 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; anothingimskcharges the investable axis alone and refuses a fee that carries a liquidation.
Arguments
w: Portfolio weights.X: Asset return matrix (observations × assets).fees:Feesstructure.imsk: The Investable Mask, ornothingwhen 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.02Related
VecNumMatNumcalc_net_returns: The portfolio series this matrix sums to alongdims = 2.calc_asset_fees: Computes the per asset vector that step 3 subtracts.calc_feesFees
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 × Nmatrix of per asset portfolio net returns. - $\mathbf{X}$:
T × Nmatrix of asset returns (observations × assets). - $\mathbf{U}$:
T × Nweight 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 × 1per asset vector of total portfolio fees computed usingcalc_fees. - $\odot$: Elementwise (Hadamard) multiplication.
- $\ominus$: Elementwise (Hadamard) subtraction.
Algorithm
- Scale each observation of
Xby the weights held through it, givingX ⊙ w, theT × Nmatrix of gross per asset contributions. - On the
args...method, return that matrix unchanged. The method reads none of its trailing arguments, so anothingfeesreaches it and charges nothing rather than charging a zero fee. - On the
fees::Feesmethod, hand the matrix and the path's first row tocharge_asset_fees, which subtracts the per asset per period charge from every row, and the one-off charge on the clockfees.fanames.imsksays which columns each axis of the fee was priced on, so each charge lands in its own; anothingimskcharges the investable axis alone and refuses a fee that carries a liquidation.
Validation
size(w) == size(X), else the broadcast raises aDimensionMismatch. A fold's path has the size of the fold's asset returns by construction, andassert_held_weights_shapechecks a stored one.
Arguments
w: Weight path (observations × assets).X: Asset return matrix (observations × assets).fees:Feesstructure.imsk: The Investable Mask, ornothingwhen 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.016Related
MatNumweight_path: Makes the path this method reads.SelfFinancingDriftcalc_net_returns: The portfolio series this matrix sums to alongdims = 2.calc_asset_fees: Computes the per asset vector that step 3 subtracts.Fees
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'sFeeson the caller's universe, which takes precedence over the result's own and is viewed at the result's Investable Mask throughfold_fees.args...: Additional arguments (ignored by the refusing method).
Validation
- The fold carries a
HeldWeightsResult, else anArgumentErrornaming the two switches.
Returns
ret::MatNum: Per asset net returns of the fold, one row per observation.
Related
PortfolioOptimisers.calc_turnover — Function
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
- Allocate
tn, one entry per observation ofw, and writeNaNinto every entry. - For each observation
tafter the first, sum the absolute differences of the two consecutive weight vectors, givingtn[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 ofw,NaNat the first.
Examples
julia> calc_turnover([0.5 0.5; 0.25 0.75])2-element Vector{Float64}: NaN 0.5Related
PortfolioOptimisers.cumulative_returns — Function
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 × 1vector 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 × 1vector of simple cumulative portfolio returns. - $\boldsymbol{CR}_{r}$:
T × 1vector 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
- Read
compound, which selects one of two helpers. - When
compoundisfalse, reachabsolute_cumulative_returns, giving the running sum ofXalongdims. - When
compoundistrue, reachrelative_cumulative_returns, giving the running product ofone(eltype(X)) .+ Xalongdims.
Arguments
X: Array of asset or portfolio returns (vector or matrix).compound: Iftrue, 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 asX.
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.019898Related
ArrNumabsolute_cumulative_returns: The helper that step 2 reaches.relative_cumulative_returns: The helper that step 3 reaches.drawdowns
PortfolioOptimisers.drawdowns — Function
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 × 1vector of simple drawdowns. - $DD_{a}(\boldsymbol{X},\, j)$: Simple drawdown at period
j. - $\boldsymbol{DD}_{r}(\boldsymbol{X})$:
T × 1vector of compound drawdowns. - $DD_{r}(\boldsymbol{X},\, j)$: Compound drawdown at period
j. - $\boldsymbol{X}$:
T × 1vector 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
- Read
compound, which selects one of two builders. - When
compoundisfalse, reachabsolute_drawdown_arr, forwardingcXanddims. - When
compoundistrue, reachrelative_drawdown_arr, forwardingcXanddims. - The builder that step 2 or step 3 reaches cumulates
Xalongdimsfirst, unlesscXistrue, in which case it takesXas the cumulative series it already is. - That builder then subtracts the running peak, seeded at the initial capital.
Arguments
X: Array of asset or portfolio returns (vector or matrix).compound: Iftrue, computes compounded drawdowns; otherwise, computes simple drawdowns.cX: Iftrue, treatsXas cumulative returns; otherwise, computes cumulative returns first.dims: Dimension along which to perform the computation.
Returns
dd::ArrNum: Array of drawdowns, same shape asX. 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.05Related
ArrNumcumulative_returnsabsolute_drawdown_arr: The builder that step 2 reaches, and one of the two homes of the running-peak convention.relative_drawdown_arr: The builder that step 3 reaches, and the other home of the running-peak convention.