Optimization (wraquant.opt)

Portfolio and mathematical optimization: mean-variance optimization, risk parity, Black-Litterman, Hierarchical Risk Parity, convex/nonlinear solvers, and multi-objective optimization.

Portfolio methods:

  • max_sharpe – maximum Sharpe ratio portfolio

  • min_volatility – minimum variance portfolio

  • risk_parity – equal risk contribution

  • black_litterman – equilibrium + views blending

  • hierarchical_risk_parity – clustering-based diversification (Lopez de Prado)

  • equal_weight / inverse_volatility – naive diversification baselines

Optimization solvers:

  • Convex: QP, SOCP, SDP

  • Linear: LP, MILP

  • Nonlinear: local and global optimization

  • Multi-objective: Pareto front, NSGA-II, epsilon-constraint

Quick Example

from wraquant.opt import max_sharpe, risk_parity, black_litterman

# Max Sharpe portfolio
result = max_sharpe(returns_df)
print(f"Weights: {result['weights']}")
print(f"Sharpe:  {result['sharpe_ratio']:.4f}")

# Risk parity (equal risk contribution)
rp = risk_parity(returns_df)
print(f"Risk parity weights: {rp['weights']}")

# Black-Litterman with views
bl = black_litterman(returns_df, market_caps, views={"AAPL": 0.12})
print(f"BL weights: {bl['weights']}")

# HRP (no covariance inversion needed)
from wraquant.opt import hierarchical_risk_parity
hrp = hierarchical_risk_parity(returns_df)

Constraints

from wraquant.opt import weight_constraint, sector_constraints, turnover_constraint

# Weight bounds
w_bounds = weight_constraint(lower=0.02, upper=0.30)

# Sector constraints
sectors = {"Tech": ["AAPL", "MSFT"], "Finance": ["JPM", "GS"]}
s_bounds = sector_constraints(sectors, max_sector=0.40)

# Turnover constraint (limit rebalancing costs)
t_bound = turnover_constraint(max_turnover=0.20, current_weights=old_weights)

See also

API Reference

Portfolio and mathematical optimization.

Provides a complete optimization toolkit for quantitative portfolio construction, from classical mean-variance optimization through modern hierarchical and Bayesian methods, plus general-purpose convex, linear, nonlinear, and multi-objective solvers for custom formulations.

Key sub-modules:

  • Portfolio optimization (portfolio) – The core allocation methods: mean_variance (Markowitz MVO with optional constraints), min_volatility (global minimum variance portfolio), max_sharpe (tangency portfolio), risk_parity (equal risk contribution – each asset contributes equally to portfolio volatility), equal_weight (1/N benchmark), inverse_volatility (weight inversely proportional to vol), hierarchical_risk_parity (Lopez de Prado’s HRP – uses hierarchical clustering to avoid inverting the covariance matrix), black_litterman (blend market equilibrium with investor views).

  • Convex optimization (convex) – minimize_quadratic, solve_qp (quadratic program), solve_socp (second-order cone), solve_sdp (semidefinite program) via CVXPY.

  • Linear optimization (linear) – solve_lp, solve_milp (mixed-integer LP), and transportation_problem.

  • Nonlinear optimization (nonlinear) – minimize (local), global_minimize (basin-hopping, differential evolution), and root_find.

  • Multi-objective (multi_objective) – pareto_front, nsga2 (evolutionary multi-objective), and epsilon_constraint.

  • Constraint utilities (utils) – weight_constraint, sum_to_one_constraint, sector_constraints, turnover_constraint, and cardinality_constraint for building realistic constraint sets.

  • Result types (base) – OptimizationResult, Objective, and Constraint dataclasses for structured output.

Example

>>> from wraquant.opt import max_sharpe, risk_parity
>>> result = max_sharpe(returns, risk_free_rate=0.04)
>>> print(result.weights, result.sharpe_ratio)
>>> rp = risk_parity(returns)

Use wraquant.opt for portfolio allocation decisions. For risk measurement and decomposition of the resulting portfolio, see wraquant.risk. For parallel optimization sweeps across constraint sets, see wraquant.scale.parallel_optimize.

class Constraint[source]

Bases: object

Optimization constraint specification.

Parameters:
  • type (str) – Constraint type (‘eq’ for equality, ‘ineq’ for inequality).

  • fun (callable) – Constraint function.

  • name (str, default: '') – Human-readable name.

type: str
fun: callable
name: str = ''
__init__(type, fun, name='')
Parameters:
  • type (str)

  • fun (callable)

  • name (str, default: '')

Return type:

None

class Objective[source]

Bases: object

Optimization objective specification.

Parameters:
  • fun (callable) – Objective function to minimize.

  • name (str, default: '') – Human-readable name.

fun: callable
name: str = ''
__init__(fun, name='')
Parameters:
  • fun (callable)

  • name (str, default: '')

Return type:

None

class OptimizationResult[source]

Bases: object

Result of a portfolio optimization.

Parameters:
  • weights (ndarray[tuple[Any, ...], dtype[floating]]) – Optimal portfolio weights.

  • expected_return (float, default: 0.0) – Expected portfolio return.

  • volatility (float, default: 0.0) – Portfolio volatility (std dev).

  • sharpe_ratio (float, default: 0.0) – Portfolio Sharpe ratio.

  • asset_names (list[str], default: <factory>) – Names of assets.

  • metadata (dict, default: <factory>) – Additional solver-specific information.

weights: ndarray[tuple[Any, ...], dtype[floating]]
expected_return: float = 0.0
volatility: float = 0.0
sharpe_ratio: float = 0.0
asset_names: list[str]
metadata: dict
to_dict()[source]

Return weights as {asset_name: weight} dict.

Return type:

dict[str, float]

__init__(weights, expected_return=0.0, volatility=0.0, sharpe_ratio=0.0, asset_names=<factory>, metadata=<factory>)
Parameters:
Return type:

None

mean_variance(returns, target_return=None, risk_free=0.0, periods_per_year=252, bounds=(0.0, 1.0), shrink=False, shrinkage_method='ledoit_wolf')[source]

Mean-variance optimization (Markowitz).

Use mean-variance optimization to find the portfolio that minimises risk for a given target return (efficient frontier), or maximises the Sharpe ratio when no target is specified. This is the foundation of modern portfolio theory.

Solves:

min w’ Sigma w s.t. w’ mu = target_return

sum(w) = 1 bounds[i][0] <= w[i] <= bounds[i][1]

When target_return is None, maximises (w'mu - rf) / sqrt(w'Sigma w).

Parameters:
  • returns (DataFrame) – Asset return DataFrame (columns = assets). Must contain at least 2 assets and enough observations for a stable covariance estimate.

  • target_return (float | None, default: None) – Target annualised return (None = max Sharpe).

  • risk_free (float, default: 0.0) – Annual risk-free rate for Sharpe calculation.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset (min, max). Use (0, 1) for long-only; (-1, 1) to allow shorting.

  • shrink (bool, default: False) – If True, use a shrinkage estimator for the covariance matrix instead of the sample covariance. Shrinkage produces a better-conditioned matrix when the number of assets is large relative to the number of observations.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method when shrink=True"ledoit_wolf" (default), "oas", or "basic". Forwarded to wraquant.stats.correlation.shrunk_covariance.

Return type:

OptimizationResult

Returns:

