edgekit.optimize
Portfolio optimization — turn a vector of expected returns and a covariance matrix into weights. Covariance estimators (sample and shrunk), the classic optimizers (min-variance, max-Sharpe, mean-variance), the efficient frontier, and the risk-parity family, all in closed numpy.
What's inside. Two covariance estimators (sample_cov, ledoit_wolf) feed everything downstream. The optimizers split into two camps: the unconstrained analytic solvers (min_variance, max_sharpe, mean_variance) that solve in closed form, and the iterative risk-based allocators (equal_risk_contribution). efficient_frontier traces the whole risk/return locus, and portfolio_vol / portfolio_return / risk_contributions are the small diagnostics you evaluate any weight vector with.
equal_risk_contribution (which stays non-negative by construction). The weights sum to 1 but are otherwise unbounded.Covariance estimators#
sample_cov#
The plain sample covariance matrix of a returns panel — the maximum-likelihood estimate. Fast and unbiased, but noisy and often near-singular when assets outnumber observations, which is exactly when you want to shrink it instead.
sample_cov(returns) -> np.ndarray # (n_assets, n_assets)returns— a(T, n)array/DataFrame of asset returns (rows = periods).
Returns: an (n, n) numpy covariance matrix.
import edgekit as ek
cov = ek.optimize.sample_cov(rets_panel)ledoit_wolf#
Ledoit-Wolf shrinkage covariance — pulls the noisy sample matrix toward a scaled-identity target by an analytically optimal intensity. The go-to estimator for optimization: it stays well-conditioned and invertible, which keeps max_sharpe and min_variance from blowing up on estimation noise.
ledoit_wolf(returns) -> np.ndarray # shrunk (n, n)returns— a(T, n)array/DataFrame of asset returns.
Returns: an (n, n) shrunk covariance matrix (same shape as sample_cov).
cov = ek.optimize.ledoit_wolf(rets_panel) # prefer this for optimization inputsOptimizers#
min_variance#
The global minimum-variance portfolio — the weights that minimise wᵀ Σ w subject only to summing to 1. Ignores expected returns entirely, which is a feature: it is the most estimation-robust point on the frontier because Σ is far easier to estimate than μ.
min_variance(cov) -> np.ndarray # unconstrained, sums to 1cov— an(n, n)covariance matrix.
Returns: an (n,) weight vector summing to 1 (can be negative).
w = ek.optimize.min_variance(cov)max_sharpe#
The tangency portfolio — weights that maximise the Sharpe ratio (wᵀμ − rf) / sqrt(wᵀΣw). The most sensitive of the three to estimation error in μ; always feed it a shrunk covariance and treat the result as a starting point, not gospel.
max_sharpe(mu, cov, rf=0.0) -> np.ndarray # tangency, unconstrained| Param | Type | Default | Meaning |
|---|---|---|---|
mu | array (n,) | — | Expected returns per asset. |
cov | array (n, n) | — | Covariance matrix (use ledoit_wolf). |
rf | float | 0.0 | Risk-free rate in the same units as mu. |
Returns: an (n,) weight vector summing to 1 (can be negative).
w = ek.optimize.max_sharpe(mu, cov, rf=0.02)mean_variance#
The Markowitz mean-variance solution — maximises wᵀμ − (risk_aversion/2)·wᵀΣw. Sweeping risk_aversion from small to large walks the weights from aggressive (return-seeking) toward the minimum-variance corner.
mean_variance(mu, cov, risk_aversion=1.0) -> np.ndarray| Param | Type | Default | Meaning |
|---|---|---|---|
mu | array (n,) | — | Expected returns per asset. |
cov | array (n, n) | — | Covariance matrix. |
risk_aversion | float | 1.0 | Risk penalty λ; higher = more conservative. |
Returns: an (n,) weight vector summing to 1 (can be negative).
w = ek.optimize.mean_variance(mu, cov, risk_aversion=3.0)efficient_frontier#
Trace the efficient frontier: n portfolios spanning the achievable return range, each the minimum-variance portfolio for its target return. The one call that gives you the whole risk/return locus to plot or to pick a point off.
efficient_frontier(mu, cov, n=50) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
mu | array (n_assets,) | — | Expected returns per asset. |
cov | array (n_assets, n_assets) | — | Covariance matrix. |
n | int | 50 | Number of frontier points to trace. |
Returns: a dict with keys "returns" (target return of each point), "vols" (its volatility), "weights" (an (n, n_assets) matrix of the weights at each point), and "sharpe" (the Sharpe of each point).
ef = ek.optimize.efficient_frontier(mu, cov, n=60)
ef["vols"], ef["returns"] # x, y to plot the frontier
best = ef["weights"][ef["sharpe"].argmax()] # max-Sharpe point on the frontier
Risk parity & contributions#
risk_contributions#
Decompose a portfolio's variance into each asset's share — the total risk contributed by each position, which sums to the portfolio volatility. The diagnostic that reveals when a “diversified” book is actually one bet in disguise.
risk_contributions(weights, cov) -> np.ndarray| Param | Type | Default | Meaning |
|---|---|---|---|
weights | array (n,) | — | The portfolio weights. |
cov | array (n, n) | — | Covariance matrix. |
Returns: an (n,) array of per-asset risk contributions (summing to portfolio vol).
rc = ek.optimize.risk_contributions(w, cov)
rc / rc.sum() # fractional risk share per assetequal_risk_contribution#
The risk-parity portfolio — iteratively solves for weights where every asset contributes the same amount of risk. Non-negative by construction (no shorting), and far more robust than max-Sharpe because it never touches μ. The workhorse allocator for a multi-strategy book.
equal_risk_contribution(cov, iters=200, tol=1e-8) -> np.ndarray| Param | Type | Default | Meaning |
|---|---|---|---|
cov | array (n, n) | — | Covariance matrix. |
iters | int | 200 | Maximum fixed-point iterations. |
tol | float | 1e-8 | Convergence tolerance on the weight update. |
Returns: an (n,) non-negative weight vector summing to 1.
w = ek.optimize.equal_risk_contribution(cov)
ek.optimize.risk_contributions(w, cov) # all roughly equalDiagnostics#
portfolio_vol#
Portfolio volatility for a weight vector — the square root of wᵀ Σ w.
portfolio_vol(weights, cov) -> floatweights— an(n,)weight vector.cov— the(n, n)covariance matrix.
Returns: a float portfolio volatility (same units as the covariance inputs).
portfolio_return#
Portfolio expected return for a weight vector — the dot product wᵀ μ.
portfolio_return(weights, mu) -> floatweights— an(n,)weight vector.mu— the(n,)expected-return vector.
Returns: a float expected portfolio return.
w = ek.optimize.max_sharpe(mu, cov)
r = ek.optimize.portfolio_return(w, mu)
v = ek.optimize.portfolio_vol(w, cov)
sharpe = r / vSee also#
- edgekit.factors — estimate the
μand betas that feed the optimizers. - edgekit.risk — VaR/CVaR on the resulting portfolio return stream.
- edgekit.metrics — score the optimized book on PF / MAR / Sharpe.
