cs_neutralize

Defined in fynance.features.cross_section

cs_neutralize(X, exposures)[source]

Per-bar cross-sectional OLS neutralization, NaN-aware.

At each bar t, regresses the valid entries of X on the corresponding valid rows of exposures (plus an intercept) by ordinary least squares, and returns the residual — the part of X orthogonal to the exposure(s). This is the standard way to strip an unwanted factor (e.g. sector, beta, size) out of a raw signal before ranking it.

Parameters:
Xarray_like

Panel, shape (T, N).

exposuresarray_like

Exposure(s) to neutralize against: (T, N) for a single factor, or (T, N, K) for K factors.

Returns:
np.ndarray

(T, N) residual panel. NaN where X (or the exposure) is NaN, and for every asset of a bar with fewer than K + 1 valid assets.

See also

cs_demean, cs_zscore

Notes

For bar t with valid-asset design matrix \(D_t = [\mathbb{1}, F_t] \in \mathbb{R}^{n_t \times (K+1)}\) (an intercept column plus the K exposure columns) and valid target \(y_t\), the fit solves the least-squares problem

\[\hat\beta_t = \arg\min_\beta \lVert y_t - D_t \beta \rVert_2^2\]

via numpy.linalg.lstsq, and the output is \(y_t - D_t \hat\beta_t\). A bar needs at least \(K + 1\) valid assets to identify the \(K + 1\) parameters (intercept + K exposures); bars with fewer valid assets output NaN for every asset. An asset is valid at bar t only if both X[t] and every column of exposures[t] are non-NaN there.

Examples

A single factor that is an exact linear driver of X neutralizes to (numerically) zero:

>>> import numpy as np
>>> f = np.array([[1., 2., 3., 4.]])
>>> X = 2. + 3. * f
>>> np.round(cs_neutralize(X, f), 8)
array([[0., 0., 0., 0.]])

A bar with fewer valid assets than parameters needed (here 1 valid asset, 2 needed for an intercept + 1 factor) is entirely NaN:

>>> X = np.array([[1., np.nan, np.nan]])
>>> f = np.array([[1., 2., 3.]])
>>> cs_neutralize(X, f)
array([[nan, nan, nan]])