OptimizationResult with optimal weights, expected return, volatility, and Sharpe ratio. Access result.weights for the allocation and result.sharpe_ratio for the risk-adjusted metric.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['SPY', 'TLT', 'GLD'])
>>> result = mean_variance(returns, target_return=0.05)
>>> np.isclose(result.weights.sum(), 1.0)
True

Notes

Markowitz optimization is sensitive to estimation error in the mean return vector. Consider black_litterman or hierarchical_risk_parity for more robust alternatives.

See also

max_sharpe: Convenience wrapper for max-Sharpe optimization. min_volatility: Minimum variance portfolio. risk_parity: Equal risk contribution portfolio.

min_volatility(returns, bounds=(0.0, 1.0), periods_per_year=252, shrink=False, shrinkage_method='ledoit_wolf')[source]

Minimum volatility portfolio.

Use the minimum volatility portfolio when your primary objective is risk reduction rather than return maximisation. This portfolio sits at the leftmost point of the efficient frontier and does not require a return estimate, making it more robust than mean-variance to estimation error in expected returns.

Solves: min w’ Sigma w, s.t. sum(w) = 1, bounds.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset (default long-only (0, 1)).

  • periods_per_year (int, default: 252) – Trading periods per year.

  • shrink (bool, default: False) – If True, use a shrinkage covariance estimator.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method ("ledoit_wolf", "oas", or "basic").

Return type:

OptimizationResult

Returns:

OptimizationResult with minimum variance weights. The volatility field gives the lowest achievable portfolio standard deviation.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(0)
>>> returns = pd.DataFrame(np.random.randn(252, 4) * 0.01,
...                        columns=['A', 'B', 'C', 'D'])
>>> result = min_volatility(returns)
>>> result.volatility > 0
True

See also

mean_variance: Full mean-variance with target return. risk_parity: Equal risk contribution (also estimation-robust).

max_sharpe(returns, risk_free=0.0, bounds=(0.0, 1.0), periods_per_year=252, shrink=False, shrinkage_method='ledoit_wolf')[source]

Maximum Sharpe ratio portfolio.

Use max-Sharpe when you want the portfolio with the highest risk-adjusted return. This is the tangency portfolio on the efficient frontier – the point where a line from the risk-free rate is tangent to the frontier.

Maximises: (w’mu - rf) / sqrt(w’Sigma w), s.t. sum(w) = 1, bounds.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset.

  • periods_per_year (int, default: 252) – Trading periods per year.

  • shrink (bool, default: False) – If True, use a shrinkage covariance estimator.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method ("ledoit_wolf", "oas", or "basic").

Return type:

OptimizationResult

Returns:

OptimizationResult with maximum Sharpe weights. The sharpe_ratio field gives the optimal risk-adjusted return.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['SPY', 'TLT', 'GLD'])
>>> result = max_sharpe(returns, risk_free=0.04)
>>> np.isclose(result.weights.sum(), 1.0)
True

See also

mean_variance: Mean-variance with a target return constraint. min_volatility: Minimum risk portfolio.

risk_parity(returns, periods_per_year=252)[source]

Risk parity (equal risk contribution) portfolio.

Use risk parity when you want each asset to contribute equally to total portfolio risk. Unlike mean-variance, risk parity does not require expected return estimates, making it robust to estimation error. It is the basis of many institutional “all-weather” strategies.

Minimises: sum_i (RC_i / sigma_p - 1/N)^2

where RC_i = w_i * (Sigma w)_i / sigma_p is asset i’s risk contribution and sigma_p is portfolio volatility.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with risk parity weights. Lower-volatility assets receive higher weights; higher-volatility assets receive lower weights.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * np.array([0.01, 0.02, 0.005]),
...                        columns=['Bonds', 'Equity', 'Gold'])
>>> result = risk_parity(returns)
>>> result.weights[0] > result.weights[1]  # bonds get more weight (lower vol)
True

References

  • Maillard, Roncalli & Teiletche (2010), “The Properties of Equally Weighted Risk Contribution Portfolios”

See also

hierarchical_risk_parity: HRP (no inversion of covariance matrix). min_volatility: Minimum variance (not risk-balanced).

equal_weight(returns, periods_per_year=252)[source]

Equal weight portfolio (1/N).

Use the equal-weight portfolio as a robust baseline. Despite its simplicity, 1/N consistently outperforms many optimised portfolios out-of-sample because it avoids estimation error entirely.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with equal weights (each asset receives weight 1/N).

Example

>>> import pandas as pd, numpy as np
>>> returns = pd.DataFrame(np.random.randn(100, 4) * 0.01,
...                        columns=['A', 'B', 'C', 'D'])
>>> result = equal_weight(returns)
>>> np.allclose(result.weights, 0.25)
True

References

  • DeMiguel, Garlappi & Uppal (2009), “Optimal Versus Naive Diversification”

See also

inverse_volatility: Simple vol-weighted alternative. risk_parity: Optimisation-based risk balancing.

inverse_volatility(returns, periods_per_year=252)[source]

Inverse volatility weighted portfolio.

Use inverse-volatility weighting as a simple, estimation-light alternative to mean-variance. Assets with lower volatility receive higher weights, producing a portfolio that tilts toward stability without requiring a full covariance estimate.

Weight_i = (1 / sigma_i) / sum_j(1 / sigma_j)

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with inverse vol weights. Lower-volatility assets receive higher allocations.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(0)
>>> returns = pd.DataFrame(
...     np.random.randn(252, 3) * np.array([0.005, 0.02, 0.01]),
...     columns=['Bonds', 'Equity', 'Gold'])
>>> result = inverse_volatility(returns)
>>> result.weights[0] > result.weights[1]  # Bonds > Equity
True

See also

equal_weight: Uniform weighting (ignores vol entirely). risk_parity: Equalises risk contribution (uses covariance).

hierarchical_risk_parity(returns, periods_per_year=252)[source]

Hierarchical Risk Parity (HRP) by Lopez de Prado.

Use HRP when you want a stable, estimation-robust portfolio that does not require covariance matrix inversion. HRP applies hierarchical clustering to the correlation matrix, then allocates via recursive bisection using inverse variance. This avoids the instability of mean-variance optimisation and produces portfolios that are naturally diversified across asset clusters.

Algorithm:
  1. Compute correlation-based distance and hierarchical linkage.

  2. Quasi-diagonalise the covariance matrix.

  3. Recursively bisect the sorted assets, allocating by inverse variance of each cluster.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with HRP weights. Weights are always positive (long-only) and sum to 1.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 5) * 0.01,
...                        columns=['A', 'B', 'C', 'D', 'E'])
>>> result = hierarchical_risk_parity(returns)
>>> np.isclose(result.weights.sum(), 1.0)
True

References

  • Lopez de Prado (2016), “Building Diversified Portfolios that Outperform Out-of-Sample”

See also

risk_parity: Equal risk contribution (requires covariance inversion). mean_variance: Classical Markowitz (more sensitive to estimation error).

black_litterman(returns, views, tau=0.05, risk_free=0.0, periods_per_year=252)[source]

Black-Litterman model.

Use Black-Litterman when you have subjective views on expected returns for some assets and want to combine them with market equilibrium returns in a Bayesian framework. BL produces more stable and intuitive portfolios than raw mean-variance because it starts from an equilibrium prior (implied by market capitalisation) and blends in your views proportionally to your confidence.

The posterior expected return is:

E[r] = [(tau Sigma)^{-1} + P’ Omega^{-1} P]^{-1}
  • [(tau Sigma)^{-1} pi + P’ Omega^{-1} Q]

