Skip to content

Commit 9e96ff2

Browse files
authored
Remove cvxpy to compute the first point (#296)
* introduce the cla function * obsolete empty cell? * remove cvxpy * remove the cvxpy construct * notebook updated * linting
1 parent e3a95ca commit 9e96ff2

File tree

7 files changed

+25
-261
lines changed

7 files changed

+25
-261
lines changed

book/marimo/cla.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
# dependencies = [
44
# "marimo==0.14.13",
55
# "numpy==2.3.0",
6-
# "cvxcla==1.3.1"
6+
# "cvxcla==1.3.2"
77
# ]
88
# ///
99
"""Little demo for the Critical Line Algorithm."""
1010

1111
import marimo
1212

13-
__generated_with = "0.13.15"
13+
__generated_with = "0.14.13"
1414
app = marimo.App()
1515

1616
with app.setup:
@@ -35,13 +35,23 @@ def _():
3535
@app.cell
3636
def _():
3737
slider = mo.ui.slider(4, 100, step=1, value=10, label="Size of the problem")
38+
# display the slider
3839
slider
3940
return (slider,)
4041

4142

42-
@app.cell(hide_code=True)
43-
def _(slider):
44-
n = slider.value
43+
@app.function(hide_code=True)
44+
def cla(n):
45+
"""Compute using the Critical Line Algorithm (CLA) an efficient frontier.
46+
47+
Args:
48+
n (int): The dimension size of the mean vector, lower and upper bounds
49+
arrays, and covariance matrix used in the computation.
50+
51+
Returns:
52+
numpy.ndarray: The efficient frontier generated by the CLA based on the
53+
provided parameters.
54+
"""
4555
mean = np.random.randn(n)
4656
lower_bounds = np.zeros(n)
4757
upper_bounds = np.ones(n)
@@ -57,18 +67,14 @@ def _(slider):
5767
a=np.ones((1, len(mean))),
5868
b=np.ones(1),
5969
).frontier
60-
return (f1,)
61-
62-
63-
@app.cell
64-
def _(f1):
65-
f1.interpolate(2).plot(volatility=True, markers=True)
66-
f1.plot()
67-
return
70+
return f1
6871

6972

7073
@app.cell
71-
def _():
74+
def _(slider):
75+
frontier = cla(slider.value)
76+
frontier.interpolate(2).plot(volatility=True, markers=True)
77+
frontier.plot()
7278
return
7379

7480

experiments/init_algo.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import numpy as np
1616
from loguru import logger
1717

18-
from src.cvxcla.first import init_algo, init_algo_lp
18+
from cvxcla.first import init_algo
1919

2020
if __name__ == "__main__":
2121
# Define a large-scale portfolio problem with 10,000 assets
@@ -32,9 +32,3 @@
3232
# Print the indices of free variables in the solution
3333
print("Free variables from init_algo:")
3434
print(np.where(tp.free)[0])
35-
36-
# Compute the first turning point using the linear programming implementation
37-
tp = init_algo_lp(mean=mean, lower_bounds=np.zeros_like(upper_bound), upper_bounds=upper_bound)
38-
# Print the indices of free variables in the solution
39-
print("Free variables from init_algo_lp:")
40-
print(np.where(tp.free)[0])

pyproject.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ description = "Critical line algorithm for the efficient frontier"
55
readme = "README.md"
66
requires-python = ">=3.10"
77
dependencies = [
8-
"cvxpy-base>=1.5.1",
98
"numpy>=2.0.0",
109
"scipy>=1.14.1",
11-
"clarabel>=0.9.0",
1210
"plotly>=6.0.1",
1311
"kaleido==1.0.0"
1412
]
@@ -42,12 +40,9 @@ build-backend = "hatchling.build"
4240
packages = ["src/cvxcla"]
4341

4442
[tool.deptry.per_rule_ignores]
45-
#DEP001 = ["cvxpy"]
46-
DEP002 = ["clarabel", "kaleido"]
43+
DEP002 = ["kaleido"]
4744

4845
[tool.deptry]
4946
# see https://deptry.com/usage/#pep-621-dev-dependency-groups
5047
pep621_dev_dependency_groups = ["dev"]
5148

52-
[tool.deptry.package_module_name_map]
53-
cvxpy-base = ["cvxpy"]

src/cvxcla/first.py

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
from __future__ import annotations
2222

23-
import cvxpy as cp
2423
import numpy as np
2524
from numpy.typing import NDArray
2625

