Skip to content

Risk Models & Solve Helpers

Supporting data structures used internally by the engine and exposed for advanced usage.

FactorModel

FactorModel dataclass

Frozen dataclass for a factor risk model decomposition (Section 4.1).

Encapsulates the three components of the factor model

.. math::

\bm{\Sigma} = \mathbf{B}\mathbf{F}\mathbf{B}^\top + \mathbf{D}

where

  • :math:\mathbf{B} \in \mathbb{R}^{n \times k} is the factor loading matrix: column :math:j gives the sensitivity of each asset to factor :math:j.
  • :math:\mathbf{F} \in \mathbb{R}^{k \times k} is the factor covariance matrix (positive definite), capturing how the :math:k factors co-vary.
  • :math:\mathbf{D} = \operatorname{diag}(d_1, \dots, d_n) with :math:d_i > 0 is the idiosyncratic variance diagonal, capturing the asset-specific variance unexplained by the common factors.

The central assumption is :math:k \ll n: the dominant systematic sources of risk are captured by a handful of factors while the idiosyncratic component is, by construction, uncorrelated across assets.

Attributes:

Name Type Description
factor_loadings ndarray

Factor loading matrix :math:\mathbf{B}, shape (n, k).

factor_covariance ndarray

Factor covariance matrix :math:\mathbf{F}, shape (k, k).

idiosyncratic_var ndarray

Idiosyncratic variance vector :math:(d_1, \dots, d_n), shape (n,). All entries must be strictly positive.

Examples:

>>> import numpy as np
>>> loadings = np.eye(3, 2)
>>> cov = np.eye(2) * 0.5
>>> idio = np.array([0.5, 0.5, 1.0])
>>> fm = FactorModel(factor_loadings=loadings, factor_covariance=cov, idiosyncratic_var=idio)
>>> fm.n_assets
3
>>> fm.n_factors
2
>>> fm.covariance.shape
(3, 3)

factor_loadings: np.ndarray instance-attribute

factor_covariance: np.ndarray instance-attribute

idiosyncratic_var: np.ndarray instance-attribute

n_assets: int property

Number of assets n (rows of factor_loadings).

n_factors: int property

Number of factors k (columns of factor_loadings).

covariance: np.ndarray property

Reconstruct the full :math:n \times n covariance matrix.

Computes :math:\bm{\Sigma} = \mathbf{B}\mathbf{F}\mathbf{B}^\top + \mathbf{D} by combining the low-rank systematic component with the diagonal idiosyncratic component.

Returns:

Type Description
ndarray

np.ndarray: Shape (n, n) symmetric covariance matrix.

Examples:

>>> import numpy as np
>>> loadings = np.array([[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]])
>>> cov = np.eye(2)
>>> idio = np.ones(3)
>>> fm = FactorModel(factor_loadings=loadings, factor_covariance=cov, idiosyncratic_var=idio)
>>> fm.covariance.diagonal().tolist()
[2.0, 2.0, 1.0]

woodbury_condition_number: float property

Condition number of the inner :math:k \times k Woodbury matrix.

Returns the condition number of the matrix

.. math::

\mathbf{M} = \mathbf{F}^{-1} + \mathbf{B}^\top\mathbf{D}^{-1}\mathbf{B}

which is the matrix actually inverted during :meth:solve. A large value (above _DEFAULT_COND_THRESHOLD ≈ 1e12) indicates that the Woodbury solve is numerically unreliable.

This property gives callers a way to inspect the numerical health of the model without performing a full solve. Unlike the condition number of the full :math:n \times n covariance matrix, this measure is specific to the :math:k \times k inner system solved inside the Woodbury identity.

Returns:

Name Type Description
float float

Condition number :math:\kappa(\mathbf{M}). Returns

float

inf when :math:\mathbf{F} is not positive-definite (e.g.

float

singular or indefinite), as the Cholesky decomposition used to

form float

math:\mathbf{F}^{-1} fails in that case.

