project_weights

Defined in fynance.portfolio.constraints

project_weights(w, box=(-1.0, 1.0), gross_max=None, net_range=None, groups=None, group_bounds=None, method='auto')[source]

Project weights onto a feasible set: box, gross, net and group bounds.

Least-distance (Euclidean) projection of a weight vector, or of every row of a (T, N) weight book independently, onto the intersection of:

  • a per-asset box (lo_i <= w_i <= hi_i);

  • a gross-leverage cap (sum(|w_i|) <= gross_max);

  • a net-exposure range (net_lo <= sum(w_i) <= net_hi);

  • named group bounds (lo_g <= sum_{i in g} w_i <= hi_g).

Meant as a composable overlay run after any allocator or signal and before fynance.backtest.engine.backtest.

Parameters:
warray_like

Weights, shape (N,) or (T, N). When 2-D, each row is projected independently (no cross-row interaction) and the output has the same shape.

boxtuple of float or array_like, optional

Either a scalar (lo, hi) pair applied to every asset, or a (2, N) array-like of per-asset (lo_i, hi_i) bounds. Must satisfy lo <= hi element-wise. Default (-1.0, 1.0).

gross_maxfloat, optional

Cap on gross leverage sum(|w_i|). Default None (no cap).

net_rangetuple of float, optional

(lo, hi) bounds on the net exposure sum(w_i). Default None (unconstrained).

groupsdict of str to sequence of int, optional

Named groups of asset indices, e.g. {'tech': [0, 1], 'bond': [2]}. Only groups referenced by group_bounds constrain the projection. Default None.

group_boundsdict of str to tuple of float, optional

(lo, hi) bounds on each named group’s net sum sum_{i in group} w_i. Every key must exist in groups. Default None (no group constraints).

method{‘auto’, ‘exact’}, optional

'auto' (default) uses a closed-form fast path (clip then scale) when only box and/or gross_max are active and the box contains zero; 'exact' always uses the SLSQP least-distance projection.

Returns:
np.ndarray

Projected weights, same shape as w.

Raises:
ValueError

If box lower bounds exceed upper bounds, if a group_bounds key is not in groups, if a group’s indices are out of range, if the constraint set is infeasible (message contains 'infeasible'), or if SLSQP fails to converge on the exact path.

Notes

The fast path is exact for the box (a plain clip), but the gross cap is applied as a uniform scaling of the clipped vector, not the strict least-distance projection onto {box} \cap {gross <= gross_max} — this is standard practice (cheap, and identical to the exact projection whenever the box does not bind at the optimum). Pass method='exact' to force the SLSQP least-distance projection instead.

Examples

Fast-path gross cap on a 3-asset long-short book:

>>> import numpy as np
>>> w = np.array([0.8, -0.6, 0.3])
>>> v = project_weights(w, box=(-1.0, 1.0), gross_max=1.0)
>>> np.round(v, 4)
array([ 0.4706, -0.3529,  0.1765])

Group-bound example — cap the net “tech” exposure to [-0.5, 0.5]:

>>> w = np.array([0.8, 0.6, -0.3])
>>> v = project_weights(
...     w, groups={'tech': [0, 1], 'bond': [2]},
...     group_bounds={'tech': (-0.5, 0.5)},
... )
>>> np.round(v, 6)
array([ 0.35,  0.15, -0.3 ])