Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

First solve (Python)

A minimal long-only mean-variance rebalance. Arrays must be NumPy float64.

import numpy as np
from ledge import PortfolioProblem

rng = np.random.default_rng(7)
n, k = 100, 8
F = rng.normal(0.0, 0.2, size=(n, k))       # factor loadings
omega = np.diag(rng.uniform(0.04, 0.12, size=k))  # factor covariance
d = rng.uniform(0.05, 0.10, size=n)         # idiosyncratic variance
mu = rng.normal(0.08, 0.02, size=n)         # expected returns

problem = PortfolioProblem(
    F,
    omega,
    d,
    mu,
    risk_aversion=8.0,
    budget=1.0,
    lower_bounds=np.zeros(n),
    upper_bounds=np.full(n, 0.03),
)
result = problem.solve()
print(result.status, result.weights.sum(), result.primal_residual)

The objective minimized is

risk_aversion / 2 * (w - b)' Sigma (w - b)      [b = 0 without a benchmark]
- expected_returns' w
+ turnover_penalty / 2 * ||w - previous_weights||^2
+ l1_turnover_costs' |w - previous_weights|

subject to sum(w) = budget, lower <= w <= upper, and any linear constraints you pass (equality_matrix @ w = equality_rhs, inequality_matrix @ w <= inequality_rhs).

The next date

Pass the previous solution as a warm start and price turnover:

next_problem = PortfolioProblem(
    F,
    omega,
    d,
    mu + rng.normal(0.0, 0.005, size=n),
    risk_aversion=8.0,
    lower_bounds=np.zeros(n),
    upper_bounds=np.full(n, 0.03),
    previous_weights=result.weights,
    l1_turnover_costs=0.001,   # 10 bps per unit traded; scalar broadcasts
)
second = next_problem.solve(warm_start=result.weights)
print(second)

For repeated dates prefer a rolling sequence, which caches the factorization and chains full primal/dual warm starts automatically.

Reading the result

SolveResult carries status, weights, objective, iterations, solve_time_seconds, independently evaluated primal_residual / dual_residual, polished, and — for infeasible problems — an auditable certificate. The lossless to_json() output also contains every dual multiplier block. Failures raise by default; pass raise_on_failure=False to inspect the result instead. convergence_hints explains unconverged solves in portfolio vocabulary.

One-shot function form: ledge.solve_mean_variance_factor(...) takes the same keyword arguments and returns the same SolveResult.