The source files can be found in examples/.

Higher moment estimation

Mean–variance optimisation only looks at the first two moments. But asset returns are skewed and fat-tailed, and risk measures like NegativeSkewness and Kurtosis need estimates of the coskewness and cokurtosis tensors to capture that. These high-order moments are even harder to estimate than the covariance: the cokurtosis matrix is $N^2 \times N^2$, so with a short window it is wildly over-parametrised and numerically near-singular. As with the covariance, denoising and sparsification rescue it.

In PortfolioOptimisers the high-order moments live in a HighOrderPriorEstimator, which wraps a low-order prior and adds a Coskewness (ske) and Cokurtosis (kte) estimator. Each accepts the same MatrixProcessing pipeline — Denoise and LoGo — that we apply to covariances.

When to reach for this

Reach for high-order moment estimation whenever you optimise against a skew- or tail-sensitive risk measure (NegativeSkewness, Kurtosis, and the square-root variants), or build a Pareto surface over them. And reach for denoised high-order moments essentially always when you do: the raw cokurtosis on a short window is numerically singular, so denoising is what makes these optimisations well-posed rather than a nicety. If you only use variance/tail measures that need no tensors, skip it.

using PortfolioOptimisers, PrettyTables, LinearAlgebrahmmtfmt = (v, i, j) -> begin    if j == 1        return v    else        return isa(v, Number) ? "$(round(v, digits=6))" : v    endend;resfmt = (v, i, j) -> begin    if j == 1        return v    else        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v    endend;

1. ReturnsResult data

We use the same S&P 500 slice as the other examples.

using CSV, TimeSeries, DataFramesX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)
ReturnsResult
    nx ┼ 20-element Vector{String}
     X ┼ 252×20 Matrix{Float64}
    nf ┼ nothing
     F ┼ nothing
    nb ┼ nothing
     B ┼ nothing
    ts ┼ 252-element Vector{Date}
    iv ┼ nothing
  ivpa ┼ nothing
    nz ┼ nothing
     Z ┴ nothing

2. High-order priors

We build three high-order priors that differ only in how the coskewness and cokurtosis are processed: raw (vanilla), FixedDenoised, and LoGo-sparsified. We then compare the condition numbers of the coskewness negative-spectral-slice matrix V and the cokurtosis matrix kt.

