Skip to main content

Second-order portfolio optimizer for mean-variance-skewness-kurtosis (MVSK) via Yau's Affine-Normal Descent

Project description

YAND-MVSK: Portfolio Optimization with Tail Risk Control

Asset allocation that sees beyond mean and variance. Optimizes return, volatility, skewness, and kurtosis in one shot.

Markowitz gave us mean-variance. But financial returns have fat tails and asymmetry: the 2008 crash, the COVID drop, the meme stock spikes. YAND-MVSK optimizes across all four statistical moments, so your portfolio accounts for tail risk that traditional optimizers ignore. It solves in 5-10 iterations even for 800+ assets.

YAND-MVSK convergence and performance

Who is this for?

  • Quant researchers building factor portfolios or smart-beta strategies that control for higher moments
  • Risk managers who want to penalize negative skewness (crash exposure) and excess kurtosis (tail risk)
  • Asset allocators optimizing across ETFs, stocks, or multi-asset universes where return distributions are non-Gaussian
  • Academic finance: reproducible implementation of a state-of-the-art MVSK algorithm for benchmarking

Why higher moments?

Mean-variance optimization assumes returns are Gaussian. Real markets aren't. Equities exhibit negative skewness (crashes are sharper than rallies) and excess kurtosis (extreme moves happen more than a normal distribution predicts). Ignoring these moments means your "optimal" portfolio is optimized for a world that doesn't exist.

YAND-MVSK lets you express preferences over all four moments in a single convex optimization: maximize return, minimize variance, maximize skewness (prefer upside), minimize kurtosis (avoid tail events).

Quickstart

uv add yand-mvsk
import numpy as np
from yand_mvsk import yand_mvsk_solve, crra_coefficients

# Your return matrix: T observations x n assets
R = np.random.default_rng(42).standard_normal((504, 50)) * 0.02

# Solve: 3 lines, done
result = yand_mvsk_solve(R, crra_coefficients(gamma=6))

print(result.x[:5])       # portfolio weights
print(result.converged)    # True
print(result.n_iter)       # typically 5-10

How it works

The solver never builds O(n³) coskewness tensors or O(n⁴) cokurtosis tensors. Instead, it stores only the T×n return matrix and computes everything through matrix-vector products:

Operation Cost What it replaces
Gradient O(Tn) Would need O(n³) with explicit tensors
Hessian-vector product O(Tn) Would need O(n⁴) with explicit tensors
Quartic line search O(Tn) Exact minimization of a degree-4 polynomial

This means n=800 solves in 0.05s on a laptop.

API

yand_mvsk_solve(R, c, **kwargs) → MVSKResult

Parameter Type Default Description
R (T, n) array required Return matrix
c (4,) array required Preference weights [c₁, c₂, c₃, c₄]
x0 (n,) array equal-weight Initial portfolio
tau float 1e-8 Lower bound on each weight
tol float 1e-6 KKT convergence tolerance
max_iter int 300 Iteration budget
line_search str 'quartic' 'quartic' (exact) or 'armijo'
use_pcg bool False Use conjugate gradients for large problems
verbose bool False Print per-iteration diagnostics

crra_coefficients(gamma) → array

Returns CRRA preference coefficients for risk aversion γ:

c = (1, γ/2, γ(γ+1)/6, γ(γ+1)(γ+2)/24)

check_convexity(c) → bool

Checks sufficient convexity condition: c₄ > 0 and 8·c₂·c₄ > 3·c₃².

MVSKResult

Field Type Description
x ndarray Optimal portfolio weights
f_val float Objective value
kkt_residual float First-order optimality measure
n_iter int Iterations used
converged bool Whether tolerance was reached
history list[float] Per-iteration objective values

Performance

Tested on synthetic benchmarks (T=252 daily observations, CRRA γ=6). Compared against scipy.optimize.minimize (SLSQP) solving the same MVSK objective:

Assets (n) YAND iters YAND time scipy SLSQP time Speedup
20 5 0.006s 0.017s
100 7 0.014s 0.12s
200 8 0.025s 0.61s 24×
800 7 0.52s 28.3s 54×

YAND also finds better optima — the second-order descent with exact quartic line search reaches lower objective values than SLSQP at every scale.

Acknowledgement

This is an independent Python implementation of the YAND-MVSK algorithm by Wang, Niu, Sheshmani, and Yau, not affiliated with or endorsed by the original authors. All credit for the algorithm design, theoretical analysis, and convergence guarantees belongs to them. The affine-normal descent framework originates from the geometric work of Cheng, Yau, and collaborators, later developed into the YAND optimization framework by Niu et al.

Paper: arXiv:2604.25378 | Prior MATLAB implementation by the authors: MVSK-Multi

If you use this code in research, please cite the original paper and this implementation:

@article{wang2026yandmvsk,
  title={YAND-MVSK: Yau's Affine-Normal Descent for Large-Scale Unrestricted Mean-Variance-Skewness-Kurtosis Portfolio Optimization},
  author={Wang, Ya-Juan and Niu, Yi-Shuai and Sheshmani, Artan and Yau, Shing-Tung},
  journal={arXiv preprint arXiv:2604.25378},
  year={2026}
}

@software{wu2026yandmvsk,
  title={yand-mvsk: Python implementation of YAND-MVSK portfolio optimization},
  author={Wu, Wenbin},
  url={https://github.com/dthinkr/yand-mvsk},
  year={2026}
}

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

yand_mvsk-0.1.0.tar.gz (9.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

yand_mvsk-0.1.0-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

File details

Details for the file yand_mvsk-0.1.0.tar.gz.

File metadata

  • Download URL: yand_mvsk-0.1.0.tar.gz
  • Upload date:
  • Size: 9.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for yand_mvsk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 39103f76cbc641b0b2ce4449741f46fce7e22d0e5c1b4bd7134aee67661d5229
MD5 0ec29cf2e8e37856ba26c2542fdf5318
BLAKE2b-256 9e415b0bf53b1e936471e89bf193e8b7df42787d0b9178a22d3202002d3659ac

See more details on using hashes here.

File details

Details for the file yand_mvsk-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: yand_mvsk-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 10.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for yand_mvsk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c59f96c9b59026169add998f1fa78935c93dda4b94e4ab779ecf3d5e7774cf6e
MD5 49c8c5dee82153a6809a7d08598e7360
BLAKE2b-256 adc732d99fa8df283c5528d5b193ffce7efa6ee020aa0584e9892f3db24f646f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page