Mahalanobis regime bias: private API

Functions

PortfolioOptimisers.lagrange_stencil — Function
lagrange_stencil(
    x0::Number,
    h::Number,
    N::Integer,
    x::Number
) -> Tuple{Any, NTuple{8, Any}}

Computes the weights of the 8-point Lagrange interpolant on a uniform grid at one point.

The stencil is the eight grid points nearest to x, moved inside the grid at its two ends.

Arguments

  • x0::Number: First point of the grid.
  • h::Number: Step of the grid.
  • N::Integer: Count of points in the grid, at least eight.
  • x::Number: Point to interpolate at.

Returns

  • (i0, wts)::Tuple{Int, NTuple{8, <:Number}}: Index of the first point of the stencil, and the weight of each of its eight points.

Related

source
PortfolioOptimisers.chebyshev_stencil — Function
chebyshev_stencil(
    sc::Number,
    s::Number
) -> Tuple{NTuple{8, Any}, NTuple{8, Any}}

Computes the weights of the degree-7 Chebyshev interpolant on $[0, s_{c}]$, and of its integral from zero, at one point.

The eight nodes are $s_{l} = s_{c} (1 + \cos\theta_{l}) / 2$, $\theta_{l} = \pi (l - 1/2) / 8$.

Arguments

  • sc::Number: Right end $s_{c}$ of the interval.
  • s::Number: Point in $[0, s_{c}]$.

Returns

  • (val, int)::Tuple{NTuple{8, <:Number}, NTuple{8, <:Number}}: Weight of each node in the value at s, and in the integral from zero to s.

Related

source
PortfolioOptimisers.stencil_table — Function
stencil_table(f, pts::AbstractArray) -> Any

Evaluates a stencil function at every point of an array, and stores the results in an array of the element type of the first result.

A loop fills the table, because a comprehension over points of an abstract number type leaves its element type to collect, and the analysis of the package then meets a generic method that does not apply.

Arguments

  • f: Function from a point to its stencil.
  • pts::AbstractArray: Points, at least one.

Returns

  • table::AbstractArray: f at each point, with the shape of pts.

Related

source
PortfolioOptimisers.mahalanobis_value_stencil — Function
mahalanobis_value_stencil(
    s::Number,
    geo::NamedTuple
) -> Tuple{Any, NTuple{8, Any}}

Returns the stencil of the removed part $q$ at one point, as it acts on the Chebyshev values of $q$ followed by $q$ on the grid.

The Chebyshev interpolant serves $[0, s_{c}]$, the grid Lagrange interpolant serves the grid, and past the grid $q(s) = s_{\mathrm{end}}\, q(s_{\mathrm{end}}) / s$, because every eigenvalue of the removed part then has $s \mu \gg 1$.

Arguments

Returns

  • st::Tuple{Int, NTuple{8, <:Number}}: First index and weights, for stencil_dot.

Related

source
PortfolioOptimisers.mahalanobis_integral_stencil — Function
mahalanobis_integral_stencil(
    s::Number,
    geo::NamedTuple
) -> Tuple{Any, NTuple{8, Any}}

Returns the stencil of the integral $Q(s) = \int_{0}^{s} q$ at one point, as it acts on the vector that mahalanobis_q_integral! fills.

The Chebyshev integral serves $[0, s_{c}]$, the grid Lagrange interpolant of $Q$ serves the grid, and past the grid $Q(s) = Q(s_{\mathrm{end}}) + s_{\mathrm{end}}\, q(s_{\mathrm{end}}) \ln(s / s_{\mathrm{end}})$.

Arguments

Returns

  • st::Tuple{Int, NTuple{8, <:Number}}: First index and weights, for stencil_dot.

Related

source
PortfolioOptimisers.mahalanobis_bias_grid — Function
mahalanobis_bias_grid(
    decay::Number
) -> NamedTuple{(:h, :sc, :sg, :NG, :nodes, :su, :xf, :pairs, :ho, :geo, :cut, :junction, :Qjunction, :Qcheb, :fnodes, :so, :Qout, :fout), <:Tuple{Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:sc, :xlo, :xhi, :h, :NG, :send), <:NTuple{6, Any}}, Nothing, Any, Any, Vector{<:NTuple{8, Any}}, Vararg{Any, 4}}}
mahalanobis_bias_grid(
    decay::Number,
    hf::Number
) -> NamedTuple{(:h, :sc, :sg, :NG, :nodes, :su, :xf, :pairs, :ho, :geo, :cut, :junction, :Qjunction, :Qcheb, :fnodes, :so, :Qout, :fout), <:Tuple{Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:sc, :xlo, :xhi, :h, :NG, :send), <:NTuple{6, Any}}, Nothing, Any, Any, Vector{<:NTuple{8, Any}}, Vararg{Any, 4}}}
mahalanobis_bias_grid(
    decay::Number,
    hf::Number,
    xflo::Number
) -> NamedTuple{(:h, :sc, :sg, :NG, :nodes, :su, :xf, :pairs, :ho, :geo, :cut, :junction, :Qjunction, :Qcheb, :fnodes, :so, :Qout, :fout), <:Tuple{Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:sc, :xlo, :xhi, :h, :NG, :send), <:NTuple{6, Any}}, Nothing, Any, Any, Vector{<:NTuple{8, Any}}, Vararg{Any, 4}}}
mahalanobis_bias_grid(
    decay::Number,
    hf::Number,
    xflo::Number,
    xfhi::Number
) -> NamedTuple{(:h, :sc, :sg, :NG, :nodes, :su, :xf, :pairs, :ho, :geo, :cut, :junction, :Qjunction, :Qcheb, :fnodes, :so, :Qout, :fout), <:Tuple{Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:sc, :xlo, :xhi, :h, :NG, :send), <:NTuple{6, Any}}, Nothing, Any, Any, Vector{<:NTuple{8, Any}}, Vararg{Any, 4}}}

