Coskewness: private API
PortfolioOptimisers.negative_spectral_coskewness — Function
negative_spectral_coskewness(cskew::MatNum, X::MatNum,
mp::Option{<:AbstractMatrixProcessingEstimator})Internal helper that builds the negative spectral skewness matrix.
negative_spectral_coskewness splits the coskewness tensor into its N symmetric blocks of size N x N, keeps the negative part of the spectrum of each block, and sums the negated parts into one N x N matrix. The matrix processing estimator runs once, on the summed result, and not on the individual blocks.
The reduction runs on the block, which is the same law matrix_processing_block! keeps for a covariance frame, read at this quantity's own resolution. An available-case fit under a CoveragePolicy emits a frame: the tensor carries NaN across the rows and the pair columns of every asset the policy refused. The block is derived from the tensor's own per-asset diagonal, cskew[i, (i - 1) * N + i], which is asset i's third central moment and is finite exactly where the fit answered for that asset. A non-finite cell inside the block — a triple whose three assets were each estimated and which share no observation — is refused by name with assert_finite_block, and never fed to eigen. The spectral step and mp then run on the block alone, and the answer is written back into a NaN frame of the full width, so an asset the policy refused leaves this verb as it entered it.
This verb is the one place that rule is written for a coskewness, and the three call sites that reduce one — _coskewness, port_opt_view of a HighOrderPrior and the high order factor prior — all reach it.
Mathematical definition
Write the coskewness tensor as $\mathbf{S} = [\mathbf{S}_{1} \vert \mathbf{S}_{2} \vert \ldots \vert \mathbf{S}_{N}]$. Each block $\mathbf{S}_{i}$ is symmetric, so its eigendecomposition is real. Keep the negative eigenvalues alone and negate the sum:
\[\begin{align} \mathbf{S}_{i} &= \mathbf{Q}_{i} \mathbf{\Lambda}_{i} \mathbf{Q}_{i}^{\intercal}\,, \\ \mathbf{S}_{i}^{-} &= \mathbf{Q}_{i} \mathbf{\Lambda}_{i}^{-} \mathbf{Q}_{i}^{\intercal}\,, \\ \mathbf{V} &= -\sum\limits_{i=1}^{N} \mathbf{S}_{i}^{-}\,. \end{align}\]
Where:
- $\mathbf{S}_{i}$: $i$-th $N \times N$ block of the coskewness tensor.
- $\mathbf{Q}_{i}$: Matrix of eigenvectors of $\mathbf{S}_{i}$.
- $\mathbf{\Lambda}_{i}^{-}$: Diagonal matrix holding the negative eigenvalues of $\mathbf{S}_{i}$, with every non-negative one set to zero.
- $\mathbf{V}$: Negative spectral skewness matrix. It is positive semidefinite, because it is a sum of negated negative semidefinite matrices.
- $N$: Number of assets.
The entry $\mathbf{S}_{i,\,aj}$ is the third comoment of the deviations of the assets $a$, $i$ and $j$. That comoment does not depend on the order of its three assets, which is why the block is symmetric.
Algorithm
- Read
Nfrom the row count ofcskew, and take the blockblkasisfinite.(cskew[i, (i - 1) * N + i])overi. - Reduce the tensor whole when
blkholds notrue. It carries no block to refuse anything inside, so it is the plain path's and meets its refusal. - Refuse a non-finite cell inside the block with
assert_finite_block. The block is the whole tensor whenblkholds nofalse, and the refusal covers that case too. - Cut the tensor to the block, at the rows
blkand the pair columns ofcoverage_pair_index, and cutXto the same assets. Both are the objects themselves whereblkholds nofalse. - Allocate the accumulator
Vof zeros at the width of the block. - For each block index
i, takecoskew_jk, the view of the columns(i - 1) * N + 1toi * Nof the cut tensor. - Eigendecompose
coskew_jk, giving the eigenvaluesvalsand the eigenvectorsvecs, and subtract fromVthe negative semidefinite part thatnegative_spectral_partrebuilds from them. - Run
matrix_processing!once, on the accumulatedV. No block is processed on its own. - Return
Vwhere the block is the whole tensor, and writeVinto the block of aNaNframe of the full width otherwise.
Arguments
cskew: Coskewness tensor,assets × assets², laid out asNblocks ofNcolumns. It may be a frame, whose refused assets carryNaNacross their rows and their pair columns.X: Data matrix (observations × assets).matrix_processing!reads it, and the spectral step does not.mp: Optional matrix processing estimator.::AbstractMatrixProcessingEstimator: The estimator processes the accumulatedVin-place.::Nothing: No-op.Vis the raw sum of the negated negative parts.
Validation
- The block of
cskewis finite. AnIsNonFiniteErrornaming the cell is thrown otherwise.
Returns
V::MatNum: Processed coskewness matrixassets x assets. It is the $\mathbf{V}$ above, so a portfolio's negative quadratic skewness is the quadratic form $\boldsymbol{w}^{\intercal} \mathbf{V} \boldsymbol{w}$. An asset outside the block carriesNaNacross its row and its column.
Related
Coskewness_coskewnessmatrix_processing!matrix_processing_block!negative_spectral_partassert_finite_blockCoveragePolicycoskewness
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 7.2.5.1, Equations 7.104 and 7.105.
- [31] D. Cajas. On the Spectral Decomposition of Portfolio Skewness and its Application to Portfolio Optimization. Available at SSRN 4540021 (2023).
PortfolioOptimisers.negative_spectral_part — Function
negative_spectral_part(vals::AbstractVector{<:Real}, vecs::MatNum, Tf::Type) -> MatNum
negative_spectral_part(vals::AbstractVector{<:Complex}, vecs::MatNum, Tf::Type) -> MatNumRebuilds one block of a coskewness tensor from the negative part of its spectrum alone.
The inner step of negative_spectral_coskewness, taken out so the real and the complex spectrum are two methods rather than one branch. LinearAlgebra.eigen of a general real matrix answers a real spectrum or a complex one, and only the value says which, so a branch on eltype(vals) is a runtime test that no caller's types settle: the two methods settle it on the argument instead, and each body then sees a concrete element type.
Every non-negative eigenvalue is clamped to zero and every negative one is kept, so the reconstruction is the negative semidefinite part of the block. The caller subtracts it, which is what makes the accumulated matrix positive semidefinite.
The complex method is a fallback rather than a second definition: a block of the tensor is symmetric, so its spectrum is real, and eigen answers a complex one only where round-off has left the block asymmetric. It clamps the real and the imaginary parts the same way and takes the real part of the reconstruction.
Arguments
vals: The eigenvalues of the block.vecs: The eigenvectors of the block.Tf: Element type of the tensor, which sets the clamp's bounds.
Returns
S::MatNum: The negative semidefinite part of the block, of the shape of the block.
Related
PortfolioOptimisers.coverage_coskewness — Function
coverage_coskewness(ske, cvg, X; dims::Int = 1, mean = nothing,
active_mask = nothing, kwargs...) -> (MatNum, MatNum)Fits a coskewness tensor over the arm the estimator's coverage policy selects.
The cvg field of the estimator is passed as the second argument, so the arm is chosen by dispatch on the policy rather than by a branch on its value, exactly as coverage_covariance does for a covariance. The moment algorithm of ske chooses between the FullMoment arm, which centres on each asset's own mean and takes the deviations whole, and the SemiMoment arm, which clips them at zero first.
Arguments
ske: Coskewness estimator.cvg: The policy the estimator carries, which selects the arm.X: Data matrix (observations × assets).dims: Dimension along which to perform the computation.mean: Optional mean vector. The available-case arms refuse one.active_mask: The active mask of the Asset Panel over the window, ornothing. The Coverage Universe arm ignores it.kwargs...: Additional keyword arguments passed to the mean estimator.
Returns
cskew::MatNum: Coskewness tensorassets x assets².V::MatNum: Processed coskewness matrixassets x assets.
Related
PortfolioOptimisers.coverage_coskewness — Method
coverage_coskewness(
ske::Coskewness{<:Any, <:Any, <:FullMoment},
::Nothing,
X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
dims,
mean,
active_mask,
kwargs...
) -> Tuple{Any, Any}
Nothing method of the FullMoment arm of coverage_coskewness. The Coverage Universe arm, which centres the whole window on one vector and delegates to _coskewness, and which is the body the verb has always had.
Related
PortfolioOptimisers.coverage_coskewness — Method
coverage_coskewness(
ske::Coskewness{<:Any, <:Any, <:SemiMoment},
::Nothing,
X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
dims,
mean,
active_mask,
kwargs...
) -> Tuple{Any, Any}
Nothing method of the SemiMoment arm of coverage_coskewness. The Coverage Universe arm, which clips the de-meaned returns at zero before delegating, and which is the body the verb has always had.
Related
PortfolioOptimisers.coverage_coskewness — Method
coverage_coskewness(
ske::Coskewness,
cvg::CoveragePolicy,
X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
dims,
mean,
active_mask,
kwargs...
) -> Tuple{Any, Any}
CoveragePolicy method of coverage_coskewness. The available-case arm: each triple is fitted on the observations at which its three assets are all finite and active, and each cell carries its own denominator.
The centre is each asset's own available-case mean, its own finite and active observations alone, and not the triple's. A per-triple centre would be assets³ running means, which is the same reason the SemiMoment arm of coverage_covariance centres per asset. So an external mean is refused, as it is there.
The SemiMoment arm differs in one line, the clip, which is why the two share this body through coverage_comoment_block. Neither folds: an exact per-cell recursion at this order needs the pairwise second co-moments over each triple's own observation set, so the online form of an available-case coskewness is a buffer refit through Online.
Algorithm
- Read the valid entries, the per-asset available-case centre and the per-asset bookkeeping of the block with
coverage_comoment_block. - Take the numerator as
transpose(Y) * zand the denominator astranspose(Mi) * zc, wherezandzcare the pairwise expansions of the zeroed deviations and of the valid mask. - Divide with
coverage_divide, and frame the refused assets down the asset axis and the pair axis withcoverage_refuse_comoment!. - Reduce the frame with
negative_spectral_coskewness, which keeps the block rule.
Related
PortfolioOptimisers._coskewness — Function
_coskewness(Y::MatNum, X::MatNum, mp::AbstractMatrixProcessingEstimator, w::Option{<:StatsBase.AbstractWeights}) -> (MatNum, MatNum)Internal helper that builds the coskewness tensor from a deviation matrix.
_coskewness returns the tensor together with its negative spectral skewness matrix. The matrix processing estimator runs on the second one alone, inside negative_spectral_coskewness, and never on the tensor.
Mathematical definition
The $N \times N^{2}$ coskewness tensor is the third comoment matrix of the deviations:
Unweighted:
\[\begin{align} \hat{\mathbf{S}} &= \frac{1}{T} \mathbf{Y}^\intercal \mathbf{Z}\,, \\ \mathbf{Z}_{t,\cdot} &= (\boldsymbol{1}^\intercal \otimes \boldsymbol{y}_t^\intercal) \odot (\boldsymbol{y}_t^\intercal \otimes \boldsymbol{1}^\intercal)\,. \end{align}\]
Weighted:
\[\begin{align} \hat{\mathbf{S}} &= \frac{1}{\sum_{t=1}^{T} w_t} (\boldsymbol{w} \odot \mathbf{Y})^\intercal \mathbf{Z}\,. \end{align}\]
Where:
- $\hat{\mathbf{S}}$: $N \times N^{2}$ coskewness tensor. Its entry $\hat{\mathbf{S}}_{a,\,(i-1)N+j}$ is the third comoment of the deviations of the assets $a$, $i$ and $j$, so each of its $N$ blocks of $N$ columns is symmetric.
- $\mathbf{Y}$: $T \times N$ deviation matrix.
FullMomenttakes the centred returns, andSemiMomentclips every positive entry of them to zero. - $\boldsymbol{y}_t$: $N \times 1$ deviation vector of observation $t$, the $t$-th row of $\mathbf{Y}$. Its $i$-th entry is $y_{t,\,i}$.
- $\mathbf{Z}$: $T \times N^{2}$ pairwise expansion of $\mathbf{Y}$, whose $t$-th row is $\mathbf{Z}_{t,\cdot}$ and whose entry $\mathbf{Z}_{t,\,(i-1)N+j}$ is the product $y_{t,\,i} \, y_{t,\,j}$.
- $\boldsymbol{w}$: $T \times 1$ observation weights vector.
- $w_{t}$: Observation weight of observation $t$.
- $T$: Number of observations.
- $N$: Number of assets.
- $\boldsymbol{1}$: $N \times 1$ vector of ones.
- $\otimes$: Kronecker product.
- $\odot$: Element-wise product. Where the operands differ in shape, it broadcasts along the row axis.
Algorithm
- Build
o, the $1 \times N$ row of ones. - Build
z, the pairwise expansionkron(o, Y) ⊙ kron(Y, o). Its column(i - 1) * N + jis the element-wise product of the columnsiandjofY. - Without weights, form
cskewastranspose(Y) * z / size(Y, 1). - With weights, form
cskewastranspose(w .* Y) * z / sum(w). The weights multiply the left factor alone, so each summand carries one weight and not three. - Reduce
cskewtoVwithnegative_spectral_coskewness, which runsmpon the reduced matrix. - Return the pair
(cskew, V).
Arguments
Y: Deviation matrix (observations × assets), already centred by the caller.X: Data matrix (observations × assets). It reachesmatrix_processing!throughnegative_spectral_coskewness.mp: Matrix processing estimator.w: Optional observation weights. The unweighted method takesnothingthrough itsargs....
Returns
cskew::MatNum: Coskewness tensorassets x assets².V::MatNum: Processed coskewness matrixassets x assets.
Related
References
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
- [30]
- D. Cajas. Convex Optimization of Portfolio Kurtosis. Available at SSRN 4202967 (2022).
- [31]
- D. Cajas. On the Spectral Decomposition of Portfolio Skewness and its Application to Portfolio Optimization. Available at SSRN 4540021 (2023).