where pi = implied equilibrium returns, P = pick matrix, Q = view returns, Omega = view uncertainty.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • views (dict[str, float]) – Dict mapping asset name to expected return view (e.g., {'AAPL': 0.12} means you expect AAPL to return 12% annualised).

  • tau (float, default: 0.05) – Uncertainty scaling parameter (typical range 0.01-0.1). Higher tau gives more weight to your views.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with BL-adjusted weights. The weights reflect a blend of market equilibrium and your views.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['AAPL', 'MSFT', 'GOOG'])
>>> views = {'AAPL': 0.15}  # bullish on AAPL
>>> result = black_litterman(returns, views, tau=0.05)
>>> result.weights[0] > 1 / 3  # AAPL gets more weight
True

References

  • Black & Litterman (1992), “Global Portfolio Optimization”

  • He & Litterman (1999), “The Intuition Behind Black-Litterman”

See also

mean_variance: Pure mean-variance (no views prior). risk_parity: View-free risk-based allocation.

minimize_quadratic(Q, c, A_eq=None, b_eq=None, A_ub=None, b_ub=None, bounds=None)[source]

Solve min 0.5 * x’Qx + c’x subject to linear constraints via SLSQP.

Use this for portfolio optimisation (where Q is the covariance matrix), regularised regression, and any convex quadratic problem. The SLSQP solver handles moderate problem sizes (n < 1000) well; for larger problems, use solve_qp with the 'osqp' or 'cvxpy' backend.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution vector), objective (optimal value), status ('optimal' or error message), and success (bool).

Example

>>> import numpy as np
>>> Q = np.array([[2, 0], [0, 2]], dtype=float)
>>> c = np.array([-4, -6], dtype=float)
>>> result = minimize_quadratic(Q, c, bounds=[(0, 10), (0, 10)])
>>> result['success']
True
>>> np.allclose(result['x'], [2, 3], atol=0.1)
True

See also

solve_qp: Multi-backend QP solver (scipy, OSQP, cvxpy).

solve_qp(Q, c, A_eq=None, b_eq=None, A_ub=None, b_ub=None, bounds=None, solver='scipy')[source]

Quadratic program solver with backend dispatch.

Use solve_qp as the primary entry point for quadratic programming. It dispatches to scipy (no extra deps), OSQP (fast first-order solver for large sparse QPs), or cvxpy (general-purpose convex solver).

Solves:

min  0.5 * x' Q x + c' x
s.t. A_eq x  = b_eq
     A_ub x <= b_ub
     lb <= x <= ub
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution), objective (optimal value), status (solver status), and success (bool).

Raises:

ValueError – If solver is not recognised.

Example

>>> import numpy as np
>>> Q = np.eye(3) * 2
>>> c = np.array([-2, -3, -1], dtype=float)
>>> A_eq = np.ones((1, 3))
>>> b_eq = np.array([1.0])
>>> result = solve_qp(Q, c, A_eq=A_eq, b_eq=b_eq,
...                   bounds=[(0, 1)] * 3)
>>> result['success']
True

See also

minimize_quadratic: Scipy-only QP solver. wraquant.opt.portfolio.mean_variance: Portfolio-specific QP.

solve_socp(c, A, b, cone_constraints=None)[source]

Solve a Second-Order Cone Program via cvxpy.

Solves:

min  c' x
s.t. A x == b
     ||A_i x + b_i|| <= c_i' x + d_i   (for each cone constraint)

Each element of cone_constraints is a dict with keys A_cone, b_cone, c_cone, and d_cone.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x, objective, status, and success.

solve_sdp(C, constraints)[source]

Solve a Semidefinite Program via cvxpy.

Solves:

min  tr(C X)
s.t. tr(A_i X) == b_i   for each constraint
     X >> 0              (positive semidefinite)
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys X (optimal matrix), objective, status, and success.

solve_lp(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None, bounds=None, method='highs')[source]

Solve a linear program via scipy.optimize.linprog().

Use LP for problems with a linear objective and linear constraints, such as transaction cost minimisation, portfolio rebalancing with turnover constraints, or resource allocation.

Solves:

min  c' x
s.t. A_ub x <= b_ub
     A_eq x == b_eq
     lb <= x <= ub
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (optimal solution vector), objective (optimal value of c’x), status ('optimal' or error), and success (bool).

Example

>>> import numpy as np
>>> # Minimise -x1 - 2*x2 s.t. x1 + x2 <= 4, x1, x2 >= 0
>>> c = np.array([-1, -2], dtype=float)
>>> A_ub = np.array([[1, 1]], dtype=float)
>>> b_ub = np.array([4.0])
>>> result = solve_lp(c, A_ub=A_ub, b_ub=b_ub, bounds=[(0, None), (0, None)])
>>> result['success']
True
>>> np.isclose(result['objective'], -8.0)
True

See also

solve_milp: Mixed-integer LP (handles integer constraints). wraquant.opt.convex.solve_qp: Quadratic programming.

solve_milp(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None, bounds=None, integrality=None)[source]

Solve a mixed-integer linear program via scipy.optimize.milp().

Use MILP when some decision variables must be integers, such as selecting a discrete number of assets to hold, binary buy/sell decisions, or lot-size-constrained portfolio construction.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution – integer variables will be rounded), objective, status, and success.

Example

>>> import numpy as np
>>> # Select 2 assets from 4 (binary selection)
>>> c = np.array([-3, -5, -2, -4], dtype=float)
>>> A_eq = np.ones((1, 4))
>>> b_eq = np.array([2.0])
>>> result = solve_milp(c, A_eq=A_eq, b_eq=b_eq,
...                    bounds=[(0, 1)] * 4,
...                    integrality=[1, 1, 1, 1])
>>> result['success']
True
>>> int(sum(result['x']))  # exactly 2 assets selected
2

See also

solve_lp: Continuous LP (faster, no integer constraints).

transportation_problem(costs, supply, demand)[source]

Solve the transportation / assignment problem as an LP.

Given m supply nodes and n demand nodes, find the shipment matrix X of shape (m, n) that minimises total cost.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (shipment matrix), objective (total cost), status, and success.

Raises:

ValueError – If total supply does not equal total demand.

minimize(fun, x0, method='SLSQP', bounds=None, constraints=None, jac=None, hess=None, options=None)[source]

General nonlinear program solver.

Use this for any smooth optimization problem with nonlinear objectives or constraints, such as fitting option pricing models, calibrating yield curves, or custom portfolio objectives with non-convex penalties.

Wrapper around scipy.optimize.minimize() that returns a plain dict instead of an OptimizeResult.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution), fun (objective value at solution), success (bool), and n_iter (iteration count).

Example

>>> import numpy as np
>>> # Minimize Rosenbrock function
>>> def rosenbrock(x):
...     return (1 - x[0])**2 + 100 * (x[1] - x[0]**2)**2
>>> result = minimize(rosenbrock, np.array([0.0, 0.0]))
>>> result['success']
True
>>> np.allclose(result['x'], [1, 1], atol=1e-3)
True

See also

global_minimize: Global optimization for multimodal problems. wraquant.opt.convex.solve_qp: Quadratic programming (convex).

global_minimize(fun, bounds, method='differential_evolution', seed=None, **kwargs)[source]

Global optimisation via stochastic search methods.

Use global optimization when the objective has multiple local minima (e.g., calibrating stochastic volatility models, fitting regime switching parameters, or optimising non-convex trading strategies). Local solvers get trapped; global methods explore the search space before converging.

