Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ bin

# uv
uv.lock

# Local generated artifacts
weights.csv
7 changes: 4 additions & 3 deletions example/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def deviation_risk_parity(w, cov_matrix):

# Black-Litterman
spy_prices = pd.read_csv(
"tests/resources/spy_prices.csv", parse_dates=True, index_col=0, squeeze=True
)
"tests/resources/spy_prices.csv", parse_dates=True, index_col=0
).squeeze()
delta = black_litterman.market_implied_risk_aversion(spy_prices)

mcaps = {
Expand Down Expand Up @@ -147,7 +147,8 @@ def deviation_risk_parity(w, cov_matrix):


# Crticial Line Algorithm
cla = CLA(mu, S)
# Set use_cvxcla=True to use the optional high-performance cvxcla backend.
cla = CLA(mu, S, use_cvxcla=True)
print(cla.max_sharpe())
cla.portfolio_performance(verbose=True)
plotting.plot_efficient_frontier(cla) # to plot
Expand Down
57 changes: 54 additions & 3 deletions pypfopt/cla.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
by Marcos Lopez de Prado and David Bailey.
"""

import warnings

import numpy as np
import pandas as pd

Expand Down Expand Up @@ -47,7 +49,9 @@ class CLA(BaseOptimizer):
- ``save_weights_to_file()`` saves the weights to csv, json, or txt.
"""

def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1)):
def __init__(
self, expected_returns, cov_matrix, weight_bounds=(0, 1), use_cvxcla=False
):
"""
Parameters
----------
Comment thread
ayushraj09 marked this conversation as resolved.
Expand All @@ -59,6 +63,9 @@ def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1)):
weight_bounds : tuple (float, float) or (list/ndarray, list/ndarray) or list(tuple(float, float))
minimum and maximum weight of an asset, defaults to (0, 1).
Must be changed to (-1, 1) for portfolios with shorting.
use_cvxcla : bool, optional
whether to use the ``cvxcla`` library as a high-performance backend,
defaults to False. Requires ``cvxcla`` to be installed

Raises
------
Expand Down Expand Up @@ -103,6 +110,31 @@ def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1)):
tickers = list(range(len(self.mean)))
super().__init__(len(tickers), tickers)

# Optional cvxcla backend
self.use_cvxcla = False
self._cvxcla_engine = None
if use_cvxcla:
try:
from cvxcla import CLA as _CVXCLAEngine

n = self.n_assets
self._cvxcla_engine = _CVXCLAEngine(
mean=self.expected_returns,
covariance=self.cov_matrix,
lower_bounds=self.lB.flatten(),
upper_bounds=self.uB.flatten(),
a=np.ones((1, n)),
b=np.ones(1),
)
self.use_cvxcla = True
except ImportError:
warnings.warn(
"cvxcla is not installed – falling back to the standard CLA "
"implementation. Install it with: pip install cvxcla",
RuntimeWarning,
stacklevel=2,
)

@staticmethod
def _infnone(x):
"""
Expand Down Expand Up @@ -159,7 +191,7 @@ def _compute_w(self, covarF_inv, covarFB, meanF, wB):
w1 = np.dot(g4, wB)
g4 = np.dot(onesF.T, w1)
g = -self.ls[-1] * g1 / g2 + (1 - g3 + g4) / g2
g = float(g[0, 0])
g = float(g.item() if hasattr(g, "item") else g)
# 2) compute weights
w2 = np.dot(covarF_inv, onesF)
w3 = np.dot(covarF_inv, meanF)
Expand Down Expand Up @@ -189,7 +221,7 @@ def _compute_lambda(self, covarF_inv, covarFB, meanF, wB, i, bi):
l3 = np.dot(l2, wB)
l2 = np.dot(onesF.T, l3)
res = ((1 - l1 + l2) * c4[i] - c1 * (bi + l3[i])) / c
res = float(res[0, 0])
res = float(res.item() if hasattr(res, "item") else res)
return res, bi

