edgekit

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.

!min_variance / max_sharpe / mean_variance are unconstrained
These three are closed-form solvers that allow negative weights — they can and will short. There is no long-only or box constraint baked in. If your book is long-only or capped, clip and renormalise the result yourself, or reach for 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 inputs

Optimizers#

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 1
  • cov — 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
ParamTypeDefaultMeaning
muarray (n,)Expected returns per asset.
covarray (n, n)Covariance matrix (use ledoit_wolf).
rffloat0.0Risk-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
ParamTypeDefaultMeaning
muarray (n,)Expected returns per asset.
covarray (n, n)Covariance matrix.
risk_aversionfloat1.0Risk 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
ParamTypeDefaultMeaning
muarray (n_assets,)Expected returns per asset.
covarray (n_assets, n_assets)Covariance matrix.
nint50Number 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
efficient_frontier chart
The efficient frontier with each portfolio coloured by Sharpe ratio.

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
ParamTypeDefaultMeaning
weightsarray (n,)The portfolio weights.
covarray (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 asset

equal_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
ParamTypeDefaultMeaning
covarray (n, n)Covariance matrix.
itersint200Maximum fixed-point iterations.
tolfloat1e-8Convergence 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 equal

Diagnostics#

portfolio_vol#

Portfolio volatility for a weight vector — the square root of wᵀ Σ w.

portfolio_vol(weights, cov) -> float
  • weights — 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) -> float
  • weights — 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 / v

See also#