Skip to main content

Critical line algorithm for the efficient frontier

Project description

📈 cvxcla - Critical Line Algorithm for Portfolio Optimization

PyPI version License: MIT Downloads Coverage


Quick Links: 📖 Documentation🐛 Report Bug💡 Request Feature💬 Discussions


📋 Overview

cvxcla is a Python package that implements the Critical Line Algorithm (CLA) for portfolio optimization. The CLA efficiently computes the entire efficient frontier for portfolio optimization problems with linear constraints and bounds on the weights.

The Critical Line Algorithm was introduced by Harry Markowitz in The Optimization of Quadratic Functions Subject to Linear Constraints and further described in his book Portfolio Selection.

The algorithm is based on the observation that the efficient frontier is a piecewise linear function when expected return is plotted against expected variance. The CLA computes the turning points (corners) of the efficient frontier, allowing for efficient representation of the entire frontier.

I gave the plenary talk at EQD's Singapore conference.

🧮 Why the Algorithm Works

The Markowitz problem is a quadratic program parametrized by a return target λ:

min  wᵀΣw - λ · μᵀw
s.t. Aw = b,  lb ≤ w ≤ ub

As λ sweeps from ∞ (maximize return) down to 0 (minimize variance), the solution traces the entire efficient frontier. The key insight is that between consecutive events, the optimal weights are a linear function of λ:

w(λ) = α + λ · β

This holds because the KKT optimality conditions are linear in λ whenever the active set — which assets sit at their bounds — is fixed. The algorithm exploits this in three steps:

  1. Start at λ = ∞, where the portfolio concentrates on the highest-return asset within bounds (init_algo, called from _first_turning_point).

  2. Solve the KKT system for the current active set to find α and β (_solve), then decrease λ until one of two events occurs (main loop):

    • a free asset hits its bound (leaves the free set), or
    • a blocked asset's KKT multiplier changes sign (enters the free set).
  3. Update the active set (exactly one asset changes status) and repeat until λ ≤ 0.

Because only one asset changes per step and each step requires only a single linear solve, the algorithm traces the full frontier cheaply and exactly — no approximation needed.

✨ Features

  • Efficient computation of the entire efficient frontier
  • Support for linear constraints and bounds on portfolio weights
  • Multiple implementations based on different approaches from the literature
  • Visualization of the efficient frontier using Plotly
  • Computation of the maximum Sharpe ratio portfolio
  • Fully tested and documented codebase

🚀 Installation

Using pip

pip install cvxcla

To include plotting support (Plotly and Kaleido):

pip install cvxcla[plot]

Development Setup

To set up a development environment:

  1. Clone the repository:

    git clone https://github.com/cvxgrp/cvxcla.git
    cd cvxcla
    
  2. Create a virtual environment and install dependencies:

    make install
    

This will:

  • Install the uv package manager
  • Create a Python 3.12 virtual environment
  • Install all dependencies from pyproject.toml

🔧 Usage

Here's a simple example of how to use cvxcla to compute the efficient frontier:

import numpy as np
# Set a seed for reproducibility
np.random.seed(42)
from cvxcla import CLA

# Define your portfolio problem
n = 10  # Number of assets
mean = np.random.randn(n)  # Expected returns
cov = np.random.randn(n, n)
covariance = cov @ cov.T  # Covariance matrix
lower_bounds = np.zeros(n)  # No short selling
upper_bounds = np.ones(n)  # No leverage

# Create a CLA instance
cla = CLA(
    mean = mean,
    covariance = covariance,
    lower_bounds = lower_bounds,
    upper_bounds = upper_bounds,
    a = np.ones((1, n)),  # Fully invested constraint
    b = np.ones(1)
)

# Access the efficient frontier
frontier = cla.frontier