Makes the fixed grids and interpolation weights of the level recursion of mahalanobis_level_bias, in the number type of decay.

The removed part $q$ of the recursion is held on $[0, s_{c}]$ by eight Chebyshev values and on $\ln s \in [-1, 30]$ by a grid of step $1/4$. The two parts overlap on the first four steps of the grid, and the grid values there are always the Chebyshev values, so the function has one value at every point. A Chebyshev interpolant of more nodes, or a grid that starts lower, amplifies round-off from one level to the next: the recursion multiplies an error of frequency $\omega$ by about $1 - \ln(\omega) / 2$.

Arguments

  • decay::Number: Decay of the weights. Its number type is the type of every weight.
  • hf::Number: Step of the lattice xf of $\ln \sigma$, $1/8$ by default.
  • xflo::Number: First point of xf, $-61 + \ln(1 - \lambda)$ by default.
  • xfhi::Number: Last point of xf, 42 by default. Past it a stencil extrapolates, which a step of $1/8$ bears and a finer step does not.

Returns

  • grid::NamedTuple: The step h, the grid sg, the node points nodes, the steps su of the integral, the pair stencils, the output grid so of regime_bias_table, the lattice xf on which g and $\rho_{0}$ are tabulated, the bounds geo of the grid, and cut = nothing, which mahalanobis_level_bias replaces where the transform is cut.

Related

source
PortfolioOptimisers.stencil_dot — Function
stencil_dot(
    st::Tuple{Integer, NTuple{8, var"#s2310"} where var"#s2310"<:Number},
    v::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}
) -> Any
stencil_dot(
    st::Tuple{Integer, NTuple{8, var"#s2307"} where var"#s2307"<:Number},
    v::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    m::Integer
) -> Any

Evaluates an 8-point stencil on a vector.

Arguments

  • st::Tuple{Integer, NTuple{8, <:Number}}: First index and weights of the stencil.
  • v::VecNum: Vector of values.
  • m::Integer: Shift of the first index.

Returns

  • val::Number: $\sum_{a} w_{a} v_{i_{0} + m + a - 1}$.

Related

source
PortfolioOptimisers.mahalanobis_lattice — Function
mahalanobis_lattice(
    decay::Number,
    Ks::AbstractVector{<:Integer},
    xf::AbstractRange,
    _::Nothing
) -> Tuple{Matrix, Matrix, Any}

Tabulates the log-determinant $g$ of the Laplace transform of one direction and its derivative $\rho_{0}$ on the lattice of mahalanobis_bias_grid, at each count of Ks, before the normalisation of the weights.

Without a HAC adjustment the weights are $\lambda^{j}$, so $g(s) = \sum_{j} \ln(1 + s \lambda^{j})$ and $\rho_{0}(s) = \sum_{j} \lambda^{j} / (1 + s \lambda^{j})$, and the table adds one observation at a time. The weights are positive, so no count has a cut.

With one, hac_log_det_path! gives $g(s) = \ln \det(I + s B)$ for the banded matrix $B = A / c$ at every count, one lattice point at a time, and hac_log_det_peak finds the cut of each count. A point past the first zero of the determinant holds $g = \infty$.

Arguments

  • decay::Number: Decay of the weights.
  • Ks::AbstractVector{<:Integer}: Counts of observations.
  • xf::AbstractRange: Lattice of $\ln s$.
  • hac_lags::Option{<:Union{<:Integer, <:VecNum}}: Count of HAC lags, the weight of each lag, or nothing.

Returns

  • (G, Rh, peaks)::Tuple: $g$ and $\rho_{0}$ with one column for each count, and for each count the peak of hac_log_det_peak, or nothing where the transform takes no cut.

Related

source
PortfolioOptimisers.hac_log_det_path! — Function
hac_log_det_path!(
    g::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    r::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    s::Number,
    decay::Number,
    hac_lags::Union{Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}
) -> Tuple{AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}

Computes $\ln \det(I + s B)$ and its derivative in $s$ at one point, for each count of observations up to the length of g, where $B$ is the banded matrix $A / c$ of regime_bias_table.

The rows of the banded LDLᵀ factorisation of hac_ldl_taylor_row! carry each pivot with its derivative, so $\ln \det$ is the sum of the logs of the pivots, and its derivative the sum of $d_{m}' / d_{m}$. $B$ at one count is the leading block of $B$ at the next, so a pivot that is not positive marks that count and every larger one. At zero lags the buffers keep one row, which no lag reads, so the path is that of the plain weights.