@@ -75,95 +74,6 @@ def init_algo(
7574
return TurningPoint(free=free, weights=weights)
7675

7776

78-
def init_algo_lp(
79-
mean: NDArray[np.float64],
80-
lower_bounds: NDArray[np.float64],
81-
upper_bounds: NDArray[np.float64],
82-
a_eq: NDArray[np.float64] | None = None,
83-
b_eq: NDArray[np.float64] | None = None,
84-
solver=cp.CLARABEL,
85-
**kwargs,
86-
# A_ub: NDArray[np.float64] | None = None,
87-
# b_ub: NDArray[np.float64] | None = None,
88-
) -> TurningPoint:
89-
"""Compute the first turning point using linear programming.
90-
91-
This function formulates the problem of finding the first turning point as a linear
92-
programming problem and solves it using a convex optimization solver. The objective
93-
is to maximize the expected return subject to the constraints that the weights sum
94-
to 1 and are within their bounds.
95-
96-
Args:
97-
mean: Vector of expected returns for each asset.
98-
lower_bounds: Vector of lower bounds for asset weights.
99-
upper_bounds: Vector of upper bounds for asset weights.
100-
a_eq: Matrix for additional linear equality constraints (Ax = b).
101-
If None, only the fully invested constraint (sum(weights) = 1) is used.
102-
b_eq: Vector for additional linear equality constraints (Ax = b).
103-
If None, only the fully invested constraint (sum(weights) = 1) is used.
104-
solver: The CVXPY solver to use for the optimization.
105-
**kwargs: Additional keyword arguments to pass to the solver.
106-
107-
Returns:
108-
A TurningPoint object representing the first point on the efficient frontier.
109-
110-
Raises:
111-
ValueError: If the problem is infeasible or if lower bounds exceed upper bounds.
112-
113-
"""
114-
if a_eq is None:
115-
a_eq = np.atleast_2d(np.ones_like(mean))
116-
117-
if b_eq is None:
118-
b_eq = np.array([1.0])
119-
120-
# if A_ub is None:
121-
# A_ub = np.atleast_2d(np.zeros_like(mean))
122-
123-
# if b_ub is None:
124-
# b_ub = np.array([0.0])
125-
126-
w = cp.Variable(mean.shape[0], "weights")
127-
128-
objective = cp.Maximize(mean.T @ w)
129-
constraints = [
130-
a_eq @ w == b_eq,
131-
# A_ub @ w <= b_ub,
132-
lower_bounds <= w,
133-
w <= upper_bounds,
134-
cp.sum(w) == 1.0,
135-
]
136-
137-
problem = cp.Problem(objective, constraints)
138-
problem.solve(solver=solver, **kwargs)
139-
# check status of problem is optimal
140-
if problem.status != cp.OPTIMAL:
141-
raise ValueError("Could not construct a fully invested portfolio")
142-
143-
# assert problem.status == cp.OPTIMAL
144-
# print(problem.status)
145-
# print(status)
146-
147-
w = w.value
148-
149-
# compute the distance from the closest bound
150-
# distance = np.min(
151-
# np.array([np.abs(w - lower_bounds), np.abs(upper_bounds - w)]), axis=0
152-
# )
153-
154-
# which element has the largest distance to any bound?
155-
# Even if all assets are at their bounds,
156-
# we get a (somewhat random) free asset.
157-
# index = np.argmax(distance)
158-
159-
# free = np.full_like(mean, False, dtype=np.bool_)
160-
# free[index] = True
161-
162-
free = _free(w, lower_bounds, upper_bounds)
163-
164-
return TurningPoint(free=free, weights=w)
165-
166-
16777
def _free(
16878
w: NDArray[np.float64], lower_bounds: NDArray[np.float64], upper_bounds: NDArray[np.float64]
16979
) -> NDArray[np.bool_]:

src/tests/test_cla.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pandas import DataFrame
1414

1515
from cvxcla import CLA
16-
from cvxcla.first import init_algo_lp
16+
from cvxcla.first import init_algo
1717

1818

1919
def test_solver(input_data: Any, results: Any) -> None:
@@ -71,7 +71,7 @@ def test_example(example: DataFrame, example_solution: DataFrame) -> None:
7171
upper_bounds = 0.5 * np.ones(ns)
7272

7373
# covariance = example.cov().values
74-
tp = init_algo_lp(mean=means.values, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
74+
tp = init_algo(mean=means.values, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
7575
assert np.allclose(tp.weights, np.array([0.1, 0.5, 0.4]), atol=1e-9)
7676
assert np.allclose(tp.free, np.array([False, False, True]))
7777

src/tests/test_first.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import numpy as np
1111
import pytest
1212

13-
from cvxcla.first import init_algo, init_algo_lp
13+
from cvxcla.first import init_algo
1414
from cvxcla.types import TurningPoint
1515

1616

@@ -29,12 +29,6 @@ def test_init_algo(n: int) -> None:
2929
lower_bounds = np.zeros(n)
3030
upper_bounds = np.random.rand(n)
3131

32-
first = init_algo_lp(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
33-
34-
assert np.sum(first.free) == 1
35-
assert np.sum(first.weights) == pytest.approx(1.0)
36-
assert isinstance(first, TurningPoint)
37-
3832
first = init_algo(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
3933

4034
assert np.sum(first.free) == 1
@@ -51,11 +45,6 @@ def test_small() -> None:
5145
mean = np.array([1.0, 2.0, 3.0])
5246
lower_bound = np.array([0.0, 0.0, 0.0])
5347
upper_bound = np.array([0.4, 0.4, 0.4])
54-
tp = init_algo_lp(mean=mean, lower_bounds=lower_bound, upper_bounds=upper_bound)
55-
56-
assert np.allclose(tp.weights, [0.2, 0.4, 0.4])
57-
assert tp.lamb == np.inf
58-
assert np.allclose(tp.free, [True, False, False])
5948

6049
tp = init_algo(mean=mean, lower_bounds=lower_bound, upper_bounds=upper_bound)
6150

@@ -74,9 +63,6 @@ def test_no_fully_invested_portfolio() -> None:
7463
lower_bounds = np.array([0.0, 0.0, 0.0])
7564
upper_bounds = np.array([0.2, 0.2, 0.2])
7665

77-
with pytest.raises(ValueError, match="Could not construct a fully invested portfolio"):
78-
init_algo_lp(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
79-
8066
with pytest.raises(ValueError, match="Could not construct a fully invested portfolio"):
8167
init_algo(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
8268

@@ -91,8 +77,5 @@ def test_lb_ub_mixed() -> None:
9177
lower_bounds = np.ones(3)
9278
mean = np.ones(3)
9379

94-
with pytest.raises(ValueError):
95-
init_algo_lp(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)
96-
9780
with pytest.raises(ValueError):
9881
init_algo(mean=mean, lower_bounds=lower_bounds, upper_bounds=upper_bounds)

0 commit comments

Comments
 (0)