Toolkit & custom strategies¶
The public building blocks for authoring strategies. See the Custom strategies guide.
Toolkit¶
toolkit
¶
Public building blocks for authoring custom portfolio strategies.
This module is the stable, documented surface for writing your own optimizer. Everything a built-in optimizer uses internally is re-exported here so a custom strategy can be written the same idiomatic way:
from jaxfolio import toolkit as tk def my_strategy(returns): ... mu, cov, names, _ = tk.moments(returns) ... projection = tk.make_projection(long_only=True, weight_bounds=(0.0, 1.0)) ... def objective(w): ... return w @ cov @ w - 0.1 * (w @ mu) # min-variance with a return tilt ... w, _info = tk.solve_projected_gradient(objective, tk.equal_start(len(names)), projection) ... return tk.finalize_result(w, names, "My Strategy", mu=mu, cov=cov)
The heavy lifting (annualized diagnostics, weight cleanup) lives in
:func:finalize_result, which the built-in classical and graph optimizers also
use, so custom results are indistinguishable from built-in ones downstream
(backtester, compare, plots).
__all__
module-attribute
¶
__all__ = [
"as_matrix",
"moments",
"mean_returns",
"sample_covariance",
"ewma_covariance",
"ledoit_wolf_covariance",
"correlation_from_covariance",
"make_projection",
"project_simplex",
"project_box_budget",
"normalize_weights",
"softmax_weights",
"solve_projected_gradient",
"solve_constrained",
"select_projection",
"equal_start",
"portfolio_return",
"portfolio_variance",
"portfolio_volatility",
"sharpe_ratio",
"finalize_result",
"PERIODS_PER_YEAR",
"PortfolioResult",
"OptimizerConfig",
]
OptimizerConfig
dataclass
¶
OptimizerConfig(
risk_free_rate: float = 0.0,
long_only: bool = True,
weight_bounds: tuple[float, float] | None = None,
max_iter: int = 2000,
solver: str = "spg",
learning_rate: float | None = None,
tol: float = 1e-07,
l2_reg: float = 0.0,
)
Configuration shared by the projected-gradient classical optimizers.
Attributes:
| Name | Type | Description |
|---|---|---|
risk_free_rate |
float
|
Per-period risk-free rate used by Sharpe-style objectives. |
long_only |
bool
|
If |
weight_bounds |
tuple[float, float] | None
|
Explicit |
max_iter |
int
|
Maximum projected-gradient iterations. |
solver |
str
|
Which projected-gradient solver to use. |
learning_rate |
float | None
|
Step size for the solver. |
tol |
float
|
Convergence tolerance. For |
l2_reg |
float
|
Optional L2 penalty on weights (encourages diversification). |
bounds
¶
Resolve the effective per-asset weight bounds.
Honors an explicit weight_bounds; otherwise defaults to (0, 1)
for a long-only portfolio and (-1, 1) when shorting is allowed.
Source code in src/jaxfolio/types.py
PortfolioResult
dataclass
¶
PortfolioResult(
weights: ndarray,
assets: list[str],
method: str,
expected_return: float | None = None,
volatility: float | None = None,
sharpe: float | None = None,
metadata: dict[str, Any] = dict(),
)
The output of an optimizer: weights plus diagnostics.
Attributes:
| Name | Type | Description |
|---|---|---|
weights |
ndarray
|
Portfolio weights as a 1-D numpy array (sums to 1). |
assets |
list[str]
|
Asset names aligned with |
method |
str
|
Human-readable optimizer name. |
expected_return |
float | None
|
Annualized (or per-period, if unscaled) expected portfolio return. |
volatility |
float | None
|
Portfolio volatility on the same scale as |
sharpe |
float | None
|
Sharpe ratio implied by |
metadata |
dict[str, Any]
|
Free-form diagnostics (iterations, converged flag, cluster labels, ...). |
as_dict
¶
Return {asset: weight} sorted by descending absolute weight.
normalize_weights
¶
Rescale weights to sum to budget (assumes a non-zero sum).
Source code in src/jaxfolio/constraints/projections.py
project_box_budget
¶
project_box_budget(
v: Array,
lower: float = 0.0,
upper: float = 1.0,
budget: float = 1.0,
*,
max_iter: int = 50,
tol: float = 1e-10,
) -> Array
Project v onto {w : lower <= w <= upper, sum(w) = budget}.
Solves for the Lagrange multiplier tau on the budget constraint by
bisection: w(tau) = clip(v - tau, lower, upper) is monotone decreasing in
tau, so we bisect until sum(w(tau)) == budget. Feasible whenever
n*lower <= budget <= n*upper.
Source code in src/jaxfolio/constraints/projections.py
project_simplex
¶
Euclidean projection of v onto the scaled simplex summing to budget.
Implements the classic Duchi et al. (2008) sort-based algorithm, which is exact and differentiable almost everywhere.
Source code in src/jaxfolio/constraints/projections.py
softmax_weights
¶
Map unconstrained logits to long-only weights via softmax (sums to budget).
as_matrix
¶
Return (matrix, asset_names) from a returns object.
Accepts a pandas DataFrame (columns become asset names), or a raw array
(names default to asset_0...). NaNs are replaced with zeros.
Source code in src/jaxfolio/moments/estimators.py
correlation_from_covariance
¶
Convert a covariance matrix to a correlation matrix.
ewma_covariance
¶
ewma_covariance(
returns: Array,
*,
halflife: float = 63.0,
periods_per_year: int | None = None,
) -> Array
Exponentially-weighted covariance matrix.
Recent observations receive more weight; halflife is in periods (default
~one quarter of trading days).
Source code in src/jaxfolio/moments/estimators.py
ledoit_wolf_covariance
¶
ledoit_wolf_covariance(
returns: Array, *, periods_per_year: int | None = None
) -> tuple[Array, float]
Ledoit-Wolf shrinkage toward a scaled-identity target.
Returns (shrunk_cov, shrinkage_intensity). The shrinkage intensity is
estimated analytically (Ledoit & Wolf, 2004) and clipped to [0, 1].
Source code in src/jaxfolio/moments/estimators.py
mean_returns
¶
Sample mean of returns; annualized if periods_per_year is given.
Source code in src/jaxfolio/moments/estimators.py
sample_covariance
¶
Sample covariance matrix (denominator T - 1); optionally annualized.
Requires at least two observations; a single row has no defined covariance.
Source code in src/jaxfolio/moments/estimators.py
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
portfolio_return
¶
portfolio_variance
¶
portfolio_volatility
¶
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
sharpe_ratio
¶
Sharpe ratio of a weight vector given moments.
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
equal_start
¶
finalize_result
¶
finalize_result(
weights,
assets: list[str],
method: str,
*,
mu: Array | None = None,
cov: Array | None = None,
returns=None,
risk_free: float = 0.0,
periods_per_year: int = PERIODS_PER_YEAR,
metadata: dict | None = None,
clean_eps: float = 1e-10,
) -> PortfolioResult
Assemble a :class:PortfolioResult with annualized diagnostics.
Provide the moments either directly (mu and cov) or implicitly via
returns (sample moments are computed from it). Weights are coerced to a
flat float array and values below clean_eps in magnitude are zeroed. This
is the single source of truth for the annualized return / volatility / Sharpe
reported on every optimizer result — built-in and custom alike.
Source code in src/jaxfolio/results.py
moments
¶
Return (mu, cov, asset_names, return_matrix) from a returns object.
cov_estimator optionally overrides the default sample covariance.
Source code in src/jaxfolio/results.py
Custom strategy authoring¶
custom
¶
Author your own portfolio strategy with minimal boilerplate.
Two entry points, both producing a proper :class:PortfolioResult that works
everywhere a built-in optimizer does (backtester, compare, plots):
- :func:
custom_strategy/ :meth:CustomStrategy.from_weights— wrap a function that maps returns to a weight vector (array) or a{asset: weight}mapping. Weights are validated, optionally renormalized, and annualized diagnostics are attached automatically. - :meth:
CustomStrategy.from_objective— provide a JAX objectivef(w, ctx)and let the shared projected-gradient solver optimize it under the configured constraints (this is exactly how the built-in classical optimizers are built).
Both optionally register the resulting strategy by name via :mod:jaxfolio.registry.
CustomStrategy
¶
CustomStrategy(
name: str, fn: Callable[[object], PortfolioResult]
)
A user-defined strategy exposed with the standard optimizer interface.
Instances are callable as strategy(returns) -> PortfolioResult, so they
drop straight into :func:jaxfolio.backtest.compare and the plotting layer.
Source code in src/jaxfolio/custom.py
from_weights
classmethod
¶
from_weights(
name: str,
weight_fn: Callable[[object], object],
*,
renormalize: bool = True,
risk_free: float = 0.0,
register: bool = False,
description: str = "",
) -> CustomStrategy
Wrap a returns -> weights function into a full strategy.
weight_fn may return a numpy/JAX array aligned to the asset columns or
a {asset: weight} mapping. If renormalize is True (default)
and the weights sum to a non-zero value, they are scaled to sum to 1.
Source code in src/jaxfolio/custom.py
from_objective
classmethod
¶
from_objective(
name: str,
objective_fn: Callable[[Array, _Context], Array],
*,
config: OptimizerConfig | None = None,
register: bool = False,
description: str = "",
) -> CustomStrategy
Build a strategy by minimizing a JAX objective under constraints.
objective_fn(w, ctx) must be JAX-differentiable and return a scalar to
minimize; ctx is a :class:_Context exposing mu, cov,
returns, assets, n. The shared projected-gradient solver and
the configured constraint set (long-only / bounds) are reused verbatim.
Source code in src/jaxfolio/custom.py
custom_strategy
¶
custom_strategy(
name: str,
weight_fn: Callable[[object], object],
*,
renormalize: bool = True,
risk_free: float = 0.0,
register: bool = False,
description: str = "",
) -> CustomStrategy
Shorthand for :meth:CustomStrategy.from_weights (the common case).
Source code in src/jaxfolio/custom.py
Result assembly¶
results
¶
Low-level result assembly and moment extraction.
Kept dependency-light on purpose: this module imports only the moment estimators
(never the optimizer package), so both the optimizers and the higher-level
:mod:jaxfolio.toolkit can import it without creating an import cycle.
moments
¶
Return (mu, cov, asset_names, return_matrix) from a returns object.
cov_estimator optionally overrides the default sample covariance.
Source code in src/jaxfolio/results.py
equal_start
¶
finalize_result
¶
finalize_result(
weights,
assets: list[str],
method: str,
*,
mu: Array | None = None,
cov: Array | None = None,
returns=None,
risk_free: float = 0.0,
periods_per_year: int = PERIODS_PER_YEAR,
metadata: dict | None = None,
clean_eps: float = 1e-10,
) -> PortfolioResult
Assemble a :class:PortfolioResult with annualized diagnostics.
Provide the moments either directly (mu and cov) or implicitly via
returns (sample moments are computed from it). Weights are coerced to a
flat float array and values below clean_eps in magnitude are zeroed. This
is the single source of truth for the annualized return / volatility / Sharpe
reported on every optimizer result — built-in and custom alike.