Spaces:
Sleeping
Sleeping
File size: 2,816 Bytes
558db1e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import numpy as np
import pandas as pd
import cvxpy as cp
from config import logger, Color
from constraints import make_nearest_psd
def exact_risk_parity_allocation(cov_matrix: pd.DataFrame, silent: bool = False) -> pd.Series:
"""
Computes the Exact True Risk Parity (Equal Risk Contribution - ERC) portfolio.
Instead of using heuristic or graph-theoretic approaches (like HRP), this uses
the strictly convex Spinu logarithmic barrier formulation to guarantee mathematically
exact equal marginal risk contributions.
The optimization problem is:
min 0.5 * x^T * Sigma * x - c * sum(ln(x))
subject to x >= 0
The optimal portfolio weights are then w = x / sum(x).
Args:
cov_matrix: The asset covariance matrix (N x N)
silent: If True, suppresses console logs
Returns:
pd.Series containing the normalized optimal Risk Parity weights.
"""
if not silent:
print(f" {Color.DIM}ℹ Solving Exact True Risk Parity (ERC) formulation via CVXPY...{Color.RESET}", end="", flush=True)
tickers = list(cov_matrix.columns)
n = len(tickers)
# Ensure strict positive semi-definiteness for CVXPY convex solver
sigma_vals = make_nearest_psd(cov_matrix.values)
# Define variables
x = cp.Variable(n, nonneg=True)
# Spinu Objective: 0.5 * x^T * Sigma * x - sum(ln(x))
# We use cp.sum(cp.log(x)) as the barrier. Note that cp.log enforces strict positivity internally.
risk = 0.5 * cp.quad_form(x, cp.psd_wrap(sigma_vals))
barrier = cp.sum(cp.log(x))
objective = cp.Minimize(risk - barrier)
# We do not constrain sum(x) == 1 here. We solve unconstrained (besides positivity)
# and then normalize the raw allocations x into portfolio weights w.
prob = cp.Problem(objective)
try:
# Solve using ECOS or SCS since it involves logs
prob.solve(solver=cp.SCS, max_iters=5000, eps=1e-5)
except Exception as e:
logger.error(f"SCS Failed for Exact Risk Parity: {e}. Retrying with ECOS...")
try:
prob.solve(solver=cp.ECOS)
except Exception as e2:
raise RuntimeError(f"Exact Risk Parity optimization failed: {e2}")
if prob.status not in [cp.OPTIMAL, cp.OPTIMAL_INACCURATE] or x.value is None:
raise RuntimeError(f"Exact Risk Parity did not converge. Status: {prob.status}")
# Normalize to get valid portfolio weights summing to 1
raw_x = x.value
weights = raw_x / np.sum(raw_x)
# Clean up near-zero rounding errors
weights[weights < 1e-6] = 0.0
weights = weights / np.sum(weights)
if not silent:
print(f" {Color.GREEN}done.{Color.RESET}")
return pd.Series(weights, index=tickers)
|