Supports differential_evolution, dual_annealing, and basinhopping.

Parameters:
  • fun (Callable[..., float]) – Scalar objective function f(x) -> float.

  • bounds (list[tuple[float, float]]) – Search bounds [(lb, ub), ...] for each variable.

  • method (str, default: 'differential_evolution') – Algorithm name – 'differential_evolution' (population-based, most robust), 'dual_annealing' (simulated annealing + local search), or 'basinhopping' (random restarts + local optimization).

  • seed (int | None, default: None) – Random seed for reproducibility.

  • **kwargs (Any) – Additional keyword arguments forwarded to the chosen scipy solver.

Return type:

dict[str, Any]

Returns:

Dict with keys x (best solution found), fun (objective at solution), success (bool), and n_iter.

Raises:

ValueError – If method is not recognised.

Example

>>> import numpy as np
>>> # Rastrigin function (many local minima, global min at origin)
>>> def rastrigin(x):
...     return 20 + sum(xi**2 - 10*np.cos(2*np.pi*xi) for xi in x)
>>> result = global_minimize(rastrigin, [(-5, 5), (-5, 5)], seed=42)
>>> result['fun'] < 1.0  # near global minimum of 0
True

See also

minimize: Local nonlinear solver (faster but gets trapped).

root_find(fun, x0, method='hybr', jac=None)[source]

Find roots of a nonlinear system.

Use root finding for implied volatility calculation (solve BS(sigma) - market_price = 0), yield-to-maturity computation, or any problem where you need to find x such that f(x) = 0.

Wrapper around scipy.optimize.root().

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (root), fun (residual at root – should be near zero), success (bool), and n_iter (function evaluations or iteration count).

Example

>>> import numpy as np
>>> # Find x such that x^2 - 4 = 0
>>> result = root_find(lambda x: x**2 - 4, np.array([1.0]))
>>> result['success']
True
>>> np.isclose(result['x'][0], 2.0)
True

See also

minimize: Find minimum of a function (not root).

pareto_front(objectives, constraints=None, n_points=50, bounds=None)[source]

Approximate the Pareto frontier via the weighted-sum method.

Use the weighted-sum approach for a quick Pareto front approximation when you have a convex bi-objective problem (e.g., risk vs. return, cost vs. tracking error). For non-convex problems or more than 2 objectives, use nsga2 instead.

For each of n_points evenly-spaced weight vectors the scalar weighted-sum of the objectives is minimised using SLSQP.

Parameters:
  • objectives (list[Callable[..., float]]) – List of scalar objective functions f(x) -> float to be minimised simultaneously.

  • constraints (list[dict[str, Any]] | None, default: None) – Scipy-compatible constraint dicts applied to every sub-problem.

  • n_points (int, default: 50) – Number of Pareto-front samples (default 50).

  • bounds (list[tuple[float, float]] | None, default: None) – Variable bounds [(lb, ub), ...].

Return type:

dict[str, Any]

Returns:

Dict with keys points (non-dominated decision vectors, shape (k, n)), objectives (corresponding objective values, shape (k, m)), and n_points (number of non-dominated points found).

Example

>>> import numpy as np
>>> f1 = lambda x: float(x[0] ** 2)
>>> f2 = lambda x: float((x[0] - 1) ** 2)
>>> result = pareto_front([f1, f2], bounds=[(0, 1)], n_points=20)
>>> result['n_points'] > 0
True

See also

nsga2: Evolutionary multi-objective optimizer (handles non-convex). epsilon_constraint: Epsilon-constraint Pareto method.

nsga2(objectives, bounds, pop_size=100, n_gen=200, seed=None)[source]

NSGA-II multi-objective optimisation via pymoo.

Use NSGA-II when you have two or more competing objectives and want to find the Pareto-optimal frontier. In finance, this is used for risk-return trade-offs, cost-tracking trade-offs in execution, and multi-factor portfolio construction. NSGA-II is an evolutionary algorithm that maintains diversity across the Pareto front.

Parameters:
  • objectives (list[Callable[..., float]]) – List of scalar objective functions f(x) -> float, each to be minimised.

  • bounds (list[tuple[float, float]]) – Variable bounds [(lb, ub), ...].

  • pop_size (int, default: 100) – Population size (default 100). Larger populations improve frontier coverage but increase computation.

  • n_gen (int, default: 200) – Number of generations (default 200).

  • seed (int | None, default: None) – Random seed for reproducibility.

Returns:

  • pareto_front: np.ndarray of decision vectors on the Pareto front (shape (n_points, n_var)).

  • pareto_objectives: np.ndarray of objective values (shape (n_points, n_obj)).

  • n_points: number of Pareto-optimal solutions found.

Return type:

dict[str, Any]

Example

>>> import numpy as np
>>> # Minimise x^2 and (x-2)^2 simultaneously
>>> f1 = lambda x: float(x[0] ** 2)
>>> f2 = lambda x: float((x[0] - 2) ** 2)
>>> result = nsga2([f1, f2], bounds=[(0, 3)], pop_size=50, n_gen=100, seed=42)
>>> result['n_points'] > 0
True

References

  • Deb et al. (2002), “A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II”

See also

pareto_front: Weighted-sum Pareto approximation (no pymoo needed). epsilon_constraint: Epsilon-constraint method.

epsilon_constraint(primary_obj, secondary_objs, epsilon_values, bounds=None, constraints=None)[source]

Epsilon-constraint method for multi-objective optimization.

Minimises the primary_obj while constraining each secondary objective to be at most the corresponding epsilon value.

Parameters:
  • primary_obj (Callable[..., float]) – Primary objective function to minimise.

  • secondary_objs (list[Callable[..., float]]) – Secondary objective functions.

  • epsilon_values (list[ndarray[tuple[Any, ...], dtype[floating]]]) – List of 1-D arrays. For each secondary objective k, epsilon_values[k] gives the set of upper-bound values to iterate over. The Cartesian product of all epsilon arrays defines the grid of sub-problems.

  • bounds (list[tuple[float, float]] | None, default: None) – Variable bounds [(lb, ub), ...].

  • constraints (list[dict[str, Any]] | None, default: None) – Additional scipy constraint dicts.

Return type:

dict[str, Any]

Returns:

Dict with keys points (decision vectors), primary_values (primary objective at each point), secondary_values (secondary objective values), and n_points.

weight_constraint(n_assets, lb=0.0, ub=1.0)[source]

Generate uniform bounds for portfolio weights.

Parameters:
  • n_assets (int) – Number of assets.

  • lb (float, default: 0.0) – Lower bound for each weight.

  • ub (float, default: 1.0) – Upper bound for each weight.

Return type:

dict[str, Any]

Returns:

Dict with key bounds — a list of (lb, ub) tuples, one per asset.

sum_to_one_constraint(n_assets)[source]

Equality constraint requiring weights to sum to one.

Compatible with scipy.optimize.minimize() constraint format.

Parameters:

n_assets (int) – Number of assets (used only for documentation / introspection; the constraint function works for any length).

Return type:

dict[str, Any]

Returns:

Scipy-style constraint dict {'type': 'eq', 'fun': ...}.

sector_constraints(n_assets, sectors, sector_limits)[source]

Build inequality constraints for sector / group weight limits.

Parameters:
  • n_assets (int) – Total number of assets.

  • sectors (dict[str, list[int]]) – Mapping of sector name to list of asset indices belonging to that sector.

  • sector_limits (dict[str, tuple[float, float]]) – Mapping of sector name to (min_weight, max_weight) tuple for the aggregate sector weight.

