From 65187b7aa3ae80cbb6bfd4bcb13e5cecf3ac23ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:44:12 +0000 Subject: [PATCH 1/2] Initial plan From 561dbfd9ee48fc4265c4d99a6d7ef927c707e1ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:50:20 +0000 Subject: [PATCH 2/2] Fix CI: Add notebook lint ignores and format code Co-authored-by: fchareyr <3171960+fchareyr@users.noreply.github.com> --- notebooks/ConstrainedRiskBudgeting.py | 27 ++++++++++++++++++--------- pyproject.toml | 1 + src/pyrb/solvers.py | 24 +++++++++++++++++++++--- tests/test_risk_budgeting.py | 8 +++++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/notebooks/ConstrainedRiskBudgeting.py b/notebooks/ConstrainedRiskBudgeting.py index d38267a..856a304 100644 --- a/notebooks/ConstrainedRiskBudgeting.py +++ b/notebooks/ConstrainedRiskBudgeting.py @@ -59,7 +59,7 @@ from pyrb import ConstrainedRiskBudgeting plt.style.use("tableau-colorblind10") -plt.rcParams.update({"figure.autolayout": True,"axes.grid": True}) +plt.rcParams.update({"figure.autolayout": True, "axes.grid": True}) # %% vol = np.array([0.05, 0.05, 0.07, 0.1, 0.15, 0.15, 0.15, 0.18]) @@ -80,7 +80,7 @@ ) cov = np.outer(vol, vol) * corr -asset_labels = [f"A{i+1}" for i in range(len(vol))] +asset_labels = [f"A{i + 1}" for i in range(len(vol))] # %% scenario_specs = [ @@ -99,7 +99,12 @@ { "name": "Add relative allocation", "description": "Adds w1 - w2 + w5 - w6 ≥ -5% on top of the high-vol cap.", - "C": np.array([[0.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0],[1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0]]), + "C": np.array( + [ + [0.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0], + [1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0], + ] + ), "d": np.array([-0.3, -0.05]), }, ] @@ -150,25 +155,25 @@ def solve_crb_scenario(cov_matrix, spec): [res["weights"] for res in scenario_results], columns=asset_labels, index=summary_df.index, - ) +) marginal_df = pd.DataFrame( [res["marginal_rc"] for res in scenario_results], columns=asset_labels, index=summary_df.index, - ) +) risk_contrib_df = pd.DataFrame( [res["risk_contrib"] for res in scenario_results], columns=asset_labels, index=summary_df.index, - ) +) risk_contrib_pct_df = pd.DataFrame( [res["risk_contrib_pct"] for res in scenario_results], columns=asset_labels, index=summary_df.index, - ) +) constraint_df = pd.DataFrame( [ @@ -178,15 +183,17 @@ def solve_crb_scenario(cov_matrix, spec): for res in scenario_results ], index=summary_df.index, - ) +) summary_display = summary_df.copy() for col in ["Total risk (volatility)", "λ*", "Sum of weights"]: summary_display[col] = summary_display[col].astype(float).round(4) + def format_df(df, formatter): return df.apply(lambda col: col.map(formatter)) + weights_display = format_df(weights_df, lambda x: f"{x:.2%}") risk_contrib_display = format_df(risk_contrib_df, lambda x: f"{x:.4f}") risk_contrib_pct_display = format_df(risk_contrib_pct_df, lambda x: f"{x:.2%}") @@ -209,7 +216,9 @@ def format_df(df, formatter): if not constraint_df.empty: constraint_display = constraint_df.copy().astype(float).round(4) - constraint_display.columns = [f"Constraint {i+1}" for i in range(constraint_display.shape[1])] + constraint_display.columns = [ + f"Constraint {i + 1}" for i in range(constraint_display.shape[1]) + ] print("\nConstraint evaluations Cw") display(constraint_display) diff --git a/pyproject.toml b/pyproject.toml index 437d098..35e68d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "tests/**/*" = ["F401", "F811"] +"notebooks/**/*" = ["F821", "B018"] # Notebooks may use IPython display() and have useless expressions for display [tool.ruff.lint.isort] known-first-party = ["pyrb"] diff --git a/src/pyrb/solvers.py b/src/pyrb/solvers.py index c5e8924..f9ee44c 100644 --- a/src/pyrb/solvers.py +++ b/src/pyrb/solvers.py @@ -38,7 +38,9 @@ def accelerate(_varphi, r, s, u, alpha=10, tau=2): "Tuple((float64[:], float64[:], float64))(float64[:], float64, float64[:], float64, float64, float64[:], float64[:], float64[:], float64[:,:], float64, float64[:,:])", nopython=True, ) -def _cycle(x, c, var, _varphi, sigma_x, Sx, budgets, expected_returns, bounds, lambda_log, cov): +def _cycle( + x, c, var, _varphi, sigma_x, Sx, budgets, expected_returns, bounds, lambda_log, cov +): """ Internal numba function for computing one cycle of the CCD algorithm. @@ -59,7 +61,13 @@ def _cycle(x, c, var, _varphi, sigma_x, Sx, budgets, expected_returns, bounds, l def solve_rb_ccd( - cov, budgets=None, expected_returns=None, risk_aversion=1.0, bounds=None, lambda_log=1.0, _varphi=0.0 + cov, + budgets=None, + expected_returns=None, + risk_aversion=1.0, + bounds=None, + lambda_log=1.0, + _varphi=0.0, ): """Solve the risk budgeting problem using cyclical coordinate descent. @@ -122,7 +130,17 @@ def solve_rb_ccd( while not cvg: x, sx, sigma_x = _cycle( - x, risk_aversion, var, _varphi, sigma_x, sx, budgets, expected_returns, bounds, lambda_log, cov + x, + risk_aversion, + var, + _varphi, + sigma_x, + sx, + budgets, + expected_returns, + bounds, + lambda_log, + cov, ) cvg = np.sum(np.array(x - x0) ** 2) <= CCD_CONVERGENCE_TOL x0 = x.copy() diff --git a/tests/test_risk_budgeting.py b/tests/test_risk_budgeting.py index 8eb8e85..addcd25 100644 --- a/tests/test_risk_budgeting.py +++ b/tests/test_risk_budgeting.py @@ -62,6 +62,7 @@ def test_cerb(): np.testing.assert_almost_equal(CRB.get_risk_contributions()[1], 0.2455, decimal=5) np.testing.assert_almost_equal(np.sum(CRB.weights[1]), 0.2) + def test_rb_with_equal_budgets(): equal_budgets = [1.0 / NUMBER_OF_ASSET] * NUMBER_OF_ASSET RB = RiskBudgeting(COVARIANCE_MATRIX, equal_budgets) @@ -82,7 +83,12 @@ def test_cerb_with_expected_returns(): d = [-0.3] CRB = ConstrainedRiskBudgeting( - COVARIANCE_MATRIX, budgets=RISK_BUDGETS, expected_returns=EXPECTED_RETURNS, bounds=BOUNDS, C=C, d=d + COVARIANCE_MATRIX, + budgets=RISK_BUDGETS, + expected_returns=EXPECTED_RETURNS, + bounds=BOUNDS, + C=C, + d=d, ) CRB.solve()