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
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 ats, and in the integral from zero tos.
Related
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:fat each point, with the shape ofpts.
Related
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
s::Number: Point to evaluate at.geo::NamedTuple:sc,xlo,xhi,h,NGandsendofmahalanobis_bias_grid.
Returns
st::Tuple{Int, NTuple{8, <:Number}}: First index and weights, forstencil_dot.
Related
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
s::Number: Point to evaluate at.geo::NamedTuple:sc,xlo,xhi,h,NGandsendofmahalanobis_bias_grid.
Returns
st::Tuple{Int, NTuple{8, <:Number}}: First index and weights, forstencil_dot.
Related
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 latticexfof $\ln \sigma$, $1/8$ by default.xflo::Number: First point ofxf, $-61 + \ln(1 - \lambda)$ by default.xfhi::Number: Last point ofxf, 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 steph, the gridsg, the node pointsnodes, the stepssuof the integral, the pair stencils, the output gridsoofregime_bias_table, the latticexfon whichgand $\rho_{0}$ are tabulated, the boundsgeoof the grid, andcut = nothing, whichmahalanobis_level_biasreplaces where the transform is cut.
Related
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
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, ornothing.
Returns
(G, Rh, peaks)::Tuple: $g$ and $\rho_{0}$ with one column for each count, and for each count the peak ofhac_log_det_peak, ornothingwhere the transform takes no cut.
Related
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 wheregis $\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
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 ofdecay,Kandhac_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 fromhac_log_det_slopeson the banded matrix.
Returns
peak::Option{<:Tuple{<:Number, <:Number}}: $(\sigma^{*}, \ln \det(I + \sigma^{*} B))$, ornothingwhere the derivative is positive on the whole lattice, because $B$ is positive definite.
Related
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}$, ornothingwhere $I + t A$ is not positive definite.
Related
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; andk, the weight $k_{i}$ of each lag, fromhac_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
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
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 ofhac_log_det_peak, ornothing.
Returns
cut::Option{<:NamedTuple}:nothingwherepeakisnothing. Else the cut $s^{*}$ assstar, and asgtthe 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 ofhac_log_det_peakis still positive, and the peak then falls on that point.
Related
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 ofmahalanobis_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
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 ofmahalanobis_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, frommahalanobis_q_integral!.m::Integer: Shift of the lattice.
Returns
(so, L)::Tuple{<:VecNum, <:VecNum}: The grid and the transform on it.
Related
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
- Fill the integral $Q$ of the removed part with
mahalanobis_q_integral!. - Read $q$ and $Q$ at each node: the Chebyshev nodes, then the grid past the junction.
- 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$.
- 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 ofmahalanobis_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 ofmahalanobis_bias_grid.
Returns
v::VecNum: The values of the next level.
Related
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 ofmahalanobis_bias_grid.
Returns
e::VecNum: The filled vector.
Related
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$, frommahalanobis_bias.n::Integer: Count of assets that contribute to the statistic.
Returns
factor::Number:bfor 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
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
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, ornothing.
Returns
K1::Int: The count.
Related
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, ornothing.centring::AbstractCentring: The centring of the estimator,PreCentredby default.debias::AbstractRegimeDebias: The debias rule,ExactDebiasby default.recursion::AbstractLevelRecursion: The rule of the level recursion,MeanLevelRecursionby default. Each factor is multiplied by the ratio ofmahalanobis_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 fromstartthat the interpolation does not serve, empty without HAC; andstart, the first count.
Related
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, thatregime_bias_openscores.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, ornothing.centring::AbstractCentring: The centring of the estimator,PreCentredby default.debias::AbstractRegimeDebias: The debias rule,ExactDebiasby default.recursion::AbstractLevelRecursion: The rule of the level recursion,MeanLevelRecursionby default.
Returns
factor::Number: The factor of the recursion where the nodes hold it. Elseinverse_wishart_biasatK, times the interpolated ratio. A count pastmahalanobis_bias_saturationreads the saturation count.
Related
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
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 ofNTuple{3, T}; andkernel, the empty kernel ofcorrelation_hac_lags!, which only the separate path under HAC fills.
Related
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 ofregime_bias!atdecay;variance, the empty table at the decay of the variance; andconvolution, the empty table ofexp_weight_convolution_table. Only the separate path fills the last two.
Related
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
bias::NamedTuple:nodes, an empty dictionary from a count of assets to the nodes ofmahalanobis_bias_nodes;kappaandspread, the empty tables ofvariance_noise_bias!, which only the separate path fills; andkernel, the empty kernel ofcorrelation_hac_lags!, which only the separate path under HAC fills.
Related
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}$:
decayandcor_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 tableskappaandspreadgrow (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 atK, one where the estimator runs one decay.
Related