Return type:

list[dict[str, Any]]

Returns:

List of scipy-style constraint dicts ('ineq' type). Two constraints per sector — one for the lower bound and one for the upper bound.

Example

>>> cons = sector_constraints(
...     5,
...     sectors={"tech": [0, 1], "energy": [2, 3, 4]},
...     sector_limits={"tech": (0.1, 0.5), "energy": (0.2, 0.6)},
... )
turnover_constraint(current_weights, max_turnover)[source]

Constraint limiting portfolio turnover.

Turnover is defined as 0.5 * sum(|w_new - w_old|).

Parameters:
Return type:

dict[str, Any]

Returns:

Scipy-style inequality constraint dict.

cardinality_constraint(n_assets, max_holdings)[source]

Information dict describing a cardinality constraint.

Cardinality constraints (limiting the number of non-zero weights) are not directly expressible as smooth inequality constraints for gradient-based solvers. This helper returns a description dict that can be consumed by MILP-based or heuristic optimisers.

Parameters:
  • n_assets (int) – Total number of assets.

  • max_holdings (int) – Maximum number of assets with non-zero weight.

Return type:

dict[str, Any]

Returns:

Dict with keys n_assets, max_holdings, and description.

Portfolio Optimization

Portfolio optimization algorithms.

Implements common portfolio construction methods using scipy.optimize (core dep). For more advanced solvers, see the convex module.

mean_variance(returns, target_return=None, risk_free=0.0, periods_per_year=252, bounds=(0.0, 1.0), shrink=False, shrinkage_method='ledoit_wolf')[source]

Mean-variance optimization (Markowitz).

Use mean-variance optimization to find the portfolio that minimises risk for a given target return (efficient frontier), or maximises the Sharpe ratio when no target is specified. This is the foundation of modern portfolio theory.

Solves:

min w’ Sigma w s.t. w’ mu = target_return

sum(w) = 1 bounds[i][0] <= w[i] <= bounds[i][1]

When target_return is None, maximises (w'mu - rf) / sqrt(w'Sigma w).

Parameters:
  • returns (DataFrame) – Asset return DataFrame (columns = assets). Must contain at least 2 assets and enough observations for a stable covariance estimate.

  • target_return (float | None, default: None) – Target annualised return (None = max Sharpe).

  • risk_free (float, default: 0.0) – Annual risk-free rate for Sharpe calculation.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset (min, max). Use (0, 1) for long-only; (-1, 1) to allow shorting.

  • shrink (bool, default: False) – If True, use a shrinkage estimator for the covariance matrix instead of the sample covariance. Shrinkage produces a better-conditioned matrix when the number of assets is large relative to the number of observations.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method when shrink=True"ledoit_wolf" (default), "oas", or "basic". Forwarded to wraquant.stats.correlation.shrunk_covariance.

Return type:

OptimizationResult

Returns:

OptimizationResult with optimal weights, expected return, volatility, and Sharpe ratio. Access result.weights for the allocation and result.sharpe_ratio for the risk-adjusted metric.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['SPY', 'TLT', 'GLD'])
>>> result = mean_variance(returns, target_return=0.05)
>>> np.isclose(result.weights.sum(), 1.0)
True

Notes

Markowitz optimization is sensitive to estimation error in the mean return vector. Consider black_litterman or hierarchical_risk_parity for more robust alternatives.

See also

max_sharpe: Convenience wrapper for max-Sharpe optimization. min_volatility: Minimum variance portfolio. risk_parity: Equal risk contribution portfolio.

min_volatility(returns, bounds=(0.0, 1.0), periods_per_year=252, shrink=False, shrinkage_method='ledoit_wolf')[source]

Minimum volatility portfolio.

Use the minimum volatility portfolio when your primary objective is risk reduction rather than return maximisation. This portfolio sits at the leftmost point of the efficient frontier and does not require a return estimate, making it more robust than mean-variance to estimation error in expected returns.

Solves: min w’ Sigma w, s.t. sum(w) = 1, bounds.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset (default long-only (0, 1)).

  • periods_per_year (int, default: 252) – Trading periods per year.

  • shrink (bool, default: False) – If True, use a shrinkage covariance estimator.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method ("ledoit_wolf", "oas", or "basic").

Return type:

OptimizationResult

Returns:

OptimizationResult with minimum variance weights. The volatility field gives the lowest achievable portfolio standard deviation.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(0)
>>> returns = pd.DataFrame(np.random.randn(252, 4) * 0.01,
...                        columns=['A', 'B', 'C', 'D'])
>>> result = min_volatility(returns)
>>> result.volatility > 0
True

See also

mean_variance: Full mean-variance with target return. risk_parity: Equal risk contribution (also estimation-robust).

max_sharpe(returns, risk_free=0.0, bounds=(0.0, 1.0), periods_per_year=252, shrink=False, shrinkage_method='ledoit_wolf')[source]

Maximum Sharpe ratio portfolio.

Use max-Sharpe when you want the portfolio with the highest risk-adjusted return. This is the tangency portfolio on the efficient frontier – the point where a line from the risk-free rate is tangent to the frontier.

Maximises: (w’mu - rf) / sqrt(w’Sigma w), s.t. sum(w) = 1, bounds.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • bounds (tuple[float, float], default: (0.0, 1.0)) – Weight bounds per asset.

  • periods_per_year (int, default: 252) – Trading periods per year.

  • shrink (bool, default: False) – If True, use a shrinkage covariance estimator.

  • shrinkage_method (str, default: 'ledoit_wolf') – Shrinkage method ("ledoit_wolf", "oas", or "basic").

Return type:

OptimizationResult

Returns:

OptimizationResult with maximum Sharpe weights. The sharpe_ratio field gives the optimal risk-adjusted return.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['SPY', 'TLT', 'GLD'])
>>> result = max_sharpe(returns, risk_free=0.04)
>>> np.isclose(result.weights.sum(), 1.0)
True

See also

mean_variance: Mean-variance with a target return constraint. min_volatility: Minimum risk portfolio.

risk_parity(returns, periods_per_year=252)[source]

Risk parity (equal risk contribution) portfolio.

Use risk parity when you want each asset to contribute equally to total portfolio risk. Unlike mean-variance, risk parity does not require expected return estimates, making it robust to estimation error. It is the basis of many institutional “all-weather” strategies.

Minimises: sum_i (RC_i / sigma_p - 1/N)^2

where RC_i = w_i * (Sigma w)_i / sigma_p is asset i’s risk contribution and sigma_p is portfolio volatility.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with risk parity weights. Lower-volatility assets receive higher weights; higher-volatility assets receive lower weights.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * np.array([0.01, 0.02, 0.005]),
...                        columns=['Bonds', 'Equity', 'Gold'])
>>> result = risk_parity(returns)
>>> result.weights[0] > result.weights[1]  # bonds get more weight (lower vol)
True

References

  • Maillard, Roncalli & Teiletche (2010), “The Properties of Equally Weighted Risk Contribution Portfolios”

See also

hierarchical_risk_parity: HRP (no inversion of covariance matrix). min_volatility: Minimum variance (not risk-balanced).

equal_weight(returns, periods_per_year=252)[source]

Equal weight portfolio (1/N).

Use the equal-weight portfolio as a robust baseline. Despite its simplicity, 1/N consistently outperforms many optimised portfolios out-of-sample because it avoids estimation error entirely.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with equal weights (each asset receives weight 1/N).