Arguments

  • g::VecNum: Output: $\ln \det(I + s B)$ at each count, $\infty$ past the first pivot that is not positive (mutated).
  • r::VecNum: Output: the derivative at each count, zero where g is $\infty$ (mutated).
  • s::Number: Point of the lattice.
  • decay::Number: Decay of the weights.
  • hac_lags::Union{<:Integer, <:VecNum}: Count of HAC lags, or the weight of each lag.

Returns

  • (g, r)::Tuple{<:VecNum, <:VecNum}: The filled vectors.

Related

source
PortfolioOptimisers.hac_log_det_peak — Function
hac_log_det_peak(
    gh::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    rh::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    xf::AbstractRange,
    decay::Number,
    K::Integer,
    hac_lags::Union{Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}
) -> Union{Nothing, Tuple{Any, Any}}

Finds the cut of the HAC transform of one count, the point $\sigma^{*}$ where $\ln \det(I + \sigma B)$ has its maximum, and the value there.

The derivative $\rho_{0}$ falls in $\sigma$, and it falls to $-\infty$ at the first zero of the determinant where $B$ has a negative eigenvalue. The lattice brackets its zero, and a bisection on the exact derivative of hac_log_det_slopes finds it. A shift of the cut by one sixteenth in $\ln \sigma$ moves the factor of the first moment by 0.3 % at two assets, a half-life of 5 and five lags, so the step of the lattice is too coarse. The value at the cut is the lattice value below it plus the integral of the exact derivative, by the 4-point Gauss–Legendre rule.

Arguments

  • gh::VecNum: $\ln \det(I + \sigma B)$ on the lattice, $\infty$ past the zero of the determinant.
  • rh::VecNum: Its derivative on the lattice.
  • xf::AbstractRange: Lattice of $\ln \sigma$.
  • decay::Number: Decay of the weights.
  • K::Integer: Count of observations.
  • hac_lags::Union{<:Integer, <:VecNum}: Count of HAC lags, or the weight of each lag.
  • slope: In place of decay, K and hac_lags, a function of $\sigma$ that gives the exact derivative, or a negative number past the first zero of the determinant. The first method reads it from hac_log_det_slopes on the banded matrix.

Returns

  • peak::Option{<:Tuple{<:Number, <:Number}}: $(\sigma^{*}, \ln \det(I + \sigma^{*} B))$, or nothing where the derivative is positive on the whole lattice, because $B$ is positive definite.

Related

source
PortfolioOptimisers.hac_log_det_slopes — Function
hac_log_det_slopes(
    decay::Number,
    K::Integer,
    t::Number,
    hac_lags::Union{Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}
) -> Union{Nothing, Tuple{Any, Any}}

Computes the two sums of the HAC fixed point of mahalanobis_bias from the banded LDLᵀ factorisation of $I + t A$, with the first and second derivatives in $t$ carried through it.