def _get_matrices(self, f):
Expand Down Expand Up @@ -406,6 +438,11 @@ def max_sharpe(self):
OrderedDict
asset weights for the max-sharpe portfolio
"""
if self.use_cvxcla:
_, weights = self._cvxcla_engine.frontier.max_sharpe
self.weights = np.asarray(weights, dtype=float)
return self._make_output_weights()

Comment thread
ayushraj09 marked this conversation as resolved.
if not self.w:
self._solve()
# 1) Compute the local max SR portfolio between any two neighbor turning points
Expand All @@ -430,6 +467,12 @@ def min_volatility(self):
OrderedDict
asset weights for the volatility-minimising portfolio
"""
if self.use_cvxcla:
# Last turning point on the frontier is the minimum variance portfolio
weights = self._cvxcla_engine.frontier.weights[-1]
self.weights = np.asarray(weights, dtype=float)
return self._make_output_weights()

if not self.w:
self._solve()
var = []
Expand Down Expand Up @@ -459,6 +502,14 @@ def efficient_frontier(self, points=100):
(float list, float list, np.ndarray list)
return list, std list, weight list
"""
if self.use_cvxcla:
frontier = self._cvxcla_engine.frontier.interpolate(points)
mu = list(frontier.returns)
sigma = list(frontier.volatility)
weights = [np.asarray(w, dtype=float) for w in frontier.weights]
self.frontier_values = (mu, sigma, weights)
return mu, sigma, weights

if not self.w:
self._solve()

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ all_extras = [
"ecos>=2.0.14,<2.1",
"plotly>=5.0.0,<7",
"cvxopt; python_version < '3.14'",
"cvxcla>=1.6.2; python_version >= '3.11'",
]

# dev - the developer dependency set, for contributors and CI
Expand Down
59 changes: 59 additions & 0 deletions tests/test_cla.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,62 @@ def test_cla_efficient_frontier():
# higher return = higher risk
assert sigma[-1] < sigma[0] and mu[-1] < mu[0]
assert weights[0].shape == (20, 1)


def test_cla_cvxcla_fallback_warning():
"""cvxcla unavailable → RuntimeWarning, falls back to standard backend."""
import sys

original = sys.modules.get("cvxcla")
sys.modules["cvxcla"] = None
try:
with pytest.warns(RuntimeWarning, match="cvxcla is not installed"):
cla = setup_cla(use_cvxcla=True)
assert cla.use_cvxcla is False
finally:
if original is None:
sys.modules.pop("cvxcla", None)
else:
sys.modules["cvxcla"] = original


def test_cla_cvxcla_max_sharpe():
"""cvxcla backend max_sharpe matches standard implementation."""
pytest.importorskip("cvxcla")
cla = setup_cla(use_cvxcla=True)
assert cla.use_cvxcla is True
w = cla.max_sharpe()
assert isinstance(w, dict)
assert set(w.keys()) == set(cla.tickers)
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(risk_free_rate=0.02),
(0.2994470912768992, 0.21764331657015668, 1.283968171780824),
rtol=1e-4,
)


def test_cla_cvxcla_min_volatility():
"""cvxcla backend min_volatility matches standard implementation."""
pytest.importorskip("cvxcla")
cla = setup_cla(use_cvxcla=True)
w = cla.min_volatility()
assert isinstance(w, dict)
assert set(w.keys()) == set(cla.tickers)
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(risk_free_rate=0.02),
(0.1505682139948257, 0.15915084514118688, 0.8204054077060994),
rtol=1e-4,
)


def test_cla_cvxcla_efficient_frontier():
"""cvxcla backend efficient_frontier sets frontier_values and returns valid lists."""
pytest.importorskip("cvxcla")
cla = setup_cla(use_cvxcla=True)
mu, sigma, weights = cla.efficient_frontier(points=50)
assert len(mu) > 0
assert len(sigma) == len(mu)
assert len(weights) == len(mu)
assert cla.frontier_values is not None