Example

>>> import pandas as pd, numpy as np
>>> returns = pd.DataFrame(np.random.randn(100, 4) * 0.01,
...                        columns=['A', 'B', 'C', 'D'])
>>> result = equal_weight(returns)
>>> np.allclose(result.weights, 0.25)
True

References

  • DeMiguel, Garlappi & Uppal (2009), “Optimal Versus Naive Diversification”

See also

inverse_volatility: Simple vol-weighted alternative. risk_parity: Optimisation-based risk balancing.

inverse_volatility(returns, periods_per_year=252)[source]

Inverse volatility weighted portfolio.

Use inverse-volatility weighting as a simple, estimation-light alternative to mean-variance. Assets with lower volatility receive higher weights, producing a portfolio that tilts toward stability without requiring a full covariance estimate.

Weight_i = (1 / sigma_i) / sum_j(1 / sigma_j)

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with inverse vol weights. Lower-volatility assets receive higher allocations.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(0)
>>> returns = pd.DataFrame(
...     np.random.randn(252, 3) * np.array([0.005, 0.02, 0.01]),
...     columns=['Bonds', 'Equity', 'Gold'])
>>> result = inverse_volatility(returns)
>>> result.weights[0] > result.weights[1]  # Bonds > Equity
True

See also

equal_weight: Uniform weighting (ignores vol entirely). risk_parity: Equalises risk contribution (uses covariance).

hierarchical_risk_parity(returns, periods_per_year=252)[source]

Hierarchical Risk Parity (HRP) by Lopez de Prado.

Use HRP when you want a stable, estimation-robust portfolio that does not require covariance matrix inversion. HRP applies hierarchical clustering to the correlation matrix, then allocates via recursive bisection using inverse variance. This avoids the instability of mean-variance optimisation and produces portfolios that are naturally diversified across asset clusters.

Algorithm:
  1. Compute correlation-based distance and hierarchical linkage.

  2. Quasi-diagonalise the covariance matrix.

  3. Recursively bisect the sorted assets, allocating by inverse variance of each cluster.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with HRP weights. Weights are always positive (long-only) and sum to 1.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 5) * 0.01,
...                        columns=['A', 'B', 'C', 'D', 'E'])
>>> result = hierarchical_risk_parity(returns)
>>> np.isclose(result.weights.sum(), 1.0)
True

References

  • Lopez de Prado (2016), “Building Diversified Portfolios that Outperform Out-of-Sample”

See also

risk_parity: Equal risk contribution (requires covariance inversion). mean_variance: Classical Markowitz (more sensitive to estimation error).

black_litterman(returns, views, tau=0.05, risk_free=0.0, periods_per_year=252)[source]

Black-Litterman model.

Use Black-Litterman when you have subjective views on expected returns for some assets and want to combine them with market equilibrium returns in a Bayesian framework. BL produces more stable and intuitive portfolios than raw mean-variance because it starts from an equilibrium prior (implied by market capitalisation) and blends in your views proportionally to your confidence.

The posterior expected return is:

E[r] = [(tau Sigma)^{-1} + P’ Omega^{-1} P]^{-1}
  • [(tau Sigma)^{-1} pi + P’ Omega^{-1} Q]

where pi = implied equilibrium returns, P = pick matrix, Q = view returns, Omega = view uncertainty.

Parameters:
  • returns (DataFrame) – Asset return DataFrame.

  • views (dict[str, float]) – Dict mapping asset name to expected return view (e.g., {'AAPL': 0.12} means you expect AAPL to return 12% annualised).

  • tau (float, default: 0.05) – Uncertainty scaling parameter (typical range 0.01-0.1). Higher tau gives more weight to your views.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

OptimizationResult

Returns:

OptimizationResult with BL-adjusted weights. The weights reflect a blend of market equilibrium and your views.

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.DataFrame(np.random.randn(252, 3) * 0.01,
...                        columns=['AAPL', 'MSFT', 'GOOG'])
>>> views = {'AAPL': 0.15}  # bullish on AAPL
>>> result = black_litterman(returns, views, tau=0.05)
>>> result.weights[0] > 1 / 3  # AAPL gets more weight
True

References

  • Black & Litterman (1992), “Global Portfolio Optimization”

  • He & Litterman (1999), “The Intuition Behind Black-Litterman”

See also

mean_variance: Pure mean-variance (no views prior). risk_parity: View-free risk-based allocation.

Convex Optimization

Convex optimization wrappers.

Core functions use pure scipy; cvxpy-based solvers are gated behind the optimization optional-dependency group.

minimize_quadratic(Q, c, A_eq=None, b_eq=None, A_ub=None, b_ub=None, bounds=None)[source]

Solve min 0.5 * x’Qx + c’x subject to linear constraints via SLSQP.

Use this for portfolio optimisation (where Q is the covariance matrix), regularised regression, and any convex quadratic problem. The SLSQP solver handles moderate problem sizes (n < 1000) well; for larger problems, use solve_qp with the 'osqp' or 'cvxpy' backend.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution vector), objective (optimal value), status ('optimal' or error message), and success (bool).

Example

>>> import numpy as np
>>> Q = np.array([[2, 0], [0, 2]], dtype=float)
>>> c = np.array([-4, -6], dtype=float)
>>> result = minimize_quadratic(Q, c, bounds=[(0, 10), (0, 10)])
>>> result['success']
True
>>> np.allclose(result['x'], [2, 3], atol=0.1)
True

See also

solve_qp: Multi-backend QP solver (scipy, OSQP, cvxpy).

solve_qp(Q, c, A_eq=None, b_eq=None, A_ub=None, b_ub=None, bounds=None, solver='scipy')[source]

Quadratic program solver with backend dispatch.

Use solve_qp as the primary entry point for quadratic programming. It dispatches to scipy (no extra deps), OSQP (fast first-order solver for large sparse QPs), or cvxpy (general-purpose convex solver).

Solves:

min  0.5 * x' Q x + c' x
s.t. A_eq x  = b_eq
     A_ub x <= b_ub
     lb <= x <= ub
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution), objective (optimal value), status (solver status), and success (bool).

Raises:

ValueError – If solver is not recognised.

Example

>>> import numpy as np
>>> Q = np.eye(3) * 2
>>> c = np.array([-2, -3, -1], dtype=float)
>>> A_eq = np.ones((1, 3))
>>> b_eq = np.array([1.0])
>>> result = solve_qp(Q, c, A_eq=A_eq, b_eq=b_eq,
...                   bounds=[(0, 1)] * 3)
>>> result['success']
True

See also

minimize_quadratic: Scipy-only QP solver. wraquant.opt.portfolio.mean_variance: Portfolio-specific QP.

solve_socp(c, A, b, cone_constraints=None)[source]

Solve a Second-Order Cone Program via cvxpy.

Solves:

min  c' x
s.t. A x == b
     ||A_i x + b_i|| <= c_i' x + d_i   (for each cone constraint)

Each element of cone_constraints is a dict with keys A_cone, b_cone, c_cone, and d_cone.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x, objective, status, and success.

solve_sdp(C, constraints)[source]

Solve a Semidefinite Program via cvxpy.

Solves:

min  tr(C X)
s.t. tr(A_i X) == b_i   for each constraint
     X >> 0              (positive semidefinite)
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys X (optimal matrix), objective, status, and success.

Linear Programming

Linear programming solvers.

