""" Markowitz mean-variance optimization (Markowitz 1952). Implements: - markowitz_max_sharpe : Maximum Sharpe Ratio via SLSQP - min_variance : Global Minimum Variance - target_return_frontier : Efficient frontier points for a given return target """ from typing import Dict, List import numpy as np from scipy.optimize import minimize def markowitz_max_sharpe( mu: np.ndarray, Sigma: np.ndarray, rf: float = 0.0, weight_bounds: tuple = (0.005, 0.30), n_restarts: int = 5, ) -> np.ndarray: """ Maximum Sharpe Ratio portfolio via SLSQP. Solves: max (w·μ - rf) / sqrt(w·Σ·w) s.t. sum(w) = 1, w_i ∈ [lb, ub] Parameters ---------- mu : Expected annualised returns, shape (n,). Sigma : Covariance matrix, shape (n, n). rf : Risk-free rate (default 0). weight_bounds : (lower, upper) per-asset weight bounds. n_restarts : Number of random starting points. Returns ------- weights : ndarray (n,) """ mu = np.asarray(mu, dtype=float) Sigma = np.asarray(Sigma, dtype=float) n = len(mu) def neg_sharpe(w): r = w @ mu v = np.sqrt(w @ Sigma @ w) return -(r - rf) / v if v > 1e-10 else 1e10 best_w = np.ones(n) / n best_sr = -np.inf for seed in range(n_restarts): rng = np.random.default_rng(seed) w0 = rng.dirichlet(np.ones(n)) res = minimize( neg_sharpe, w0, method="SLSQP", bounds=[weight_bounds] * n, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}], options={"maxiter": 1000, "ftol": 1e-9}, ) if res.success: w = np.maximum(res.x, 0) w /= w.sum() sr = -neg_sharpe(w) if sr > best_sr: best_sr, best_w = sr, w.copy() return best_w def min_variance( mu: np.ndarray, Sigma: np.ndarray, weight_bounds: tuple = (0.005, 0.30), ) -> np.ndarray: """ Global Minimum Variance portfolio. Solves: min w·Σ·w s.t. sum(w) = 1, w_i ∈ [lb, ub] Parameters ---------- mu : Expected returns (unused, kept for uniform signature). Sigma : Covariance matrix, shape (n, n). weight_bounds : (lower, upper) per-asset weight bounds. Returns ------- weights : ndarray (n,) """ mu = np.asarray(mu, dtype=float) Sigma = np.asarray(Sigma, dtype=float) n = len(mu) def portfolio_variance(w): return w @ Sigma @ w w0 = np.ones(n) / n res = minimize( portfolio_variance, w0, method="SLSQP", bounds=[weight_bounds] * n, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}], options={"maxiter": 1000, "ftol": 1e-9}, ) if res.success: w = np.maximum(res.x, 0) return w / w.sum() # Fallback: equal weight return np.ones(n) / n def target_return_frontier( mu: np.ndarray, Sigma: np.ndarray, n_points: int = 30, weight_bounds: tuple = (0.0, 1.0), ) -> List[Dict]: """ Compute efficient frontier by solving minimum-variance portfolios at a grid of target returns. Parameters ---------- mu : Expected returns, shape (n,). Sigma : Covariance matrix, shape (n, n). n_points : Number of frontier points. weight_bounds : (lower, upper) per-asset weight bounds. Returns ------- List of dicts: [{"target_return", "volatility", "sharpe", "weights"}, ...] """ mu = np.asarray(mu, dtype=float) Sigma = np.asarray(Sigma, dtype=float) n = len(mu) # Anchor min-return at global min-variance portfolio res_mv = minimize( lambda w: w @ Sigma @ w, np.ones(n) / n, method="SLSQP", bounds=[weight_bounds] * n, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}], options={"maxiter": 500}, ) min_ret = float(res_mv.x @ mu) if res_mv.success else float(np.min(mu)) max_ret = float(np.max(mu)) target_rets = np.linspace(min_ret, max_ret, n_points) frontier = [] for tr in target_rets: res = minimize( lambda w: w @ Sigma @ w, np.ones(n) / n, method="SLSQP", bounds=[weight_bounds] * n, constraints=[ {"type": "eq", "fun": lambda w: w.sum() - 1}, {"type": "eq", "fun": lambda w, tr=tr: w @ mu - tr}, ], options={"maxiter": 500, "ftol": 1e-9}, ) if res.success: w = np.maximum(res.x, 0) w /= w.sum() vol = float(np.sqrt(w @ Sigma @ w)) ret = float(w @ mu) frontier.append( { "target_return": float(tr), "volatility": vol, "sharpe": ret / vol if vol > 1e-10 else 0.0, "weights": w.tolist(), } ) return frontier