math-backend / erc_engine.py
engineportf's picture
Upload folder using huggingface_hub
558db1e verified
Raw
History Blame Contribute Delete
2.82 kB
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)