$\ell(t) = \sum_{m} \ln d_{m}(t)$ over the pivots, so $\ell' = \sum_{m} d_{m}' / d_{m}$ and $\ell'' = \sum_{m} (d_{m}'' / d_{m} - (d_{m}' / d_{m})^{2})$. A row whose diagonal $t\, c\, \lambda^{m}$ falls below the machine epsilon of decay times $1 - \lambda$ enters both sums as its weight, so the loop stops there and adds the remaining mass, as mahalanobis_bias_sums does.

Arguments

  • decay::Number: Decay of the weights.
  • K::Integer: Count of observations in the estimate.
  • t::Number: Argument, $(n + 1) b$.
  • hac_lags::Union{<:Integer, <:VecNum}: Count of HAC lags, or the weight of each lag.

Returns

  • slopes::Option{<:Tuple{<:Number, <:Number}}: $(\ell'(t), \ell'(t) + t \ell''(t))$, that is $\sum_{k} \mu_{k} / (1 + t \mu_{k})$ and $\sum_{k} \mu_{k} / (1 + t \mu_{k})^{2}$, or nothing where $I + t A$ is not positive definite.

Related

source
PortfolioOptimisers.hac_ldl_taylor_row! — Function
hac_ldl_taylor_row!(
    buf::NamedTuple,
    ta::Number,
    a::Number,
    decay::Number,
    p::Integer
) -> Any

Adds one row to the banded LDLᵀ factorisation of $I + t A$, with each entry carried as its value and its first two derivatives in $t$.

The entries of $I + t A$ are linear in $t$, so the diagonal of the row is $(1 + t a, a, 0)$ and its entry at lag $i$ is $(t a_{i}, a_{i}, 0)$, with $a_{i} = k_{i}\, a / \lambda^{i}$. The recursion is that of hac_ldl_row!, in taylor_mul and taylor_div.

Arguments

  • buf::NamedTuple: Buffers of the factorisation (mutated): l[r, i], the entry of row $m - r$ at column $m - r - i$; d[r], the pivot of row $m - r$; row, scratch; and k, the weight $k_{i}$ of each lag, from hac_lag_weights.
  • ta::Number: $t\, c\, \lambda^{m}$, the diagonal of $t A$ in the row.
  • a::Number: $c\, \lambda^{m}$, the diagonal of $A$ in the row.
  • decay::Number: Decay of the weights.
  • p::Integer: Count of lags that reach an observation of the estimate, $\min(L, m)$.

Returns

  • pivot::NTuple{3, <:Number}: The new pivot and its two derivatives.

Related

source
PortfolioOptimisers.gauss_legendre_rule — Function
gauss_legendre_rule(o::Number) -> NTuple{4, Tuple{Any, Any}}

Returns the nodes and weights of the 4-point Gauss–Legendre rule on $[-1, 1]$, in the number type of o.

Arguments

  • o::Number: One, in the number type of the rule.

Returns

  • rule::NTuple{4, <:Tuple{<:Number, <:Number}}: The pair of node and weight of each point.

Related

source
PortfolioOptimisers.mahalanobis_cut! — Function
mahalanobis_cut!(
    _::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    _::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    _::AbstractRange,
    _::Number,
    _::Nothing
)

Continues the lattice of one count past the peak of its saturated count, and returns the cut and the lattice that the last transform reads.

A Laplace transform of a positive variable is log-convex, so $\rho_{0}$ falls and stays positive. With negative eigenvalues $\rho_{0}$ falls to zero at the cut $\sigma^{*}$ and to $-\infty$ at the first zero of the determinant, so no continuation from $\sigma^{*}$ is the transform of a law, and the level recursion is not stable on one. The saturated count $\sigma \rho_{0}(\sigma)$ counts the weights with $\sigma \mu \gg 1$: it rises to $K$ for a positive spectrum, and the negative eigenvalues pull it down before $\sigma^{*}$. Past its maximum $N_{m}$ at $\sigma_{m}$ the lattice takes $\rho_{0} = N_{m} / \sigma$, the transform of $N_{m}$ saturated weights. The value and the slope of the count agree at $\sigma_{m}$, so the join is smooth, $\rho_{0}$ falls and stays positive, and the recursion runs on the whole grid, as it does on the plain weights. The last transform reads the true $g$ up to $\sigma^{*}$, so the factor stays exact at one asset. A hard cut, where the transform is zero past $\sigma^{*}$, puts a pole $k / (s^{*} - s)$ into the level $k$, and a continuation from $\sigma^{*}$ makes $\rho_{0}$ rise: the grid holds neither, and both give factors that are not finite near the gate.

Mathematical definition

\[\begin{align} N_{m} &= \max_{\sigma \leq \sigma^{*}} \sigma \rho_{0}(\sigma)\,, \qquad g(\sigma) = g(\sigma_{m}) + N_{m} \ln\frac{\sigma}{\sigma_{m}}\,, \qquad \rho_{0}(\sigma) = \frac{N_{m}}{\sigma}\,, \qquad \sigma > \sigma_{m}\,. \end{align}\]

Where:

  • $\sigma$: Point of the lattice, before the normalisation.
  • $\sigma_{m}$: The point of the lattice where the saturated count is largest.
  • $\sigma^{*}$: The peak of hac_log_det_peak, and $s^{*} = \sigma^{*} / \tilde{c}$.

Arguments

  • gh::VecNum: $g$ on the lattice (mutated past $\sigma_{m}$).
  • rh::VecNum: $\rho_{0}$ on the lattice (mutated past $\sigma_{m}$).
  • xf::AbstractRange: Lattice of $\ln \sigma$.
  • ct::Number: The normalisation $\tilde{c}$ of the lattice.
  • peak::Option{<:Tuple{<:Number, <:Number}}: The peak of hac_log_det_peak, or nothing.

Returns

  • cut::Option{<:NamedTuple}: nothing where peak is nothing. Else the cut $s^{*}$ as sstar, and as gt the true $g$ up to $\sigma^{*}$, with $g(\sigma^{*})$ past it. A point that the lattice marks dead takes $g(\sigma^{*})$ too. The lattice can mark a point dead by round-off where the exact slope of hac_log_det_peak is still positive, and the peak then falls on that point.

Related

source
PortfolioOptimisers.mahalanobis_pair_differences! — Function
mahalanobis_pair_differences!(
    dg::AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    dr::AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    grid::NamedTuple,
    gh::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    rh::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    shift::Tuple
) -> Tuple{AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}, AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}

Fills the differences of $g$ and of $\rho_{0}$ between each node $s$ and each point $s + u$ of the level recursion, for one count of observations.

Arguments

  • dg::MatNum: Output: $g(s + u) - g(s)$ for each node and step (mutated).
  • dr::MatNum: Output: $\rho_{0}(s + u) - \rho_{0}(s)$ for each node and step (mutated).
  • grid::NamedTuple: Grids of mahalanobis_bias_grid.
  • gh::VecNum: $g$ on the lattice.
  • rh::VecNum: $\rho_{0}$ on the lattice.
  • shift::Tuple{<:Integer, <:Number}: The shift $m$ of the lattice and the normalisation $\tilde{c}$.

Returns

  • (dg, dr)::Tuple{<:MatNum, <:MatNum}: The filled matrices.

Related

source
PortfolioOptimisers.mahalanobis_transform — Function
mahalanobis_transform(
    grid::NamedTuple,
    gh::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    e::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    m::Integer
) -> Tuple{Any, Any}

Evaluates the Laplace transform $e^{-D_{n-1}(s) / 2}$ of the last level on the output grid of mahalanobis_bias_grid.

Without a cut the grid is the output grid. With one, the grid moves by less than half a step so that one point falls on $s^{*}$. That point takes the weight one half of a closed trapezoid, and every point past it takes zero, so the cut adds no error of the first order in the step. Below the cut the transform reads the true $g$ of the cut, not the continuation that the recursion read.

Arguments

  • grid::NamedTuple: Grids of mahalanobis_bias_grid, with the cut of the count.
  • gh::VecNum: $g$ on the lattice, which the transform reads without a cut.
  • e::VecNum: Integral of the removed part, from mahalanobis_q_integral!.
  • m::Integer: Shift of the lattice.

Returns

  • (so, L)::Tuple{<:VecNum, <:VecNum}: The grid and the transform on it.

Related

source
PortfolioOptimisers.mahalanobis_level_step! — Function
mahalanobis_level_step!(
    v::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    e::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    bufs::Tuple,
    dg::AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    dr::AbstractMatrix{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    grid::NamedTuple
) -> AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}

Runs one level of the recursion of mahalanobis_level_bias in place.

Algorithm

  1. Fill the integral $Q$ of the removed part with mahalanobis_q_integral!.
  2. Read $q$ and $Q$ at each node: the Chebyshev nodes, then the grid past the junction.
  3. At each node $s$, sum the trapezoid rule over the steps $u$ of $e^{-(D(s + u) - D(s)) / 2} (\rho(s + u) - \rho(s))$, with $D = g - Q$ and $\rho = \rho_{0} - q$, and take the new $q$ as $q - h \sum / 2$.
  4. Write the new values into v, and set the grid values at the junction to the Chebyshev values.

Arguments

  • v::VecNum: The Chebyshev values of $q$, then $q$ on the grid (mutated).
  • e::VecNum: Buffer of mahalanobis_q_integral! (mutated).
  • bufs::Tuple{<:VecNum, <:VecNum, <:VecNum}: Buffers for $q$ and $Q$ at the nodes and for the new $q$ (mutated).
  • dg::MatNum: $g(s + u) - g(s)$ for each node and step.
  • dr::MatNum: $\rho_{0}(s + u) - \rho_{0}(s)$ for each node and step.
  • grid::NamedTuple: Grids of mahalanobis_bias_grid.

Returns

  • v::VecNum: The values of the next level.

Related

source
PortfolioOptimisers.mahalanobis_q_integral! — Function
mahalanobis_q_integral!(
    e::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    v::AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}},
    grid::NamedTuple
) -> AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}

