Unbiased Portfolio Shrinkage Approach (UPSA) with LOO or k-fold CV
Project description
Universal Portfolio Shrinkage (UPSA)
A Python implementation of the Universal Portfolio Shrinkage Approximator (UPSA), a flexible spectral shrinkage method that directly optimizes out-of-sample portfolio performance, as introduced in:
Kelly, Bryan T., Semyon Malamud, Mohammad Pourmohammadi, and Fabio Trojani. Universal portfolio shrinkage. No. w32004. National Bureau of Economic Research, 2024.
Motivation
Classical Markowitz portfolios suffer from severe estimation noise when the number of assets or factors (N) is large relative to sample size (T), leading to large gaps between in-sample and out-of-sample performance . Traditional shrinkage methods impose restrictive forms or optimize statistical proxies rather than the portfolio objective, limiting efficacy. UPSA overcomes these limitations by providing a universal spectral approximator for nonlinear shrinkage functions and tuning shrinkage directly on expected out-of-sample performance via cross-validation.
Key Features
- Universal spectral approximation: Represents a broad class of nonlinear shrinkage functions as a positive linear combination of basic ridge shrinkages, based on a Stone–Weierstrass argument (Lemma 1).
- Objective-aligned tuning: Chooses shrinkage weights by maximizing expected out-of-sample quadratic utility using leave-one-out or k-fold CV (Lemma 2).
- Efficient closed-form computation: Employs spectral formulas to compute LOO estimators for ridge portfolios without refitting eigen-decomposition for each leave-out.
- Constraint support: Optional nonnegativity and sum-to-one enforcement on UPSA ensemble weights for economic interpretability.
- Flexible CV interface: Single
cv_methodparameter accepts'loo'or integer >1 for k-fold CV. - Scalable: Handles high-dimensional settings (N ≫ T or T ≫ N) via efficient eigen-decomposition routines.
- Empirical robustness: Outperforms ridge, Ledoit–Wolf, PCA-based, and benchmark factor models in anomaly portfolio tests, achieving higher Sharpe and lower pricing errors.
Installation
Requires Python 3.7+ and dependencies: numpy, scikit-learn, cvxopt, pandas (optional).
# Install from PyPI
to=python
pip install universal-upsa
# Or install development version
git clone https://github.com/yourusername/universal-upsa.git
cd universal-upsa
pip install -e .
Quickstart
import numpy as np
import pandas as pd
from upsa.upsa import UPSA
# 1) Load returns (T×P) as DataFrame or ndarray
returns_df = pd.read_csv("returns.csv", index_col="date")
returns = returns_df.values
# 2) Define shrinkage grid (e.g., logspace spanning empirical eigenvalues)
z_list = np.logspace(-4, 2, 20)
# 3a) Fit with leave-one-out CV (default)
model_loo = UPSA(z_list=z_list).fit(returns, cv_method='loo', constraint=False)
w_loo = model_loo.get_upsa_weights()
# 3b) Or fit with 5-fold CV for larger T
model_kf = UPSA(z_list=z_list).fit(returns, cv_method=5, constraint=False)
w_kf = model_kf.get_upsa_weights()
# 4) Apply out-of-sample: compute portfolio returns
oos_df = pd.read_csv("oos_returns.csv", index_col="date")
oos = oos_df.values
port_ret = oos @ w_loo
# 5) Annualized Sharpe ratio
sharpe = np.sqrt(12) * port_ret.mean() / port_ret.std()
print(f"Annualized Sharpe: {sharpe:.3f}")
To enforce economic constraints:
model_c = UPSA(z_list=z_list).fit(returns, cv_method='loo', constraint=True)
w_c = model_c.get_upsa_weights()
API Reference
-
UPSA(z_list: np.ndarray = None): Initialize with optional shrinkage grid. IfNone, defaults to log-spaced grid spanning smallest to largest empirical eigenvalue fileciteturn3file12. -
.fit(returns, cv_method='loo', constraint=False): Fit UPSA model.returns: T×P array or DataFrame.cv_method:'loo'or integer >1 for k-fold.constraint: ifTrue, enforce nonnegativity and sum-to-one on UPSA weights. Returns fitted instance with attributesbest_z,upsa, andeff_port(matrix of ridge portfolios).
-
.get_upsa_weights() -> np.ndarray: Returns UPSA portfolio weight vector (length P). -
.get_ridge_weights() -> np.ndarray: Returns ridge portfolio weights vector (length P).
Empirical Evidence Summary
On 153 anomaly portfolios from Jensen et al. (1971–2022), UPSA achieves out-of-sample Sharpe ≈ 1.92 vs. 1.59 for best ridge, 1.31 for Ledoit–Wolf, and 1.45 for PCA-based portfolios; Cross sectional R² ≈ 66% vs. 39% for ridge. Robustness holds across subsamples and additional regularization scenarios.
Testing
Run basic sanity tests with pytest:
pip install pytest
pytest tests/test_upsa.py
Citation
Please cite the paper when using UPSA:
@article{kelly2025universal,
title={Universal portfolio shrinkage},
author={Kelly, Bryan T and Malamud, Semyon and Pourmohammadi, Mohammad and Trojani, Fabio},
year={2024},
institution={National Bureau of Economic Research}
}
License
Distributed under the MIT License. See LICENSE for details.
Contributing
Contributions welcome! Please open issues or pull requests on GitHub: https://github.com/yourusername/universal-upsa.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file universal_upsa-0.1.3.tar.gz.
File metadata
- Download URL: universal_upsa-0.1.3.tar.gz
- Upload date:
- Size: 7.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7432d2e25e4c16cbb7b6fd160b4be88c87bdbda852d107ce33303db51c00ada9
|
|
| MD5 |
1a42103809b07fa1cd8790f116bbb275
|
|
| BLAKE2b-256 |
9a36a08c9687352064d48d6c57f8ebaacc98c2910df6fb1c35914df3b25d7366
|
Provenance
The following attestation bundles were made for universal_upsa-0.1.3.tar.gz:
Publisher:
publish.yml on pourmohammadimohammad/Universal_Portfolio_Shrinkage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
universal_upsa-0.1.3.tar.gz -
Subject digest:
7432d2e25e4c16cbb7b6fd160b4be88c87bdbda852d107ce33303db51c00ada9 - Sigstore transparency entry: 539929766
- Sigstore integration time:
-
Permalink:
pourmohammadimohammad/Universal_Portfolio_Shrinkage@754ebb607b79418882f547b9d9594b60e61c8baa -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/pourmohammadimohammad
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@754ebb607b79418882f547b9d9594b60e61c8baa -
Trigger Event:
push
-
Statement type:
File details
Details for the file universal_upsa-0.1.3-py3-none-any.whl.
File metadata
- Download URL: universal_upsa-0.1.3-py3-none-any.whl
- Upload date:
- Size: 7.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a7ede6e97e8593de9f4c36ab38705cdb856dc1aa806821295b0eae877f924c9
|
|
| MD5 |
13efefb60a48710c3fd066cbd972fee8
|
|
| BLAKE2b-256 |
397ec96b8dd3909114d382d6a7938a545b985ff29aa6fc1b9d5ad07961130fa3
|
Provenance
The following attestation bundles were made for universal_upsa-0.1.3-py3-none-any.whl:
Publisher:
publish.yml on pourmohammadimohammad/Universal_Portfolio_Shrinkage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
universal_upsa-0.1.3-py3-none-any.whl -
Subject digest:
6a7ede6e97e8593de9f4c36ab38705cdb856dc1aa806821295b0eae877f924c9 - Sigstore transparency entry: 539929870
- Sigstore integration time:
-
Permalink:
pourmohammadimohammad/Universal_Portfolio_Shrinkage@754ebb607b79418882f547b9d9594b60e61c8baa -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/pourmohammadimohammad
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@754ebb607b79418882f547b9d9594b60e61c8baa -
Trigger Event:
push
-
Statement type: