Skip to content

cvxrisk: Convex Optimization for Portfolio Risk Management

PyPI version Apache 2.0 License Downloads Renovate enabled Coverage

📋 Overview

cvxrisk is a Python library for portfolio risk management using convex optimization. It provides a flexible framework for implementing various risk models and solves optimization problems directly with the Clarabel conic solver — no cvxpy required.

The library is built around an abstract Model class that standardizes the interface for different risk models, making it easy to swap between them in your optimization problems.

🚀 Installation

# Install from PyPI
pip install cvxrisk

# For development installation
git clone https://github.com/cvxgrp/cvxrisk.git
cd cvxrisk
make install

# For experimenting with the notebooks (after cloning)
make marimo

🔧 Quick Start

cvxrisk makes it easy to formulate and solve portfolio optimization problems:

import numpy as np
from cvx.risk.sample import SampleCovariance
from cvx.risk.portfolio import minrisk_problem
from cvx.core import Variable

# Create a risk model
riskmodel = SampleCovariance(num=2)

# Update the model with data
riskmodel.update(
    cov = np.array([[1.0, 0.5], [0.5, 2.0]]),
    lower_assets = np.zeros(2),
    upper_assets = np.ones(2)
)

# Define portfolio weights variable
weights = Variable(2)

# Create and solve the optimization problem
problem = minrisk_problem(riskmodel, weights)
problem.solve()

print(problem.status)
print(np.array2string(np.round(weights.value, 2), separator=" "))
Solved
[0.75 0.25]

📊 Features

cvxrisk provides several risk models:

Sample Covariance

The simplest risk model based on the sample covariance matrix:

from cvx.risk.sample import SampleCovariance
import numpy as np

riskmodel = SampleCovariance(num=2)
riskmodel.update(
    cov=np.array([[1.0, 0.5], [0.5, 2.0]]),
    lower_assets=np.zeros(2),
    upper_assets=np.ones(2),
)
# Reconstruct covariance from Cholesky factor and print
cov_est = riskmodel.parameter["chol"].value.T @ riskmodel.parameter["chol"].value
print(np.array2string(cov_est, precision=1))
[[1.  0.5]
 [0.5 2. ]]

Factor Risk Models

Factor models reduce dimensionality by projecting asset returns onto a smaller set of factors:

import numpy as np
from cvx.risk.factor import FactorModel
from cvx.linalg import pca

# Create some sample returns data
a = 100
m = 25
returns = np.random.randn(a, m)

# Compute principal components (deterministic using a fixed seed for reproducibility)
np.random.seed(0)
factors = pca(returns, n_components=10)

# Create and update the factor model
model = FactorModel(assets=m, k=10)
model.update(
    cov=factors.cov,
    exposure=factors.exposure,
    idiosyncratic_risk=factors.idiosyncratic.std(axis=0, ddof=1),
    lower_assets=np.zeros(m),
    upper_assets=np.ones(m),
    lower_factors=-0.1*np.ones(10),
    upper_factors=0.1*np.ones(10),
)

# Verify the model has the correct dimensions
print(model.parameter["exposure"].value.shape)
(10, 25)

Factor risk models use the projection of the weight vector into a lower dimensional subspace, e.g. each asset is the linear combination of $k$ factors.

$$r_i = \sum_{j=1}^k f_j \beta_{ji} + \epsilon_i$$

The factor time series are $f_1, \ldots, f_k$. The loadings are the coefficients $\beta_{ji}$. The residual returns $\epsilon_i$ are assumed to be uncorrelated with the factors.

Any position $w$ in weight space projects to a position $y = \beta^T w$ in factor space. The variance for a position $w$ is the sum of the variance of the systematic returns explained by the factors and the variance of the idiosyncratic returns.

$$Var® = Var(\beta^T w) + Var(\epsilon w)$$

We assume the residual returns are uncorrelated and hence

$$Var® = y^T \Sigma_f y + \sum_i w_i^2 Var(\epsilon_i)$$

where $\Sigma_f$ is the covariance matrix of the factors and $Var(\epsilon_i)$ is the variance of the idiosyncratic returns.

Conditional Value at Risk (CVaR)

CVaR measures the expected loss in the worst-case scenarios:

import numpy as np
from cvx.risk.cvar import CVar

# Create some sample historical returns (deterministic)
np.random.seed(0)
historical_returns = np.random.randn(50, 14)

# Create and update the CVaR model
model = CVar(alpha=0.95, n=50, m=14)
model.update(
    returns=historical_returns,
    lower_assets=np.zeros(14),
    upper_assets=np.ones(14),
)

# Verify the model parameters
print(model.alpha)
print(model.parameter["R"].value.shape)
0.95
(50, 14)

📚 Documentation

For more detailed documentation and examples, visit our documentation site.

🛠️ Development

cvxrisk uses modern Python development tools:

# Install development dependencies
make install

# Run tests
make test

# Format code
make fmt

# Start interactive notebooks
make marimo

📄 License

cvxrisk is licensed under the Apache License 2.0. See LICENSE for details.

👥 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

For more information, see CONTRIBUTING.md.