Fills the vector that the integral stencils of mahalanobis_bias_grid read, from the values of the removed part $q$.

The integral $Q(s) = \int_{0}^{s} q$ is the Chebyshev integral on the first five grid points, and then a 4-point Gauss–Legendre rule on each step of the grid, with $q$ interpolated on the grid.

Arguments

  • e::VecNum: Output: the Chebyshev values of $q$, then $Q$ on the grid, then $s\, q(s)$ at the end of the grid (mutated).
  • v::VecNum: The Chebyshev values of $q$, then $q$ on the grid.
  • grid::NamedTuple: Grids of mahalanobis_bias_grid.

Returns

  • e::VecNum: The filled vector.

Related

source
PortfolioOptimisers.inverse_wishart_bias — Function
inverse_wishart_bias(
    _::RootMeanSquaredAdjusted,
    b::Number,
    _::Integer
) -> Number

Computes the factor of method for the inverse-Wishart law $\nu / \chi^{2}_{\nu - n + 1}$ whose mean is b.

At equal weights this is the exact law of $R$, with $\nu = K$. On other weights it has the right pole at $K = n + 1$, so the ratio of the exact factor to it is smooth in $\lambda^{K}$, and mahalanobis_bias_nodes interpolates that ratio.

Arguments

  • method::RegimeAdjustedMethod: Regime adjustment method, which names the moment.
  • b::Number: Mean of $R$, from mahalanobis_bias.
  • n::Integer: Count of assets that contribute to the statistic.

Returns

  • factor::Number: b for the root mean square, $(\nu / 2) (\Gamma((\nu - n) / 2) / \Gamma((\nu - n + 1) / 2))^{2}$ for the first moment, and $(\nu / 2) e^{-\psi((\nu - n + 1) / 2)}$ for the log, with $\nu = b (n + 1) / (b - 1)$.

Related

source
PortfolioOptimisers.mahalanobis_bias_saturation — Function
mahalanobis_bias_saturation(
    decay::Number,
    n::Integer
) -> Any

Returns the count of observations past which the Mahalanobis bias factor no longer changes: the count where $\lambda^{K}$ falls below the machine epsilon, and at least the gate n + 4.

Arguments

  • decay::Number: Decay of the weights.
  • n::Integer: Count of assets that contribute to the statistic.

Returns

  • Ksat::Int: The count.

Related

source
PortfolioOptimisers.mahalanobis_bias_start — Function
mahalanobis_bias_start(
    _::Number,
    n::Integer,
    _::Nothing
) -> Any

Returns the first count of observations that the Mahalanobis statistic scores, where the interpolation of mahalanobis_bias_nodes starts.

Without a HAC adjustment that is n + 4. With one, regime_bias_open also asks that the effective count $1 / \operatorname{tr}(A^{2})$ pass n + 1, which the banded weight matrix reaches later: at 12 assets, a half-life of 10 and two lags, at 54 observations. The search stops at mahalanobis_bias_saturation.

