Optimizers¶
All optimizers share the method(returns, ...) → PortfolioResult interface. See
the Optimizers guide for the mathematics and when to
use each.
Classical¶
classical
¶
Traditional / classical portfolio optimizers.
All optimizers share the method(returns, ...) -> PortfolioResult shape and
delegate constrained problems to the projected-gradient solver in
:mod:jaxfolio.optimizers.base. Objectives are defined as module-level
functions objective(w, params) (rather than per-call closures) so the
solver's jit cache hits across repeated calls — e.g. every rebalance of a
backtest recompiles nothing after the first solve.
Closed-form / exact solvers are used where they exist (inverse-volatility, risk-parity coordinate descent) and reported alongside the constrained result.
equal_weight
¶
equal_weight(returns) -> PortfolioResult
Equal-weight (1/N) portfolio — a famously hard-to-beat baseline.
Source code in src/jaxfolio/optimizers/classical.py
inverse_volatility
¶
inverse_volatility(returns) -> PortfolioResult
Inverse-volatility (naive risk parity) weighting.
Source code in src/jaxfolio/optimizers/classical.py
minimum_variance
¶
minimum_variance(
returns, config: OptimizerConfig | None = None
) -> PortfolioResult
Global minimum-variance portfolio.
Solves min w' Sigma w subject to the configured constraints via the
spectral projected-gradient solver, which converges to the exact
minimum-variance frontier vertex.
Source code in src/jaxfolio/optimizers/classical.py
mean_variance
¶
mean_variance(
returns,
risk_aversion: float = 1.0,
config: OptimizerConfig | None = None,
) -> PortfolioResult
Markowitz mean-variance portfolio.
Maximizes w'mu - (risk_aversion/2) w'Sigma w (equivalently minimizes its
negative) under the configured constraints. Larger risk_aversion tilts
toward lower variance.
Source code in src/jaxfolio/optimizers/classical.py
maximum_sharpe
¶
maximum_sharpe(
returns, config: OptimizerConfig | None = None
) -> PortfolioResult
Maximum-Sharpe (tangency) portfolio.
Minimizes the negative Sharpe ratio under the configured constraints. The Sharpe ratio is scale-invariant, so we optimize on the simplex directly.
Source code in src/jaxfolio/optimizers/classical.py
maximum_diversification
¶
maximum_diversification(
returns, config: OptimizerConfig | None = None
) -> PortfolioResult
Maximum-diversification portfolio (Choueifaty & Coignard, 2008).
Maximizes the diversification ratio (w'sigma) / sqrt(w'Sigma w) where
sigma is the vector of asset volatilities.
Source code in src/jaxfolio/optimizers/classical.py
risk_parity
¶
risk_parity(
returns, config: OptimizerConfig | None = None
) -> PortfolioResult
Equal-risk-contribution (ERC) / risk-parity portfolio.
Each asset contributes equally to total portfolio risk. Solved with the
cyclical coordinate descent of Griveau-Billion, Richard & Roncalli (2013) on
the convex program min 0.5 x'Sigma x - (1/N) sum(log x). Each coordinate
update solves a one-dimensional quadratic in closed form,
x_i <- (-beta_i + sqrt(beta_i^2 + 4 sigma_ii b_i)) / (2 sigma_ii) with
beta_i = (Sigma x)_i - sigma_ii x_i, which keeps x_i strictly
positive and is provably convergent regardless of covariance scale. Weights
are the normalized fixed point.
Source code in src/jaxfolio/optimizers/classical.py
kelly
¶
kelly(
returns, config: OptimizerConfig | None = None
) -> PortfolioResult
Approximate Kelly-optimal (log-growth) portfolio.
Maximizes E[log(1 + w'r)] estimated over the sample paths, which is the
growth-optimal criterion. Uses the projected-gradient solver over the return
matrix directly.
Source code in src/jaxfolio/optimizers/classical.py
min_cvar
¶
min_cvar(
returns,
alpha: float = 0.95,
config: OptimizerConfig | None = None,
) -> PortfolioResult
Minimum Conditional-Value-at-Risk portfolio (Rockafellar & Uryasev, 2000).
Minimizes CVaR at confidence alpha using the smooth auxiliary-variable
formulation CVaR = tau + 1/((1-alpha)T) sum(max(loss - tau, 0)), jointly
optimizing weights w and the VaR threshold tau.
The packed (w, tau) variable (tau unconstrained) and the piecewise
objective are ill-suited to a single scalar Barzilai-Borwein step, so this
optimizer uses the Adam solver regardless of config.solver.
Source code in src/jaxfolio/optimizers/classical.py
black_litterman
¶
black_litterman(
returns,
views: dict[str, float] | None = None,
*,
view_confidence: float = 0.5,
tau: float = 0.05,
risk_aversion: float = 2.5,
market_weights: ndarray | None = None,
config: OptimizerConfig | None = None,
) -> PortfolioResult
Black-Litterman portfolio blending market equilibrium with investor views.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
views
|
dict[str, float] | None
|
Absolute views mapping asset name -> expected (per-period) return. Only
assets present in the panel are used. If |
None
|
view_confidence
|
float
|
Scalar in |
0.5
|
tau
|
float
|
Uncertainty scaling on the prior covariance. |
0.05
|
risk_aversion
|
float
|
Market risk-aversion used for reverse optimization of equilibrium returns. |
2.5
|
market_weights
|
ndarray | None
|
Prior (market-cap) weights; defaults to equal weight. |
None
|
Source code in src/jaxfolio/optimizers/classical.py
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 | |
Learning-based¶
learning
¶
Learning-based portfolio optimizers.
Two representative methods:
- :func:
deep_sharpe— an end-to-end differentiable allocation policy: a small MLP maps a window of recent returns to portfolio weights (via softmax), and is trained by gradient ascent to maximize the realized Sharpe ratio of the resulting portfolio. Pure JAX + optax, no flax dependency. - :func:
online_gradient— the exponentiated-gradient online portfolio (a Cover-style universal portfolio update) with sub-linear regret guarantees.
Both return a :class:PortfolioResult holding the final allocation; deep_sharpe
additionally stores the trained parameters so it can be rolled forward.
deep_sharpe
¶
deep_sharpe(
returns,
*,
lookback: int = 60,
hidden: tuple[int, ...] = (64, 32),
epochs: int = 300,
learning_rate: float = 0.001,
seed: int = 0,
) -> PortfolioResult
Train a differentiable MLP allocation policy to maximize in-sample Sharpe.
The policy consumes the flattened trailing lookback window of returns and
emits long-only weights. Training maximizes the annualized Sharpe of the
strategy's realized returns across all windows. The reported allocation is
the policy applied to the most recent window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lookback
|
int
|
Length of the trailing return window fed to the policy. |
60
|
hidden
|
tuple[int, ...]
|
Hidden layer widths of the MLP. |
(64, 32)
|
epochs
|
int
|
Number of full-batch gradient ascent steps. |
300
|
Source code in src/jaxfolio/optimizers/learning.py
online_gradient
¶
online_gradient(
returns, *, eta: float = 0.05
) -> PortfolioResult
Exponentiated-gradient (EG) universal online portfolio (Helmbold et al.).
Sequentially updates weights multiplicatively by the realized asset returns:
w_{t+1,i} ∝ w_{t,i} * exp(eta * r_{t,i} / (w_t . r_t)). This achieves
sub-linear regret versus the best constant-rebalanced portfolio in hindsight.
The reported allocation is the final weight vector; the wealth path is stored
in the metadata.
Source code in src/jaxfolio/optimizers/learning.py
Graph-based¶
graph
¶
Graph- and hierarchy-based portfolio optimizers.
- :func:
hierarchical_risk_parity— López de Prado's HRP: cluster assets by a correlation-distance dendrogram, quasi-diagonalize, then recursively bisect and allocate by inverse-variance. - :func:
hierarchical_equal_risk— HERC: like HRP but splits capital across the dendrogram's natural clusters by equal risk contribution. - :func:
mst_centrality— build the minimum spanning tree of the correlation network and weight assets by inverse degree/centrality (peripheral, less systemically coupled assets get more capital).
SciPy provides linkage and the MST; all allocation math stays in numpy.
hierarchical_risk_parity
¶
hierarchical_risk_parity(
returns, *, linkage_method: str = "single"
) -> PortfolioResult
Hierarchical Risk Parity (López de Prado, 2016).
Source code in src/jaxfolio/optimizers/graph.py
hierarchical_equal_risk
¶
hierarchical_equal_risk(
returns,
*,
n_clusters: int = 4,
linkage_method: str = "ward",
) -> PortfolioResult
Hierarchical Equal Risk Contribution (HERC).
Cut the dendrogram into n_clusters groups, allocate capital across
clusters by inverse cluster-variance, and within each cluster by inverse
variance. A robust, less order-sensitive cousin of HRP.
Source code in src/jaxfolio/optimizers/graph.py
mst_centrality
¶
mst_centrality(
returns, *, alpha: float = 1.0
) -> PortfolioResult
Minimum-spanning-tree centrality allocation.
Build the MST of the correlation-distance network; assets with lower degree
centrality (more peripheral, less coupled to the market core) receive more
weight: w_i ∝ 1 / (degree_i)^alpha.
Source code in src/jaxfolio/optimizers/graph.py
Solver core¶
The shared projected-gradient solver and portfolio-math primitives underlying the constrained classical methods.
base
¶
Projected-gradient solver shared by the constrained classical optimizers.
Every constrained objective (min-variance, mean-variance, max-Sharpe,
max-diversification, CVaR, ...) is minimized by the same routine: take a
gradient step on an unconstrained objective(w), then project w back onto
the feasible set. The loop runs inside jax.lax.while_loop so the whole solve
is a single jit-compiled kernel.
Two solvers are available:
"spg"(default) — spectral projected gradient with Barzilai-Borwein step sizes. It needs no learning-rate tuning: the step adapts to the local curvature every iteration, so it converges to the constrained optimum (matching a dedicated QP solver) in far fewer iterations than a fixed-step method. Convergence is measured by the projected-gradient (KKT stationarity) norm||w - P(w - ∇f)||."adam"— the classic fixed-step optax-Adam projected-gradient loop. Its smooth dynamics are preferable when differentiating through the optimizer to train an allocation policy, and it handles the non-smooth CVaR objective (with its free auxiliary variable) more gracefully than a single scalar BB step.
Performance note: :func:solve_constrained is jit-cached on the identity of
the objective/projection functions, so calling a built-in optimizer repeatedly
(e.g. at every backtest rebalance) compiles once and reuses the kernel. The
public :func:solve_projected_gradient accepts an arbitrary Python closure and
therefore re-traces per call — fine for one-off custom strategies.
make_projection
¶
make_projection(
long_only: bool,
weight_bounds: tuple[float, float],
budget: float = 1.0,
) -> Callable[[Array], Array]
Return the w -> w projection matching the requested constraint set.
Source code in src/jaxfolio/optimizers/base.py
select_projection
¶
Return (projection_fn, pparams) for the cached kernel.
aux=True selects the variant that projects only the weight-block of a
packed [w, tau] vector (used by the CVaR optimizer). pparams is a
tuple of Python floats — passed as a traced argument to the kernel.
Source code in src/jaxfolio/optimizers/base.py
solve_constrained
¶
solve_constrained(
objective,
obj_params,
w0: Array,
projection,
proj_params,
*,
solver: str = "spg",
learning_rate: float | None = None,
max_iter: int = 2000,
tol: float = 1e-07,
) -> tuple[Array, dict[str, Array]]
Cached entry point for the built-in optimizers.
objective and projection must be module-level functions (see
:mod:jaxfolio.optimizers.classical) so the underlying jit cache hits.
Returns (weights, info) with info["iterations"].
Source code in src/jaxfolio/optimizers/base.py
solve_projected_gradient
¶
solve_projected_gradient(
objective: Objective,
w0: Array,
projection: Callable[[Array], Array],
*,
learning_rate: float | None = None,
max_iter: int = 2000,
tol: float = 1e-07,
solver: str = "spg",
) -> tuple[Array, dict[str, Array]]
Minimize objective over the feasible set defined by projection.
Accepts arbitrary Python closures objective(w) -> scalar and
projection(w) -> w (the shape used by custom strategies and the
toolkit). Because the closure identity changes per call this re-traces
each time — for hot loops over a built-in optimizer, prefer the cached
:func:solve_constrained. Returns (weights, info).
Source code in src/jaxfolio/optimizers/base.py
portfolio_return
¶
portfolio_variance
¶
portfolio_volatility
¶
sharpe_ratio
¶
Sharpe ratio of a weight vector given moments.