Examples:

>>> import numpy as np
>>> loadings = np.eye(3, 1)
>>> cov = np.eye(1)
>>> idio = np.ones(3)
>>> fm = FactorModel(factor_loadings=loadings, factor_covariance=cov, idiosyncratic_var=idio)
>>> fm.woodbury_condition_number > 0
True

__post_init__() -> None

Validate shape consistency and strict positivity after initialization.

Raises:

Type Description
FactorModelError

If factor_loadings is not 2-D.

FactorModelError

If factor_covariance shape does not match the number of factors inferred from factor_loadings.

FactorModelError

If idiosyncratic_var length does not match the number of assets inferred from factor_loadings.

FactorModelError

If any element of idiosyncratic_var is not strictly positive.

solve(rhs: np.ndarray, cond_threshold: float = _DEFAULT_COND_THRESHOLD) -> np.ndarray

Solve :math:\bm{\Sigma}\,\mathbf{x} = \mathbf{b} via the Woodbury identity.

Applies the Sherman--Morrison--Woodbury formula (Section 4.3 of basanos.pdf) to avoid forming or factorising the full :math:n \times n covariance matrix:

.. math::

(\mathbf{D} + \mathbf{B}\mathbf{F}\mathbf{B}^\top)^{-1}
= \mathbf{D}^{-1}
  - \mathbf{D}^{-1}\mathbf{B}
    \bigl(\mathbf{F}^{-1} + \mathbf{B}^\top\mathbf{D}^{-1}\mathbf{B}\bigr)^{-1}
    \mathbf{B}^\top\mathbf{D}^{-1}.

Because :math:\mathbf{D} is diagonal, :math:\mathbf{D}^{-1} is free. The inner matrix is :math:k \times k with cost :math:O(k^3), and the surrounding multiplications cost :math:O(kn). Total cost is :math:O(k^3 + kn) rather than :math:O(n^3).

Parameters:

Name Type Description Default
rhs ndarray

Right-hand side vector :math:\mathbf{b}, shape (n,).

required
cond_threshold float

Condition-number threshold above which an :class:~basanos.exceptions.IllConditionedMatrixWarning is emitted. The check is applied to both factor_covariance (:math:\mathbf{F}) and to the inner :math:k \times k Woodbury matrix :math:\mathbf{F}^{-1} + \mathbf{B}^\top \mathbf{D}^{-1}\mathbf{B}. Defaults to 1e12.

_DEFAULT_COND_THRESHOLD

Returns:

Type Description
ndarray

np.ndarray: Solution vector :math:\mathbf{x}, shape (n,).

Raises:

Type Description
DimensionMismatchError

If rhs length does not match n_assets.

SingularMatrixError

If the inner :math:k \times k matrix is singular.

Examples:

>>> import numpy as np
>>> loadings = np.eye(3, 1)
>>> cov = np.eye(1)
>>> idio = np.ones(3)
>>> fm = FactorModel(factor_loadings=loadings, factor_covariance=cov, idiosyncratic_var=idio)
>>> rhs = np.array([1.0, 2.0, 3.0])
>>> x = fm.solve(rhs)
>>> np.allclose(fm.covariance @ x, rhs)
True

from_returns(returns: np.ndarray, k: int) -> FactorModel classmethod

Fit a rank-k factor model from a return matrix via truncated SVD.

Extracts latent factors from the return matrix :math:\mathbf{R} \in \mathbb{R}^{T \times n} using the Singular Value Decomposition (SVD). The top-k singular triplets define the factor model components:

.. math::

\mathbf{B} = \mathbf{V}_k, \quad
\mathbf{F} = \bm{\Sigma}_k^2 / T, \quad
\hat{d}_i = 1 - \bigl(\mathbf{B}\mathbf{F}\mathbf{B}^\top\bigr)_{ii}