Arguments

  • decay::Number: Decay of the weights.
  • n::Integer: Count of assets that contribute to the statistic.
  • hac_lags::Option{<:Union{<:Integer, <:VecNum}}: Count of HAC lags, the weight of each lag, or nothing.

Returns

  • K1::Int: The count.

Related

source
PortfolioOptimisers.mahalanobis_bias_nodes — Function
mahalanobis_bias_nodes(
    method::RegimeAdjustedMethod,
    decay::Number,
    n::Integer
) -> NamedTuple{(:sigma, :bw, :ratio, :exact, :start), <:NTuple{5, Any}}
mahalanobis_bias_nodes(
    method::RegimeAdjustedMethod,
    decay::Number,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}
) -> NamedTuple{(:sigma, :bw, :ratio, :exact, :start), <:NTuple{5, Any}}
mahalanobis_bias_nodes(
    method::RegimeAdjustedMethod,
    decay::Number,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring
) -> NamedTuple{(:sigma, :bw, :ratio, :exact, :start), <:NTuple{5, Any}}
mahalanobis_bias_nodes(
    method::RegimeAdjustedMethod,
    decay::Number,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring,
    debias::AbstractRegimeDebias
) -> NamedTuple{(:sigma, :bw, :ratio, :exact, :start), <:NTuple{5, Any}}
mahalanobis_bias_nodes(
    method::RegimeAdjustedMethod,
    decay::Number,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring,
    debias::AbstractRegimeDebias,
    recursion::AbstractLevelRecursion
) -> NamedTuple{(:sigma, :bw, :ratio, :exact, :start), <:NTuple{5, Any}}

Computes the nodes of the interpolation of the Mahalanobis bias factor over the count of observations, at one count of assets.

The ratio of the factor of mahalanobis_level_bias to the factor of inverse_wishart_bias is smooth in $\lambda^{K}$, from the gate $K = n + 4$ to the count where $\lambda^{K}$ falls below the machine epsilon. Sixteen Chebyshev–Lobatto points in $\lambda^{K}$, rounded to counts, interpolate it to within $10^{-6}$ at every count, at half-lives from 2 to 250 and from 5 to 30 assets. The nodes cost about 0.3 s at 12 assets and 0.65 s at 30 assets, once for each count of assets that a fit meets.

With a HAC adjustment the first count, mahalanobis_bias_start, is where the gate opens, and there the factor is steep: a HAC estimate has about half the effective observations of a plain one. The ratio to the law of the plain fixed point, whose pole at $K = n + 1$ is the pole of the factor, then varies too fast near the gate for a polynomial in $\lambda^{K}$ at a long half-life: sixteen points left $7 \times 10^{-3}$ at a half-life of 250. So the first 32 counts take the factor of the recursion itself, and sixteen points interpolate the rest. Over one to five lags and 2 to 12 assets that is within $10^{-9}$ at half-lives of 5, 10 and 40, and within $5 \times 10^{-6}$ at a half-life of 250. The nodes cost 0.3 s to 1.5 s at half-lives up to 40, and up to 3.7 s at a half-life of 250, five lags and 12 assets.

Arguments

  • method::RegimeAdjustedMethod: Regime adjustment method, which names the moment.
  • decay::Number: Decay of the weights.
  • n::Integer: Count of assets that contribute to the statistic, at least two.
  • hac_lags::Option{<:Union{<:Integer, <:VecNum}}: Count of HAC lags, the weight of each lag, or nothing.
  • centring::AbstractCentring: The centring of the estimator, PreCentred by default.
  • debias::AbstractRegimeDebias: The debias rule, ExactDebias by default.
  • recursion::AbstractLevelRecursion: The rule of the level recursion, MeanLevelRecursion by default. Each factor is multiplied by the ratio of mahalanobis_recursion_ratio, on the pre-centred law at every centring.

Returns

  • nodes::NamedTuple: sigma, the nodes $\lambda^{K - n - 4}$; bw, their barycentric weights; ratio, the ratio at each node; exact, the factor at each count from start that the interpolation does not serve, empty without HAC; and start, the first count.

Related

source
PortfolioOptimisers.mahalanobis_regime_bias! — Function
mahalanobis_regime_bias!(
    store::AbstractDict,
    method::RegimeAdjustedMethod,
    decay::Number,
    K::Integer,
    n::Integer
) -> Any
mahalanobis_regime_bias!(
    store::AbstractDict,
    method::RegimeAdjustedMethod,
    decay::Number,
    K::Integer,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}}
) -> Any
mahalanobis_regime_bias!(
    store::AbstractDict,
    method::RegimeAdjustedMethod,
    decay::Number,
    K::Integer,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring
) -> Any
mahalanobis_regime_bias!(
    store::AbstractDict,
    method::RegimeAdjustedMethod,
    decay::Number,
    K::Integer,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring,
    debias::AbstractRegimeDebias
) -> Any
mahalanobis_regime_bias!(
    store::AbstractDict,
    method::RegimeAdjustedMethod,
    decay::Number,
    K::Integer,
    n::Integer,
    hac_lags::Union{Nothing, Integer, AbstractVector{<:Union{var"#s152", var"#s69"} where {var"#s152"<:Number, var"#s69"<:AbstractJuMPScalar}}},
    centring::AbstractCentring,
    debias::AbstractRegimeDebias,
    recursion::AbstractLevelRecursion
) -> Any