All functions use scipy’s built-in LP/MILP solvers and require no optional dependencies.

solve_lp(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None, bounds=None, method='highs')[source]

Solve a linear program via scipy.optimize.linprog().

Use LP for problems with a linear objective and linear constraints, such as transaction cost minimisation, portfolio rebalancing with turnover constraints, or resource allocation.

Solves:

min  c' x
s.t. A_ub x <= b_ub
     A_eq x == b_eq
     lb <= x <= ub
Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (optimal solution vector), objective (optimal value of c’x), status ('optimal' or error), and success (bool).

Example

>>> import numpy as np
>>> # Minimise -x1 - 2*x2 s.t. x1 + x2 <= 4, x1, x2 >= 0
>>> c = np.array([-1, -2], dtype=float)
>>> A_ub = np.array([[1, 1]], dtype=float)
>>> b_ub = np.array([4.0])
>>> result = solve_lp(c, A_ub=A_ub, b_ub=b_ub, bounds=[(0, None), (0, None)])
>>> result['success']
True
>>> np.isclose(result['objective'], -8.0)
True

See also

solve_milp: Mixed-integer LP (handles integer constraints). wraquant.opt.convex.solve_qp: Quadratic programming.

solve_milp(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None, bounds=None, integrality=None)[source]

Solve a mixed-integer linear program via scipy.optimize.milp().

Use MILP when some decision variables must be integers, such as selecting a discrete number of assets to hold, binary buy/sell decisions, or lot-size-constrained portfolio construction.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution – integer variables will be rounded), objective, status, and success.

Example

>>> import numpy as np
>>> # Select 2 assets from 4 (binary selection)
>>> c = np.array([-3, -5, -2, -4], dtype=float)
>>> A_eq = np.ones((1, 4))
>>> b_eq = np.array([2.0])
>>> result = solve_milp(c, A_eq=A_eq, b_eq=b_eq,
...                    bounds=[(0, 1)] * 4,
...                    integrality=[1, 1, 1, 1])
>>> result['success']
True
>>> int(sum(result['x']))  # exactly 2 assets selected
2

See also

solve_lp: Continuous LP (faster, no integer constraints).

transportation_problem(costs, supply, demand)[source]

Solve the transportation / assignment problem as an LP.

Given m supply nodes and n demand nodes, find the shipment matrix X of shape (m, n) that minimises total cost.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (shipment matrix), objective (total cost), status, and success.

Raises:

ValueError – If total supply does not equal total demand.

Nonlinear Optimization

Nonlinear optimization wrappers.

Thin convenience layer over scipy.optimize for general-purpose nonlinear programming, global optimization, and root-finding.

minimize(fun, x0, method='SLSQP', bounds=None, constraints=None, jac=None, hess=None, options=None)[source]

General nonlinear program solver.

Use this for any smooth optimization problem with nonlinear objectives or constraints, such as fitting option pricing models, calibrating yield curves, or custom portfolio objectives with non-convex penalties.

Wrapper around scipy.optimize.minimize() that returns a plain dict instead of an OptimizeResult.

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (solution), fun (objective value at solution), success (bool), and n_iter (iteration count).

Example

>>> import numpy as np
>>> # Minimize Rosenbrock function
>>> def rosenbrock(x):
...     return (1 - x[0])**2 + 100 * (x[1] - x[0]**2)**2
>>> result = minimize(rosenbrock, np.array([0.0, 0.0]))
>>> result['success']
True
>>> np.allclose(result['x'], [1, 1], atol=1e-3)
True

See also

global_minimize: Global optimization for multimodal problems. wraquant.opt.convex.solve_qp: Quadratic programming (convex).

global_minimize(fun, bounds, method='differential_evolution', seed=None, **kwargs)[source]

Global optimisation via stochastic search methods.

Use global optimization when the objective has multiple local minima (e.g., calibrating stochastic volatility models, fitting regime switching parameters, or optimising non-convex trading strategies). Local solvers get trapped; global methods explore the search space before converging.

Supports differential_evolution, dual_annealing, and basinhopping.

Parameters:
  • fun (Callable[..., float]) – Scalar objective function f(x) -> float.

  • bounds (list[tuple[float, float]]) – Search bounds [(lb, ub), ...] for each variable.

  • method (str, default: 'differential_evolution') – Algorithm name – 'differential_evolution' (population-based, most robust), 'dual_annealing' (simulated annealing + local search), or 'basinhopping' (random restarts + local optimization).

  • seed (int | None, default: None) – Random seed for reproducibility.

  • **kwargs (Any) – Additional keyword arguments forwarded to the chosen scipy solver.

Return type:

dict[str, Any]

Returns:

Dict with keys x (best solution found), fun (objective at solution), success (bool), and n_iter.

Raises:

ValueError – If method is not recognised.

Example

>>> import numpy as np
>>> # Rastrigin function (many local minima, global min at origin)
>>> def rastrigin(x):
...     return 20 + sum(xi**2 - 10*np.cos(2*np.pi*xi) for xi in x)
>>> result = global_minimize(rastrigin, [(-5, 5), (-5, 5)], seed=42)
>>> result['fun'] < 1.0  # near global minimum of 0
True

See also

minimize: Local nonlinear solver (faster but gets trapped).

root_find(fun, x0, method='hybr', jac=None)[source]

Find roots of a nonlinear system.

Use root finding for implied volatility calculation (solve BS(sigma) - market_price = 0), yield-to-maturity computation, or any problem where you need to find x such that f(x) = 0.

Wrapper around scipy.optimize.root().

Parameters:
Return type:

dict[str, Any]

Returns:

Dict with keys x (root), fun (residual at root – should be near zero), success (bool), and n_iter (function evaluations or iteration count).

Example

>>> import numpy as np
>>> # Find x such that x^2 - 4 = 0
>>> result = root_find(lambda x: x**2 - 4, np.array([1.0]))
>>> result['success']
True
>>> np.isclose(result['x'][0], 2.0)
True

See also

minimize: Find minimum of a function (not root).

Multi-Objective Optimization

Multi-objective optimization.

Core routines (weighted-sum Pareto approximation and epsilon-constraint) use pure scipy. NSGA-II is gated behind the optimization extra (pymoo).

pareto_front(objectives, constraints=None, n_points=50, bounds=None)[source]

Approximate the Pareto frontier via the weighted-sum method.

Use the weighted-sum approach for a quick Pareto front approximation when you have a convex bi-objective problem (e.g., risk vs. return, cost vs. tracking error). For non-convex problems or more than 2 objectives, use nsga2 instead.

For each of n_points evenly-spaced weight vectors the scalar weighted-sum of the objectives is minimised using SLSQP.

Parameters:
  • objectives (list[Callable[..., float]]) – List of scalar objective functions f(x) -> float to be minimised simultaneously.

  • constraints (list[dict[str, Any]] | None, default: None) – Scipy-compatible constraint dicts applied to every sub-problem.

  • n_points (int, default: 50) – Number of Pareto-front samples (default 50).

  • bounds (list[tuple[float, float]] | None, default: None) – Variable bounds [(lb, ub), ...].

Return type:

dict[str, Any]

Returns:

Dict with keys points (non-dominated decision vectors, shape (k, n)), objectives (corresponding objective values, shape (k, m)), and n_points (number of non-dominated points found).

Example

>>> import numpy as np
>>> f1 = lambda x: float(x[0] ** 2)
>>> f2 = lambda x: float((x[0] - 1) ** 2)
>>> result = pareto_front([f1, f2], bounds=[(0, 1)], n_points=20)
>>> result['n_points'] > 0
True

