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 portfoliomin_volatility– minimum variance portfoliorisk_parity– equal risk contributionblack_litterman– equilibrium + views blendinghierarchical_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
Portfolio Construction – Full portfolio construction tutorial
Risk Management (wraquant.risk) – Risk decomposition for optimized portfolios
Regime Detection (wraquant.regimes) – Regime-conditional optimization
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), andtransportation_problem.Nonlinear optimization (
nonlinear) –minimize(local),global_minimize(basin-hopping, differential evolution), androot_find.Multi-objective (
multi_objective) –pareto_front,nsga2(evolutionary multi-objective), andepsilon_constraint.Constraint utilities (
utils) –weight_constraint,sum_to_one_constraint,sector_constraints,turnover_constraint, andcardinality_constraintfor building realistic constraint sets.Result types (
base) –OptimizationResult,Objective, andConstraintdataclasses 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:
objectOptimization constraint specification.
- Parameters:
- fun: callable¶
- class Objective[source]¶
Bases:
objectOptimization objective specification.
- Parameters:
fun (
callable) – Objective function to minimize.name (
str, default:'') – Human-readable name.
- fun: callable¶
- class OptimizationResult[source]¶
Bases:
objectResult 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.
- __init__(weights, expected_return=0.0, volatility=0.0, sharpe_ratio=0.0, asset_names=<factory>, metadata=<factory>)¶
- 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_returnis 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) – IfTrue, 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 whenshrink=True–"ledoit_wolf"(default),"oas", or"basic". Forwarded towraquant.stats.correlation.shrunk_covariance.
- Return type:
- Returns:
OptimizationResult with optimal weights, expected return, volatility, and Sharpe ratio. Access
result.weightsfor the allocation andresult.sharpe_ratiofor 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_littermanorhierarchical_risk_parityfor 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) – IfTrue, use a shrinkage covariance estimator.shrinkage_method (
str, default:'ledoit_wolf') – Shrinkage method ("ledoit_wolf","oas", or"basic").
- Return type:
- Returns:
OptimizationResult with minimum variance weights. The
volatilityfield 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) – IfTrue, use a shrinkage covariance estimator.shrinkage_method (
str, default:'ledoit_wolf') – Shrinkage method ("ledoit_wolf","oas", or"basic").
- Return type:
- Returns:
OptimizationResult with maximum Sharpe weights. The
sharpe_ratiofield 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:
- Return type:
- 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:
- Return type:
- 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:
- Return type:
- 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:
Compute correlation-based distance and hierarchical linkage.
Quasi-diagonalise the covariance matrix.
Recursively bisect the sorted assets, allocating by inverse variance of each cluster.
- Parameters:
- Return type:
- 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:
- 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_qpwith the'osqp'or'cvxpy'backend.- Parameters:
Q (
ndarray[tuple[Any,...],dtype[floating]]) – Positive semi-definite matrix of shape(n, n).c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear cost vector of shape(n,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix,A_eq @ x == b_eq.b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint right-hand side.A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix,A_ub @ x <= b_ub.b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint right-hand side.bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds as[(lb, ub), ...].
- Return type:
- Returns:
Dict with keys
x(solution vector),objective(optimal value),status('optimal'or error message), andsuccess(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_qpas 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:
Q (
ndarray[tuple[Any,...],dtype[floating]]) – Positive semi-definite matrix(n, n).c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear cost vector(n,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix.b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS.A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix.b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS.bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds[(lb, ub), ...].solver (
str, default:'scipy') – Backend –'scipy'(default, no extra deps),'osqp'(fast for large sparse problems, requiresoptimizationextra), or'cvxpy'(most flexible, requiresoptimizationextra).
- Return type:
- Returns:
Dict with keys
x(solution),objective(optimal value),status(solver status), andsuccess(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, andd_cone.- Parameters:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear objective vector(n,).A (
ndarray[tuple[Any,...],dtype[floating]]) – Equality constraint matrix(m, n).b (
ndarray[tuple[Any,...],dtype[floating]]) – Equality constraint RHS(m,).cone_constraints (
list[dict[str,Any]] |None, default:None) – List of SOC constraint dicts. Each dict must containA_cone(matrix),b_cone(vector),c_cone(vector), andd_cone(scalar).
- Return type:
- Returns:
Dict with keys
x,objective,status, andsuccess.
- 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:
- Returns:
Dict with keys
X(optimal matrix),objective,status, andsuccess.
- 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:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Objective coefficient vector(n,).A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix(m_ub, n).b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS(m_ub,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix(m_eq, n).b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS(m_eq,).bounds (
list[tuple[float|None,float|None]] |None, default:None) – Variable bounds[(lb, ub), ...]. UseNonefor unbounded sides.method (
str, default:'highs') – LP solver method (default'highs').
- Return type:
- Returns:
Dict with keys
x(optimal solution vector),objective(optimal value of c’x),status('optimal'or error), andsuccess(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:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Objective coefficient vector(n,).A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix(m_ub, n).b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS(m_ub,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix(m_eq, n).b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS(m_eq,).bounds (
list[tuple[float|None,float|None]] |None, default:None) – Variable bounds[(lb, ub), ...].integrality (
ndarray[tuple[Any,...],dtype[integer]] |list[int] |None, default:None) – Per-variable integrality indicator.0= continuous,1= integer. IfNoneall variables are continuous (equivalent to an LP).
- Return type:
- Returns:
Dict with keys
x(solution – integer variables will be rounded),objective,status, andsuccess.
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
Xof shape(m, n)that minimises total cost.- Parameters:
costs (
ndarray[tuple[Any,...],dtype[floating]]) – Cost matrix(m, n)wherecosts[i, j]is the per-unit shipping cost from supply i to demand j.supply (
ndarray[tuple[Any,...],dtype[floating]]) – Supply capacities(m,).demand (
ndarray[tuple[Any,...],dtype[floating]]) – Demand requirements(n,).
- Return type:
- Returns:
Dict with keys
x(shipment matrix),objective(total cost),status, andsuccess.- 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 anOptimizeResult.- Parameters:
fun (
Callable[...,float]) – Scalar objective functionf(x) -> float.x0 (
ndarray[tuple[Any,...],dtype[floating]]) – Initial guess(n,). A good starting point is critical for nonlinear problems.method (
str, default:'SLSQP') – Optimisation algorithm (e.g.'SLSQP','L-BFGS-B','trust-constr').bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds[(lb, ub), ...].constraints (
list[dict[str,Any]] |None, default:None) – List of constraint dicts accepted by scipy.jac (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,None], default:None) – Jacobian of fun or a string method ('2-point','3-point','cs'). Providing an analytical Jacobian significantly improves speed and convergence.hess (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,None], default:None) – Hessian of fun or a string method.options (
dict[str,Any] |None, default:None) – Solver-specific options dict (e.g.{'maxiter': 1000}).
- Return type:
- Returns:
Dict with keys
x(solution),fun(objective value at solution),success(bool), andn_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, andbasinhopping.- Parameters:
fun (
Callable[...,float]) – Scalar objective functionf(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:
- Returns:
Dict with keys
x(best solution found),fun(objective at solution),success(bool), andn_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:
fun (
Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]] |float]) – Vector functionf(x) -> arraywhose root is sought.x0 (
ndarray[tuple[Any,...],dtype[floating]]) – Initial guess(n,).method (
str, default:'hybr') – Solver algorithm (e.g.'hybr'(default, hybrid Powell),'lm'(Levenberg-Marquardt),'broyden1').jac (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,bool,None], default:None) – Jacobian or a flag/string for finite-difference approximation.
- Return type:
- Returns:
Dict with keys
x(root),fun(residual at root – should be near zero),success(bool), andn_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
nsga2instead.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 functionsf(x) -> floatto 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:
- Returns:
Dict with keys
points(non-dominated decision vectors, shape(k, n)),objectives(corresponding objective values, shape(k, m)), andn_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 functionsf(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:
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 objectivek,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:
- Returns:
Dict with keys
points(decision vectors),primary_values(primary objective at each point),secondary_values(secondary objective values), andn_points.
- sum_to_one_constraint(n_assets)[source]¶
Equality constraint requiring weights to sum to one.
Compatible with
scipy.optimize.minimize()constraint format.
- sector_constraints(n_assets, sectors, sector_limits)[source]¶
Build inequality constraints for sector / group weight limits.
- Parameters:
- Return type:
- 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|).
- 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.
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_returnis 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) – IfTrue, 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 whenshrink=True–"ledoit_wolf"(default),"oas", or"basic". Forwarded towraquant.stats.correlation.shrunk_covariance.
- Return type:
- Returns:
OptimizationResult with optimal weights, expected return, volatility, and Sharpe ratio. Access
result.weightsfor the allocation andresult.sharpe_ratiofor 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_littermanorhierarchical_risk_parityfor 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) – IfTrue, use a shrinkage covariance estimator.shrinkage_method (
str, default:'ledoit_wolf') – Shrinkage method ("ledoit_wolf","oas", or"basic").
- Return type:
- Returns:
OptimizationResult with minimum variance weights. The
volatilityfield 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) – IfTrue, use a shrinkage covariance estimator.shrinkage_method (
str, default:'ledoit_wolf') – Shrinkage method ("ledoit_wolf","oas", or"basic").
- Return type:
- Returns:
OptimizationResult with maximum Sharpe weights. The
sharpe_ratiofield 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:
- Return type:
- 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:
- Return type:
- 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:
- Return type:
- 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:
Compute correlation-based distance and hierarchical linkage.
Quasi-diagonalise the covariance matrix.
Recursively bisect the sorted assets, allocating by inverse variance of each cluster.
- Parameters:
- Return type:
- 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:
- 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_qpwith the'osqp'or'cvxpy'backend.- Parameters:
Q (
ndarray[tuple[Any,...],dtype[floating]]) – Positive semi-definite matrix of shape(n, n).c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear cost vector of shape(n,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix,A_eq @ x == b_eq.b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint right-hand side.A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix,A_ub @ x <= b_ub.b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint right-hand side.bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds as[(lb, ub), ...].
- Return type:
- Returns:
Dict with keys
x(solution vector),objective(optimal value),status('optimal'or error message), andsuccess(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_qpas 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:
Q (
ndarray[tuple[Any,...],dtype[floating]]) – Positive semi-definite matrix(n, n).c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear cost vector(n,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix.b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS.A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix.b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS.bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds[(lb, ub), ...].solver (
str, default:'scipy') – Backend –'scipy'(default, no extra deps),'osqp'(fast for large sparse problems, requiresoptimizationextra), or'cvxpy'(most flexible, requiresoptimizationextra).
- Return type:
- Returns:
Dict with keys
x(solution),objective(optimal value),status(solver status), andsuccess(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, andd_cone.- Parameters:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Linear objective vector(n,).A (
ndarray[tuple[Any,...],dtype[floating]]) – Equality constraint matrix(m, n).b (
ndarray[tuple[Any,...],dtype[floating]]) – Equality constraint RHS(m,).cone_constraints (
list[dict[str,Any]] |None, default:None) – List of SOC constraint dicts. Each dict must containA_cone(matrix),b_cone(vector),c_cone(vector), andd_cone(scalar).
- Return type:
- Returns:
Dict with keys
x,objective,status, andsuccess.
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:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Objective coefficient vector(n,).A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix(m_ub, n).b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS(m_ub,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix(m_eq, n).b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS(m_eq,).bounds (
list[tuple[float|None,float|None]] |None, default:None) – Variable bounds[(lb, ub), ...]. UseNonefor unbounded sides.method (
str, default:'highs') – LP solver method (default'highs').
- Return type:
- Returns:
Dict with keys
x(optimal solution vector),objective(optimal value of c’x),status('optimal'or error), andsuccess(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:
c (
ndarray[tuple[Any,...],dtype[floating]]) – Objective coefficient vector(n,).A_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint matrix(m_ub, n).b_ub (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Inequality constraint RHS(m_ub,).A_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint matrix(m_eq, n).b_eq (
ndarray[tuple[Any,...],dtype[floating]] |None, default:None) – Equality constraint RHS(m_eq,).bounds (
list[tuple[float|None,float|None]] |None, default:None) – Variable bounds[(lb, ub), ...].integrality (
ndarray[tuple[Any,...],dtype[integer]] |list[int] |None, default:None) – Per-variable integrality indicator.0= continuous,1= integer. IfNoneall variables are continuous (equivalent to an LP).
- Return type:
- Returns:
Dict with keys
x(solution – integer variables will be rounded),objective,status, andsuccess.
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
Xof shape(m, n)that minimises total cost.- Parameters:
costs (
ndarray[tuple[Any,...],dtype[floating]]) – Cost matrix(m, n)wherecosts[i, j]is the per-unit shipping cost from supply i to demand j.supply (
ndarray[tuple[Any,...],dtype[floating]]) – Supply capacities(m,).demand (
ndarray[tuple[Any,...],dtype[floating]]) – Demand requirements(n,).
- Return type:
- Returns:
Dict with keys
x(shipment matrix),objective(total cost),status, andsuccess.- 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 anOptimizeResult.- Parameters:
fun (
Callable[...,float]) – Scalar objective functionf(x) -> float.x0 (
ndarray[tuple[Any,...],dtype[floating]]) – Initial guess(n,). A good starting point is critical for nonlinear problems.method (
str, default:'SLSQP') – Optimisation algorithm (e.g.'SLSQP','L-BFGS-B','trust-constr').bounds (
list[tuple[float,float]] |None, default:None) – Variable bounds[(lb, ub), ...].constraints (
list[dict[str,Any]] |None, default:None) – List of constraint dicts accepted by scipy.jac (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,None], default:None) – Jacobian of fun or a string method ('2-point','3-point','cs'). Providing an analytical Jacobian significantly improves speed and convergence.hess (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,None], default:None) – Hessian of fun or a string method.options (
dict[str,Any] |None, default:None) – Solver-specific options dict (e.g.{'maxiter': 1000}).
- Return type:
- Returns:
Dict with keys
x(solution),fun(objective value at solution),success(bool), andn_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, andbasinhopping.- Parameters:
fun (
Callable[...,float]) – Scalar objective functionf(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:
- Returns:
Dict with keys
x(best solution found),fun(objective at solution),success(bool), andn_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:
fun (
Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]] |float]) – Vector functionf(x) -> arraywhose root is sought.x0 (
ndarray[tuple[Any,...],dtype[floating]]) – Initial guess(n,).method (
str, default:'hybr') – Solver algorithm (e.g.'hybr'(default, hybrid Powell),'lm'(Levenberg-Marquardt),'broyden1').jac (
Union[Callable[...,ndarray[tuple[Any,...],dtype[TypeVar(_ScalarT, bound=generic)]]],str,bool,None], default:None) – Jacobian or a flag/string for finite-difference approximation.
- Return type:
- Returns:
Dict with keys
x(root),fun(residual at root – should be near zero),success(bool), andn_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
nsga2instead.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 functionsf(x) -> floatto 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:
- Returns:
Dict with keys
points(non-dominated decision vectors, shape(k, n)),objectives(corresponding objective values, shape(k, m)), andn_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 functionsf(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:
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 objectivek,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:
- Returns:
Dict with keys
points(decision vectors),primary_values(primary objective at each point),secondary_values(secondary objective values), andn_points.
Base Classes¶
Base classes for optimization.
- class Constraint[source]¶
Bases:
objectOptimization constraint specification.
- Parameters:
- fun: callable¶
- class Objective[source]¶
Bases:
objectOptimization objective specification.
- Parameters:
fun (
callable) – Objective function to minimize.name (
str, default:'') – Human-readable name.
- fun: callable¶
- class OptimizationResult[source]¶
Bases:
objectResult 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.
- __init__(weights, expected_return=0.0, volatility=0.0, sharpe_ratio=0.0, asset_names=<factory>, metadata=<factory>)¶
Utilities¶
Optimization constraint and utility helpers.
Convenience functions for building constraints commonly used in portfolio and general optimisation problems.
- sum_to_one_constraint(n_assets)[source]¶
Equality constraint requiring weights to sum to one.
Compatible with
scipy.optimize.minimize()constraint format.
- sector_constraints(n_assets, sectors, sector_limits)[source]¶
Build inequality constraints for sector / group weight limits.
- Parameters:
- Return type:
- 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|).
- 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.