Returns the Mahalanobis bias factor that a regime method reads, and makes the nodes of its interpolation the first time the state meets a count of assets.

Arguments

  • store::AbstractDict: Nodes of the state, by count of assets (mutated where a count is new).
  • method::RegimeAdjustedMethod: Regime adjustment method, which names the moment.
  • decay::Number: Decay of the weights.
  • K::Integer: Count of observations in the estimate, that regime_bias_open scores.
  • n::Integer: Count of assets that contribute to the statistic.
  • hac_lags::Option{<:Union{<:Integer, <:VecNum}}: Count of HAC lags, the weight of each lag, or nothing.
  • centring::AbstractCentring: The centring of the estimator, PreCentred by default.
  • debias::AbstractRegimeDebias: The debias rule, ExactDebias by default.
  • recursion::AbstractLevelRecursion: The rule of the level recursion, MeanLevelRecursion by default.

Returns

Related

source
PortfolioOptimisers.regime_bias_store — Function
regime_bias_store(
    _::RegimeAdjustedTarget,
    _::Number,
    _::Type{T}
) -> NamedTuple{(:moments, :kernel), <:Tuple{Array{Tuple{T, T, T}, 1} where T, Vector{T} where T<:Number}}

Makes the empty table of bias factors of a target that reads one estimated variance per direction.

Arguments

  • ::RegimeAdjustedTarget: Regime-adjustment target.
  • ::Number: Ignored decay.
  • ::Type{T}: Element type of the state.

Returns

  • bias::Vector{T}: An empty vector.

Related

source
regime_bias_store(
    _::DiagonalTarget,
    decay::Number,
    _::Type{T}
) -> NamedTuple{(:moments, :kernel), <:Tuple{Array{Tuple{T, T, T}, 1} where T, Vector{T} where T<:Number}}

Makes the empty store of the diagonal target: the moments of one term of RegimeTermMoments, a triple for each count of observations, and the HAC kernel of its correlation.

Arguments

  • ::DiagonalTarget: Diagonal regime-adjustment target.
  • decay::Number: Decay of the weights, whose type is the type of the kernel.
  • ::Type{T}: Element type of the state.

Returns

  • bias::NamedTuple: moments, an empty vector of NTuple{3, T}; and kernel, the empty kernel of correlation_hac_lags!, which only the separate path under HAC fills.

Related

source
regime_bias_store(
    _::PortfolioTarget,
    decay::Number,
    _::Type{T}
) -> NamedTuple{(:table, :variance, :convolution), <:Tuple{Any, Any, Vector{T} where T<:Number}}

Makes the empty store of the portfolio target: the table of its factor, and the two tables that the separate correlation path adds.

Arguments

  • ::PortfolioTarget: Portfolio regime-adjustment target.
  • decay::Number: Decay of the correlation structure, whose type is the type of the sums.
  • ::Type{T}: Element type of the state.

Returns

  • bias::NamedTuple: table, the empty table of regime_bias! at decay; variance, the empty table at the decay of the variance; and convolution, the empty table of exp_weight_convolution_table. Only the separate path fills the last two.

Related

source
regime_bias_store(
    _::MahalanobisTarget,
    decay::Number,
    _::Type
) -> NamedTuple{(:nodes, :kappa, :spread, :kernel), <:Tuple{Dict{Int64, NamedTuple{(:sigma, :bw, :ratio, :exact, :start), var"#s183"}} where var"#s183"<:Tuple{Vector{<:Number}, Vector{<:Number}, Vector{<:Number}, Vector{<:Number}, Int64}, Vector{T} where T<:Number, Vector{T} where T<:Number, Vector{T} where T<:Number}}

Makes the empty store of the Mahalanobis bias factor: its interpolation nodes, keyed by the count of assets, the two tables of the variance factor of the separate correlation path, and the HAC kernel of its correlation.

Arguments

  • ::MahalanobisTarget: Mahalanobis regime-adjustment target.
  • decay::Number: Decay of the weights, whose type is the type of the nodes.
  • ::Type: Ignored element type of the state.

Returns

Related

source
PortfolioOptimisers.variance_noise_bias! — Function
variance_noise_bias!(
    store::NamedTuple,
    ce::RegimeAdjustedExpWeightedCovariance,
    K::Integer,
    n::Integer
) -> Any

Returns the factor by which the noise of the variance at decay moves the bias of the squared Mahalanobis distance on the separate correlation path, and grows the tables of the state where the count is past their end.

On the separate path the block is $\hat{C} = D \hat{R} D$, with the volatilities $D$ from the variance at decay and the correlation $\hat{R}$ from the recursion at cor_decay. The factor of mahalanobis_regime_bias! at cor_decay is the bias of an estimate whose diagonal carries the noise of cor_decay. At $R = I$ the statistic is $\sum_{i} (\hat{R}^{-1})_{ii} / \hat{V}_{i}$, and at equal weights $\hat{R}$ is independent of the variances, so the diagonal enters the mean through $\mathbb{E}[1/Q]$ alone: $\kappa$ is the ratio of that moment at the two decays, from the exact tables of regime_bias_table, plain or HAC.