hopes = ["Vanilla" => HighOrderPriorEstimator(),         "Denoise" => HighOrderPriorEstimator(;                                              ske = Coskewness(;                                                               mp = MatrixProcessing(;                                                                                     dn = Denoise(;                                                                                                  alg = FixedDenoise()))),                                              kte = Cokurtosis(;                                                               mp = MatrixProcessing(;                                                                                     dn = Denoise(;                                                                                                  alg = FixedDenoise())))),         "LoGo" => HighOrderPriorEstimator(;                                           ske = Coskewness(;                                                            mp = MatrixProcessing(;                                                                                  dn = Denoise(),                                                                                  alg = LoGo())),                                           kte = Cokurtosis(;                                                            mp = MatrixProcessing(;                                                                                  dn = Denoise(),                                                                                  alg = LoGo())))]prs = [k => prior(pe, rd) for (k, pe) in hopes]
3-element Vector{Pair{String}}:
 "Vanilla" => HighOrderPrior
    pr ┼ LowOrderPrior
       │       X ┼ 252×20 Matrix{Float64}
       │     o_X ┼ nothing
       │      mu ┼ 20-element Vector{Float64}
       │   sigma ┼ 20×20 Matrix{Float64}
       │    chol ┼ nothing
       │       w ┼ nothing
       │     ens ┼ nothing
       │     kld ┼ nothing
       │      ow ┼ nothing
       │      rr ┼ nothing
       │     fpr ┼ nothing
       │       Z ┴ nothing
    kt ┼ 400×400 Matrix{Float64}
    D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
    L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    sk ┼ 20×400 Matrix{Float64}
     V ┼ 20×20 Matrix{Float64}
  skmp ┼ MatrixProcessing
       │     pdm ┼ Posdef
       │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │      dn ┼ nothing
       │      dt ┼ nothing
       │     alg ┼ nothing
       │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
   fpr ┴ nothing

 "Denoise" => HighOrderPrior
    pr ┼ LowOrderPrior
       │       X ┼ 252×20 Matrix{Float64}
       │     o_X ┼ nothing
       │      mu ┼ 20-element Vector{Float64}
       │   sigma ┼ 20×20 Matrix{Float64}
       │    chol ┼ nothing
       │       w ┼ nothing
       │     ens ┼ nothing
       │     kld ┼ nothing
       │      ow ┼ nothing
       │      rr ┼ nothing
       │     fpr ┼ nothing
       │       Z ┴ nothing
    kt ┼ 400×400 Matrix{Float64}
    D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
    L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    sk ┼ 20×400 Matrix{Float64}
     V ┼ 20×20 Matrix{Float64}
  skmp ┼ MatrixProcessing
       │     pdm ┼ Posdef
       │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │      dn ┼ Denoise
       │         │      pdm ┼ Posdef
       │         │          │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │          │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │         │      alg ┼ FixedDenoise()
       │         │     args ┼ Tuple{}: ()
       │         │   kwargs ┼ @NamedTuple{}: NamedTuple()
       │         │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
       │         │        m ┼ Int64: 10
       │         │        n ┴ Int64: 1000
       │      dt ┼ nothing
       │     alg ┼ nothing
       │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
   fpr ┴ nothing

    "LoGo" => HighOrderPrior
    pr ┼ LowOrderPrior
       │       X ┼ 252×20 Matrix{Float64}
       │     o_X ┼ nothing
       │      mu ┼ 20-element Vector{Float64}
       │   sigma ┼ 20×20 Matrix{Float64}
       │    chol ┼ nothing
       │       w ┼ nothing
       │     ens ┼ nothing
       │     kld ┼ nothing
       │      ow ┼ nothing
       │      rr ┼ nothing
       │     fpr ┼ nothing
       │       Z ┴ nothing
    kt ┼ 400×400 Matrix{Float64}
    D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
    L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    sk ┼ 20×400 Matrix{Float64}
     V ┼ 20×20 Matrix{Float64}
  skmp ┼ MatrixProcessing
       │     pdm ┼ Posdef
       │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │      dn ┼ Denoise
       │         │      pdm ┼ Posdef
       │         │          │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │          │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │         │      alg ┼ ShrunkDenoise
       │         │          │   alpha ┴ Float64: 0.0
       │         │     args ┼ Tuple{}: ()
       │         │   kwargs ┼ @NamedTuple{}: NamedTuple()
       │         │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
       │         │        m ┼ Int64: 10
       │         │        n ┴ Int64: 1000
       │      dt ┼ nothing
       │     alg ┼ LoGo
       │         │    de ┼ Distance
       │         │       │   power ┼ nothing
       │         │       │     alg ┴ CanonicalDistance()
       │         │   sim ┼ MaximumDistanceSimilarity()
       │         │   pdm ┼ Posdef
       │         │       │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │       │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
   fpr ┴ nothing

The condition numbers tell the whole story. The raw cokurtosis is numerically singular (a condition number of order $10^{15}$); denoising and sparsification bring it down by many orders of magnitude, turning an ill-posed optimisation into a stable one.

pretty_table(DataFrame(; :estimator => [k for (k, _) in prs],                       Symbol("cond(V) coskew") => [cond(p.V) for (_, p) in prs],                       Symbol("cond(kt) cokurt") => [cond(p.kt) for (_, p) in prs]);             formatters = [hmmtfmt], title = "High-order moment conditioning")
            High-order moment conditioning
┌───────────┬────────────────┬───────────────────────┐
│ estimator  cond(V) coskew        cond(kt) cokurt │
│    String         Float64                Float64 │
├───────────┼────────────────┼───────────────────────┤
│   Vanilla │     129.142244 │ 1.8765543571913605e15 │
│   Denoise │      41.276399 │          16615.921472 │
│      LoGo │      60.068167 │          22717.663249 │
└───────────┴────────────────┴───────────────────────┘

3. Visualising the high-order moments

The coskewness and cokurtosis heatmaps show the denoising at work — the raw matrices are dense and noisy, the processed ones cleaner and better conditioned.

Coskewness heatmap: vanilla vs denoised.

using StatsPlots, GraphRecipes

Coskewness heatmap: denoised.

plot_coskewness(prs[1].second, rd)
Example block output

Cokurtosis eigenspectrum: vanilla vs denoised.

plot_coskewness(prs[2].second, rd)
Example block output

Cokurtosis eigenspectrum: denoised.

plot_cokurtosis(prs[1].second, rd)plot_cokurtosis(prs[2].second, rd)
Example block output

4. Why it matters: skew- and tail-aware optimisation

We minimise two high-order risk measures — NegativeSkewness and Kurtosis — using each high-order prior. With the raw (near-singular) tensors the solver is working against a degenerate problem; the denoised and sparsified priors give stable, sensible allocations.