See also

nsga2: Evolutionary multi-objective optimizer (handles non-convex). epsilon_constraint: Epsilon-constraint Pareto method.

nsga2(objectives, bounds, pop_size=100, n_gen=200, seed=None)[source]

NSGA-II multi-objective optimisation via pymoo.

Use NSGA-II when you have two or more competing objectives and want to find the Pareto-optimal frontier. In finance, this is used for risk-return trade-offs, cost-tracking trade-offs in execution, and multi-factor portfolio construction. NSGA-II is an evolutionary algorithm that maintains diversity across the Pareto front.

Parameters:
  • objectives (list[Callable[..., float]]) – List of scalar objective functions f(x) -> float, each to be minimised.

  • bounds (list[tuple[float, float]]) – Variable bounds [(lb, ub), ...].

  • pop_size (int, default: 100) – Population size (default 100). Larger populations improve frontier coverage but increase computation.

  • n_gen (int, default: 200) – Number of generations (default 200).

  • seed (int | None, default: None) – Random seed for reproducibility.

Returns:

  • pareto_front: np.ndarray of decision vectors on the Pareto front (shape (n_points, n_var)).

  • pareto_objectives: np.ndarray of objective values (shape (n_points, n_obj)).

  • n_points: number of Pareto-optimal solutions found.

Return type:

dict[str, Any]

Example

>>> import numpy as np
>>> # Minimise x^2 and (x-2)^2 simultaneously
>>> f1 = lambda x: float(x[0] ** 2)
>>> f2 = lambda x: float((x[0] - 2) ** 2)
>>> result = nsga2([f1, f2], bounds=[(0, 3)], pop_size=50, n_gen=100, seed=42)
>>> result['n_points'] > 0
True

References

  • Deb et al. (2002), “A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II”

See also

pareto_front: Weighted-sum Pareto approximation (no pymoo needed). epsilon_constraint: Epsilon-constraint method.

epsilon_constraint(primary_obj, secondary_objs, epsilon_values, bounds=None, constraints=None)[source]

Epsilon-constraint method for multi-objective optimization.

Minimises the primary_obj while constraining each secondary objective to be at most the corresponding epsilon value.

Parameters:
  • primary_obj (Callable[..., float]) – Primary objective function to minimise.

  • secondary_objs (list[Callable[..., float]]) – Secondary objective functions.

  • epsilon_values (list[ndarray[tuple[Any, ...], dtype[floating]]]) – List of 1-D arrays. For each secondary objective k, epsilon_values[k] gives the set of upper-bound values to iterate over. The Cartesian product of all epsilon arrays defines the grid of sub-problems.

  • bounds (list[tuple[float, float]] | None, default: None) – Variable bounds [(lb, ub), ...].

  • constraints (list[dict[str, Any]] | None, default: None) – Additional scipy constraint dicts.

Return type:

dict[str, Any]

Returns:

Dict with keys points (decision vectors), primary_values (primary objective at each point), secondary_values (secondary objective values), and n_points.

Base Classes

Base classes for optimization.

class Constraint[source]

Bases: object

Optimization constraint specification.

Parameters:
  • type (str) – Constraint type (‘eq’ for equality, ‘ineq’ for inequality).

  • fun (callable) – Constraint function.

  • name (str, default: '') – Human-readable name.

type: str
fun: callable
name: str = ''
__init__(type, fun, name='')
Parameters:
  • type (str)

  • fun (callable)

  • name (str, default: '')

Return type:

None

class Objective[source]

Bases: object

Optimization objective specification.

Parameters:
  • fun (callable) – Objective function to minimize.

  • name (str, default: '') – Human-readable name.

fun: callable
name: str = ''
__init__(fun, name='')
Parameters:
  • fun (callable)

  • name (str, default: '')

Return type:

None

class OptimizationResult[source]

Bases: object

Result of a portfolio optimization.

Parameters:
  • weights (ndarray[tuple[Any, ...], dtype[floating]]) – Optimal portfolio weights.

  • expected_return (float, default: 0.0) – Expected portfolio return.

  • volatility (float, default: 0.0) – Portfolio volatility (std dev).

  • sharpe_ratio (float, default: 0.0) – Portfolio Sharpe ratio.

  • asset_names (list[str], default: <factory>) – Names of assets.

  • metadata (dict, default: <factory>) – Additional solver-specific information.

weights: ndarray[tuple[Any, ...], dtype[floating]]
expected_return: float = 0.0
volatility: float = 0.0
sharpe_ratio: float = 0.0
asset_names: list[str]
metadata: dict
to_dict()[source]

Return weights as {asset_name: weight} dict.

Return type:

dict[str, float]

__init__(weights, expected_return=0.0, volatility=0.0, sharpe_ratio=0.0, asset_names=<factory>, metadata=<factory>)
Parameters:
Return type:

None

Utilities

Optimization constraint and utility helpers.

Convenience functions for building constraints commonly used in portfolio and general optimisation problems.

weight_constraint(n_assets, lb=0.0, ub=1.0)[source]

Generate uniform bounds for portfolio weights.

Parameters:
  • n_assets (int) – Number of assets.

  • lb (float, default: 0.0) – Lower bound for each weight.

  • ub (float, default: 1.0) – Upper bound for each weight.

Return type:

dict[str, Any]

Returns:

Dict with key bounds — a list of (lb, ub) tuples, one per asset.

sum_to_one_constraint(n_assets)[source]

Equality constraint requiring weights to sum to one.

Compatible with scipy.optimize.minimize() constraint format.

Parameters:

n_assets (int) – Number of assets (used only for documentation / introspection; the constraint function works for any length).

Return type:

dict[str, Any]

Returns:

Scipy-style constraint dict {'type': 'eq', 'fun': ...}.

sector_constraints(n_assets, sectors, sector_limits)[source]

Build inequality constraints for sector / group weight limits.

Parameters:
  • n_assets (int) – Total number of assets.

  • sectors (dict[str, list[int]]) – Mapping of sector name to list of asset indices belonging to that sector.

  • sector_limits (dict[str, tuple[float, float]]) – Mapping of sector name to (min_weight, max_weight) tuple for the aggregate sector weight.

Return type:

list[dict[str, Any]]

Returns:

List of scipy-style constraint dicts ('ineq' type). Two constraints per sector — one for the lower bound and one for the upper bound.

Example

>>> cons = sector_constraints(
...     5,
...     sectors={"tech": [0, 1], "energy": [2, 3, 4]},
...     sector_limits={"tech": (0.1, 0.5), "energy": (0.2, 0.6)},
... )
turnover_constraint(current_weights, max_turnover)[source]

Constraint limiting portfolio turnover.

Turnover is defined as 0.5 * sum(|w_new - w_old|).

Parameters:
Return type:

dict[str, Any]

Returns:

Scipy-style inequality constraint dict.

cardinality_constraint(n_assets, max_holdings)[source]

Information dict describing a cardinality constraint.

Cardinality constraints (limiting the number of non-zero weights) are not directly expressible as smooth inequality constraints for gradient-based solvers. This helper returns a description dict that can be consumed by MILP-based or heuristic optimisers.

Parameters:
  • n_assets (int) – Total number of assets.

  • max_holdings (int) – Maximum number of assets with non-zero weight.

Return type:

dict[str, Any]

Returns:

Dict with keys n_assets, max_holdings, and description.