# Get the maximum Sharpe ratio portfolio
max_sharpe_ratio, max_sharpe_weights = frontier.max_sharpe
print(f"Maximum Sharpe ratio: {max_sharpe_ratio:.6f}")
# Print first few weights to avoid long output
print(f"First 3 weights: {max_sharpe_weights[:3]}")
Maximum Sharpe ratio: 2.946979
First 3 weights: [0.         0.         0.08509841]

For visualization, you can plot the efficient frontier:

# Plot the efficient frontier
fig = frontier.plot(volatility=True)
fig.show()

📚 Literature and Implementations

The package includes implementations based on several key papers:

📝 Niedermayer and Niedermayer

They suggested a method to avoid the expensive inversion of the covariance matrix in Applying Markowitz's critical line algorithm. Our testing shows that in Python, this approach is not significantly faster than explicit matrix inversion using LAPACK via numpy.linalg.inv.

📝 Bailey and Lopez de Prado

We initially started with their code published in An Open-Source Implementation of the Critical-Line Algorithm for Portfolio Optimization. We've made several improvements:

  • Using boolean numpy arrays to indicate whether a weight is free or blocked
  • Rewriting the computation of the first turning point
  • Isolating the computation of λ and weight updates to make them testable
  • Using modern and immutable dataclasses throughout

Our updated implementation is included in the tests but not part of cvxcla package. We use it to verify our results and include it for educational purposes.

📝 Markowitz et al

In Avoiding the Downside: A Practical Review of the Critical Line Algorithm for Mean-Semivariance Portfolio Optimization, Markowitz and researchers from Hudson Bay Capital Management and Constantia Capital present a step-by-step tutorial.

We address a problem they overlooked: after finding the first starting point, all variables might be blocked. We enforce that one variable labeled as free (even if it sits on a boundary) to avoid a singular matrix.

Rather than using their sparse matrix construction, we bisect the weights into free and blocked parts and use a linear solver for the free part only.

🧪 Testing

Run the test suite with:

make test

🧹 Code Quality

Format and lint the code with:

make fmt

📖 Documentation

👥 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. Run the tests to make sure everything works (make test)
  4. Format your code (make fmt)
  5. Commit your changes (git commit -m 'Add some amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

📄 License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

🔍 Related Projects

  • PyCLA by Philipp Schiele - A previous implementation of the Critical Line Algorithm in Python.

  • CLA by Martin Dengler - The original implementation by David Bailey and Marcos Lopez de Prado.

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

cvxcla-1.6.2.tar.gz (227.5 kB view details)

Uploaded Source

Built Distribution

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

cvxcla-1.6.2-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file cvxcla-1.6.2.tar.gz.

File metadata

  • Download URL: cvxcla-1.6.2.tar.gz
  • Upload date:
  • Size: 227.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cvxcla-1.6.2.tar.gz
Algorithm Hash digest
SHA256 8c3523a2d91bf3e202c80b00737c4f798d865a18c89fbb7dcd0b01eea3a692f9
MD5 1e93a74761cc7729377970ef179d417c
BLAKE2b-256 0289c45818a8dddda9f7e66bb5165bb7b86f706370b8c600deed79c1d89e4e17

See more details on using hashes here.

Provenance

The following attestation bundles were made for cvxcla-1.6.2.tar.gz:

Publisher: release.yml on cvxgrp/cvxcla

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cvxcla-1.6.2-py3-none-any.whl.

File metadata

  • Download URL: cvxcla-1.6.2-py3-none-any.whl
  • Upload date:
  • Size: 16.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cvxcla-1.6.2-py3-none-any.whl
Algorithm Hash digest
SHA256 c271cfa380da7e7d915fb97b06de7a99e59f3f39cf899f9a7303b4cb3841293c
MD5 1c7c31aba0ffaf92831ded2584466f23
BLAKE2b-256 c303e80f23ba0d81c72f6016ce19ac3d46b47b4c7537d677b9aab13ef14d9a57

See more details on using hashes here.

Provenance

The following attestation bundles were made for cvxcla-1.6.2-py3-none-any.whl:

Publisher: release.yml on cvxgrp/cvxcla

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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