Coverage for src/basanos/math/_covariance_config.py: 100%
23 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-09-25 12:05 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-09-25 12:05 +0000
1"""Covariance-mode configuration for the Basanos optimizer.
3Split out of ``_config.py`` so that module holds `BasanosConfig` alone.
4`CovarianceMode`, the two per-mode configs and the `CovarianceConfig`
5discriminated union live here; ``_config.py`` re-exports all four, so existing
6imports are unaffected.
7"""
9import enum
10from typing import Annotated, Literal
12from pydantic import BaseModel, Field, model_validator
15class CovarianceMode(enum.StrEnum):
16 r"""Covariance estimation mode for the Basanos optimizer.
18 Attributes:
19 ewma_shrink: EWMA correlation matrix with linear shrinkage toward the
20 identity. Controlled by `shrink`.
21 This is the default mode.
22 sliding_window: Rolling-window factor model. A fixed block of the
23 ``W`` most recent volatility-adjusted returns is decomposed via
24 truncated SVD into ``k`` latent factors, giving the estimator
26 $$
27 \\hat{C}_t^{(W,k)} = \\frac{1}{W}
28 \\mathbf{V}_{k,t}\\mathbf{\\Sigma}_{k,t}^2\\mathbf{V}_{k,t}^\\top
29 + \\hat{D}_t
30 $$
32 where $\\hat{D}_t$ is chosen to enforce unit diagonal.
33 The system is solved efficiently via the Woodbury identity
34 (Section 4.3 of basanos.pdf) at $O(k^3 + kn)$ per step
35 rather than $O(n^3)$.
36 Configured via `SlidingWindowConfig`.
38 Examples:
39 >>> CovarianceMode.ewma_shrink
40 <CovarianceMode.ewma_shrink: 'ewma_shrink'>
41 >>> CovarianceMode.sliding_window
42 <CovarianceMode.sliding_window: 'sliding_window'>
43 >>> CovarianceMode("sliding_window")
44 <CovarianceMode.sliding_window: 'sliding_window'>
45 """
47 ewma_shrink = "ewma_shrink"
48 sliding_window = "sliding_window"
51class EwmaShrinkConfig(BaseModel):
52 """Covariance configuration for the ``ewma_shrink`` mode.
54 This is the default covariance mode. No additional parameters are required
55 beyond those already present on `BasanosConfig` (``shrink``, ``corr``).
57 .. note::
58 This class is **intentionally minimal**. The only field is the
59 ``covariance_mode`` discriminator, which is required to make Pydantic's
60 discriminated-union dispatch work correctly (see `CovarianceConfig`).
61 Before adding new EWMA-specific fields here, consider whether the field
62 name clashes with existing `BasanosConfig` top-level fields and
63 whether it would constitute a breaking change to the public API.
65 Examples:
66 >>> cfg = EwmaShrinkConfig()
67 >>> cfg.covariance_mode
68 <CovarianceMode.ewma_shrink: 'ewma_shrink'>
69 """
71 covariance_mode: Literal[CovarianceMode.ewma_shrink] = CovarianceMode.ewma_shrink
73 model_config = {"frozen": True}
76class SlidingWindowConfig(BaseModel):
77 r"""Covariance configuration for the ``sliding_window`` mode.
79 Requires both ``window`` (rolling window length) and ``n_factors`` (number
80 of latent factors for the truncated SVD factor model).
82 **Effective component count** — at each streaming step the number of SVD
83 components actually used is
85 $$
86 k_{\text{eff}} = \min(k,\; W,\; n_{\text{valid}},\; k_{\text{max}})
87 $$
89 where $k$ = ``n_factors``, $W$ = ``window``,
90 $n_{\text{valid}}$ is the number of assets with finite prices at that
91 step, and $k_{\text{max}}$ = ``max_components`` (or $+\infty$
92 when unset). This ensures the truncated SVD remains well-posed even when
93 assets temporarily drop out of the universe. Setting ``max_components``
94 explicitly caps computational cost in large universes without changing the
95 desired factor count used in batch mode.
97 Args:
98 window: Rolling window length $W \\geq 1$.
99 Rule of thumb: $W \\geq 2n$ keeps the sample covariance
100 well-posed before truncation.
101 n_factors: Number of latent factors $k \\geq 1$.
102 $k = 1$ recovers the single market-factor model; larger
103 $k$ captures finer correlation structure at the cost of
104 higher estimation noise.
105 max_components: Optional hard cap on the number of SVD components used
106 per streaming step. When set, the effective component count is
107 $\\min(k_{\\text{eff}},\\, \\texttt{max\\_components})$.
108 Useful for large universes where only a few factors dominate and
109 you want to limit SVD cost below ``n_factors``. Must be
110 $\\geq 1$ when provided. Defaults to ``None`` (no extra cap).
112 Examples:
113 >>> cfg = SlidingWindowConfig(window=60, n_factors=3)
114 >>> cfg.covariance_mode
115 <CovarianceMode.sliding_window: 'sliding_window'>
116 >>> cfg.window
117 60
118 >>> cfg.n_factors
119 3
120 >>> cfg.max_components is None
121 True
122 >>> cfg2 = SlidingWindowConfig(window=60, n_factors=10, max_components=3)
123 >>> cfg2.max_components
124 3
125 """
127 covariance_mode: Literal[CovarianceMode.sliding_window] = CovarianceMode.sliding_window
128 window: int = Field(
129 ...,
130 gt=0,
131 description=(
132 "Sliding window length W (number of most recent observations). "
133 "Rule of thumb: W >= 2 * n_assets to keep the sample covariance well-posed. "
134 "Note: the first W-1 rows of output will have zero/empty positions while the "
135 "sliding window fills up (warm-up period). Account for this when interpreting "
136 "results or sizing positions."
137 ),
138 )
139 n_factors: int = Field(
140 ...,
141 gt=0,
142 description=(
143 "Number of latent factors k for the sliding window factor model. "
144 "k=1 recovers the single market-factor model; larger k captures finer correlation "
145 "structure at the cost of higher estimation noise. "
146 "At each streaming step the actual number of components used is "
147 "min(n_factors, window, n_valid_assets[, max_components]), so the effective "
148 "rank may be lower than n_factors when the number of valid assets or the "
149 "window length is the binding constraint."
150 ),
151 )
152 max_components: int | None = Field(
153 default=None,
154 gt=0,
155 description=(
156 "Optional hard cap on the number of SVD components used per streaming step. "
157 "When set, the effective component count is "
158 "min(n_factors, window, n_valid_assets, max_components). "
159 "Useful for large universes where only a few factors dominate and you want to "
160 "limit SVD cost below n_factors. Must be >= 1 when provided. Defaults to None."
161 ),
162 )
164 model_config = {"frozen": True}
166 @model_validator(mode="after")
167 def _validate_max_components(self) -> "SlidingWindowConfig":
168 """Validate that max_components does not exceed n_factors."""
169 if self.max_components is not None and self.max_components > self.n_factors:
170 msg = f"max_components ({self.max_components}) must not exceed n_factors ({self.n_factors})"
171 raise ValueError(msg)
172 return self
175CovarianceConfig = Annotated[
176 EwmaShrinkConfig | SlidingWindowConfig,
177 Field(discriminator="covariance_mode"),
178]
179"""Discriminated union of covariance-mode configurations.
181Pydantic selects the correct sub-config based on the ``covariance_mode``
182discriminator field:
184* `EwmaShrinkConfig` when ``covariance_mode="ewma_shrink"``
185* `SlidingWindowConfig` when ``covariance_mode="sliding_window"``
186"""