using Clarabelslv = Solver(; name = :clarabel1, solver = Clarabel.Optimizer,             settings = Dict("verbose" => false),             check_sol = (; allow_local = true, allow_almost = true))ress_sk = [k => optimise(MeanRisk(; r = NegativeSkewness(), obj = MinimumRisk(),                                  opt = JuMPOptimiser(; pe = p, slv = slv)))           for (k, p) in prs]ress_kt = [k => optimise(MeanRisk(; r = Kurtosis(), obj = MinimumRisk(),                                  opt = JuMPOptimiser(; pe = p, slv = slv)))           for (k, p) in prs]pretty_table(DataFrame(["Assets" => rd.nx;                        ["NSkew $k" => r.w for (k, r) in ress_sk];                        ["Kurt $k" => r.w for (k, r) in ress_kt]]); formatters = [resfmt],             title = "Minimum skew / kurtosis weights by prior")
                            Minimum skew / kurtosis weights by prior
┌────────┬───────────────┬───────────────┬────────────┬──────────────┬──────────────┬───────────┐
│ Assets  NSkew Vanilla  NSkew Denoise  NSkew LoGo  Kurt Vanilla  Kurt Denoise  Kurt LoGo │
│ String        Float64        Float64     Float64       Float64       Float64    Float64 │
├────────┼───────────────┼───────────────┼────────────┼──────────────┼──────────────┼───────────┤
│   AAPL │         0.0 % │         0.0 % │    0.655 % │        0.0 % │        0.0 % │     0.0 % │
│    AMD │         0.0 % │         0.0 % │      0.0 % │        0.0 % │        0.0 % │     0.0 % │
│    BAC │      15.073 % │      21.071 % │   21.574 % │        0.0 % │        0.0 % │     0.0 % │
│    BBY │         0.0 % │         0.0 % │      0.0 % │        0.0 % │        0.0 % │     0.0 % │
│    CVX │       5.941 % │       2.999 % │      0.0 % │        0.0 % │      0.842 % │   7.201 % │
│     GE │         0.0 % │         0.0 % │      0.0 % │      1.548 % │      1.061 % │   6.095 % │
│     HD │         0.0 % │         0.0 % │      0.0 % │      2.616 % │      2.614 % │   3.492 % │
│    JNJ │      16.214 % │       10.15 % │   17.774 % │     40.295 % │     40.486 % │  33.468 % │
│    JPM │      30.097 % │      31.038 % │   19.773 % │      2.471 % │      2.373 % │   7.747 % │
│     KO │       0.794 % │       0.223 % │      0.0 % │      1.878 % │      1.292 % │     0.0 % │
│    LLY │       7.921 % │       7.467 % │   15.945 % │        0.0 % │        0.0 % │     0.0 % │
│    MRK │      14.537 % │      18.374 % │   23.329 % │     25.182 % │     23.836 % │  15.895 % │
│   MSFT │       7.633 % │        0.11 % │      0.0 % │        0.0 % │        0.0 % │     0.0 % │
│    PEP │         0.0 % │         0.0 % │      0.0 % │        0.0 % │      2.652 % │   0.942 % │
│    PFE │         0.0 % │         0.0 % │    0.442 % │        0.0 % │        0.0 % │   9.265 % │
│     PG │       1.365 % │       2.945 % │    0.508 % │        0.0 % │        0.0 % │     0.0 % │
│    RRC │         0.0 % │       1.989 % │      0.0 % │       4.31 % │      3.788 % │   2.112 % │
│    UNH │         0.0 % │         0.0 % │      0.0 % │        0.0 % │        0.0 % │     0.0 % │
│    WMT │       0.427 % │         0.0 % │      0.0 % │     15.848 % │     15.186 % │   10.63 % │
│    XOM │         0.0 % │       3.635 % │      0.0 % │      5.852 % │       5.87 % │   3.153 % │
└────────┴───────────────┴───────────────┴────────────┴──────────────┴──────────────┴───────────┘

The composition plot contrasts the negative skew-minimising portfolios across the three priors.

plot_stacked_bar_composition([r for (_, r) in ress_sk], rd;                             xticks = (1:length(ress_sk), [k for (k, _) in ress_sk]))
Example block output

The composition plot contrasts the kurtosis-minimising portfolios across the three priors.

plot_stacked_bar_composition([r for (_, r) in ress_kt], rd;                             xticks = (1:length(ress_kt), [k for (k, _) in ress_kt]))
Example block output

This page was generated using Literate.jl.