where :math:\mathbf{V}_k and :math:\bm{\Sigma}_k are the top-k right singular vectors and singular values of :math:\mathbf{R} respectively. When returns contains unit-variance columns (as produced by :func:~basanos.math._signal.vol_adj), the sample covariance has unit diagonal; the idiosyncratic term :math:\hat{d}_i = 1 - (\mathbf{B}\mathbf{F}\mathbf{B}^\top)_{ii} absorbs the residual so the full covariance :math:\hat{\mathbf{C}}^{(k)} also has unit diagonal. Each :math:\hat{d}_i is clamped from below at machine epsilon to guarantee strict positivity.

Parameters:

Name Type Description Default
returns ndarray

Return matrix of shape (T, n), typically volatility-adjusted log returns with rows as timestamps and columns as assets.

required
k int

Number of factors to retain. Must satisfy 1 <= k <= min(T, n).

required

Returns:

Name Type Description
FactorModel FactorModel

Fitted factor model with n_assets = n and n_factors = k.

Raises:

Type Description
FactorModelError

If returns is not 2-D.

FactorModelError

If k is outside the range [1, min(T, n)].

Examples:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> ret = rng.standard_normal((50, 5))
>>> fm = FactorModel.from_returns(ret, k=2)
>>> fm.n_factors
2
>>> fm.n_assets
5
>>> fm.covariance.shape
(5, 5)

__init__(factor_loadings: np.ndarray, factor_covariance: np.ndarray, idiosyncratic_var: np.ndarray) -> None


MatrixBundle

MatrixBundle dataclass

Container for the covariance matrix and any mode-specific auxiliary state.

Wrapping the covariance matrix in a dataclass decouples :meth:_SolveMixin._compute_position from the raw array so that future covariance modes (e.g. DCC-GARCH, RMT-cleaned) can carry additional fields through the same interface without changing the method signature.

Attributes:

Name Type Description
matrix ndarray

The (n_active, n_active) covariance sub-matrix for the active assets at a given timestamp.

matrix: np.ndarray instance-attribute

__init__(matrix: np.ndarray) -> None


WarmupState

WarmupState dataclass

Final state produced by a full batch solve; consumed by :meth:BasanosStream.from_warmup.

Returned by :meth:BasanosEngine.warmup_state and used by :meth:BasanosStream.from_warmup to initialise the streaming state without coupling to the private :meth:~_SolveMixin._iter_solve generator.

Attributes:

Name Type Description
prev_cash_pos ndarray

Cash positions at the last warmup row, shape (n_assets,). NaN for assets that were still in their own warmup period.

corr_iir_state _EwmCorrState | None

Final IIR filter memory from the EWM correlation pass, or None when using :class:~basanos.math.SlidingWindowConfig. :meth:BasanosStream.from_warmup reads these arrays to seed the incremental lfilter state without a second pass over the warmup data.

prev_cash_pos: np.ndarray instance-attribute

corr_iir_state: _EwmCorrState | None = dataclasses.field(default=None) class-attribute instance-attribute

__init__(prev_cash_pos: np.ndarray, corr_iir_state: _EwmCorrState | None = None) -> None


SolveStatus

SolveStatus

Bases: StrEnum

Solver outcome labels for each timestamp.

Since :class:SolveStatus inherits from :class:str via StrEnum, values compare equal to their string equivalents (e.g. SolveStatus.VALID == "valid"), preserving backward compatibility with code that matches on string literals.

Attributes:

Name Type Description
WARMUP

Insufficient history for the sliding-window covariance mode.

ZERO_SIGNAL

The expected-return vector was all-zero; positions zeroed.

DEGENERATE

Normalisation denominator was non-finite, solve failed, or no asset had a finite price; positions zeroed for safety.

VALID

Linear system solved successfully; positions are non-trivially non-zero.

WARMUP = 'warmup' class-attribute instance-attribute

ZERO_SIGNAL = 'zero_signal' class-attribute instance-attribute

DEGENERATE = 'degenerate' class-attribute instance-attribute

VALID = 'valid' class-attribute instance-attribute