Repository navigation
Feature/qp long only min variance - #11
Merged
Merged
Conversation
Owner
Author
Code reviewI also ran a random probe: 3,000 singular cases (fewer rows than assets) plus 600 cases with up to 62 assets. Every solve returned without error and met the KKT conditions. So most of the points below are edge cases or cleanup, not observed failures.
|
Owner
Author
|
Triage of the review above: Fixing in this PR (1–5)
Not in this PR
|
This was referenced Oct 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds long-only minimum variance for 0.2.4, solved by a small active-set QP solver written for cutup, so no crate dependency is added. It also fixes an SVD tolerance bug that could make min variance, mean-variance and max Sharpe fail on well-conditioned input.
Long-only min variance
long_only_min_variance_allocation()andlong_only_min_variance_allocation_with_config(&CovarianceConfig)minimizewᵀΣwsubject to1ᵀw = 1andw ≥ 0. When the unconstrained portfolio has no short positions, the two agree. Shrinkage and regularization work as they do for the other methods.These are new methods rather than a flag on
min_variance_allocation_with_config, because that method already takes aCovarianceConfig, and changing its argument would break the API. The 0.2.5 long-only methods can use the samelong_only_prefix.When Σ is singular, several long-only portfolios can share the minimum variance. Which one is returned depends on the solver's path, and the docs point to shrinkage for a unique answer.
Solver (
src/qp.rs)solve_nonnegativesolvesmin ½xᵀQx − cᵀxsubject toEᵀx = bandx ≥ 0for a positive semidefiniteQ, starting from a feasible point. It is a primal active-set method (Nocedal and Wright, Algorithm 16.3):lib.rs, and it now also returns the multipliersλ.ν = Qx − c + Eλis released. When none is negative, the KKT conditions hold andxis optimal.Q. When the free problem has no minimum, the solver follows a zero-curvature direction that lowers the objective, which is the projection ofconto the null space of[Q; Eᵀ]. If nothing blocks that direction, it returnsUnbounded.xmoving, isn't released again untilxmoves.NotConverged.Min variance uses only
c = 0with the budget constraint. The general form, withc, several equalities and the unbounded case, is there for 0.2.5's long-only mean-variance, max diversification and max Sharpe, and the solver's own unit tests exercise all of it.Fix: inaccurate SVD
solve_kktand max Sharpe called nalgebra'stry_svdwith a convergence tolerance of exactlyf64::EPSILON. On some matrices this returns singular vectors that don't reconstruct the input: on one heavily shrunk covariance the reconstruction was off by 1e-2. The KKT residual check then rejected the solve, and min variance returnedSingularCovarianceon a covariance close to a multiple ofI. A stress run on 200k random KKT matrices found 31 with errors up to 2e-3.5ε. On the same 200k matrices, the worst reconstruction error drops to 1e-10.solve_kktadds two rounds of iterative refinement against the exact system, which removes errors of that size.Neither change alone is enough: with an error of 1e-2, refinement doesn't converge.
test_min_variance_survives_inaccurate_svdfails on the old code and passes now. Upgrading nalgebra would break the API, because nalgebra types are part of cutup's public API.Tests
Reference fixtures grow from 126 to 138 cases, and all pass. The drift check reports 0 of 138 cases drifted.
two_blocks5,prices10x60andnear_singular4.Unit tests:
qp.rs:Qwith no descent direction stays feasible.lib.rs:[1, 0];Property tests add two properties:
(Σw)ᵢ ≥ wᵀΣwfor every asset, with equality wherewᵢ > 0.Long-only min variance also joins the existing properties for summing to 1, never shorting, column order and return scale.
The "weights sum to 1" property now measures its tolerance relative to the largest weight. Two nearly equal expected returns give mean-variance weights of ±3500, and those tripped the old absolute 1e-9.
At 50,000 cases everything passes except an occasional failure in
max_sharpe_ignores_column_order_and_joint_scale: 7e-9 against a 4.8e-9 tolerance on ill-conditioned draws. It was failing before this PR, and the PR doesn't change that test.Toolchains: stable and MSRV 1.78 both pass. Clippy, docs and fmt are clean.
Docs
reference/README.md.examples/compare.rsnow prints long-only min variance.