The noisier variance also spreads the statistic, which lowers the root and the log that FirstMomentRegimeAdjusted and LogRegimeAdjusted read. The spread is the sum over the assets of the noise of each $1 / \hat{V}_{i}$, so it averages over the assets. At one asset the block is the variance alone, and the exact factor is the method's own table at decay; the ratio $\rho$ of the method's moment to the mean's carries that step. For $n$ assets, the second-order change of the root and the log of a quadratic form whose $n$ terms carry independent noise is $3 / (n + 2)$ of the change at one asset, exactly on a sphere, so the power is $3 / (n + 2)$. It is one at one asset, and the factor tends to $\kappa$ alone as the count of assets grows. For RootMeanSquaredAdjusted, $\rho = 1$.

Mathematical definition

\[\begin{align} f_{K,n} &= \kappa_{K} \left(\frac{\rho_{\lambda,K}}{\rho_{\lambda_{c},K}}\right)^{3 / (n + 2)}\,, \qquad \kappa_{K} = \frac{\mathbb{E}_{\lambda}\left[Q_{K}^{-1}\right]}{\mathbb{E}_{\lambda_{c}}\left[Q_{K}^{-1}\right]}\,, \qquad \rho_{\lambda,K} = \frac{m_{\lambda}(Q_{K})}{\mathbb{E}_{\lambda}\left[Q_{K}^{-1}\right]}\,. \end{align}\]

Where:

  • $Q_{K}$: An estimated variance of $K$ observations over the true one.
  • $\lambda$, $\lambda_{c}$: decay and cor_decay.
  • $m_{\lambda}$: The moment of $Q^{-1}$ that the regime method reads, its table at $\lambda$.
  • $n$: Count of assets that contribute to the statistic.

At 12 assets, a half-life of 10 and a correlation half-life of 20, $\kappa = 1.0347$, and the power of the ratio is 0.9981 for FirstMomentRegimeAdjusted and 0.9963 for LogRegimeAdjusted; at two HAC lags, where the tables at cor_decay read the kernel of correlation_hac_lags!, 1.0762, 0.9960 and 0.9921. Against the true factor of the separate path, measured in the steady state over 64 seeds, the three methods then read 1.0018, 1.0018 and 1.0017 at $R = I$, where the spread had put them at 1.0018, 0.9999 and 0.9980. At 4 assets and half-lives of 5 and 40 the power leaves 0.02 % and 0.06 % of a gap of 1.5 % and 3.0 %. It holds the noise of the variances with the correlation fixed. The noise of $\hat{R}$ amplifies the spread, the data that the two estimates share reduces it, and the rule that divides each correlation row by a volatility moves it. Without HAC the three cancel at $R = I$. The spread grows with the correlation of the assets, which the power does not read: at an equicorrelation of 0.8 it carries 42 % of it, so the methods read 0.9969, 0.9943 and 0.9917.

Under HAC each row divides by the volatility before its update, which damps the lagged terms of the row, and the tables at cor_decay read the kernel of hac_row_kernel that holds the damping. At two lags the methods then read 1.0076, 1.0078 and 1.0077 at $R = I$, and 1.0049, 0.9993 and 0.9931 at an equicorrelation of 0.8. Over 4 to 24 assets, half-lives of 5 to 20, correlation half-lives of 20 and 40 and one to four lags the mean reads 0.2 % to 2.2 % high at $R = I$, where the Bartlett kernel read 0.8 % to 9.5 % low, and −2.6 % to +3.4 % over every correlation of the same grid; the rest grows with the count of assets and of lags. Where each row divides by the volatility after its update (hac_vol_before = VolatilityAfterUpdate()), the tables keep the Bartlett kernel, and the mean reads 1.1 % to 3.0 % high at two lags.

The factor does not read the correlation of the assets. With $h_{i} = \sqrt{Q_{ii} / \hat{V}_{i}}$ the statistic splits exactly as $\sum_{i} h_{i}^{2} (Q^{-1} R)_{ii} - \frac{1}{2} \sum_{i,j} (Q^{-1})_{ij} R_{ij} (h_{i} - h_{j})^{2}$. Where the correlation rows are not standardised, the mean of the first sum does not read $R$. The standardisation moves both sums with $R$, by amounts that nearly cancel: at an equicorrelation of 0.8 the first falls by 1.66 % and the second rises by 1.17 %. The coupling of the noise of $Q^{-1}$ with the noise of $h$ carries most of each change, and no expansion in the noise of the variances holds it to the accuracy that the small difference needs. Without HAC the three methods read 0.9897 to 1.0018 of the truth over equicorrelations from 0 to 0.9 and a three-factor model, which reads 0.9947 for the mean.

Arguments

  • store::NamedTuple: Store of the state, whose tables kappa and spread grow (mutated).
  • ce::RegimeAdjustedExpWeightedCovariance: Covariance estimator configuration.
  • K::Integer: Count of observations in the estimate.
  • n::Integer: Count of assets that contribute to the statistic.

Returns

  • factor::Number: The factor at K, one where the estimator runs one decay.

Related

source