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

1"""Covariance-mode configuration for the Basanos optimizer. 

2 

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""" 

8 

9import enum 

10from typing import Annotated, Literal 

11 

12from pydantic import BaseModel, Field, model_validator 

13 

14 

15class CovarianceMode(enum.StrEnum): 

16 r"""Covariance estimation mode for the Basanos optimizer. 

17 

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 

25 

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 $$ 

31 

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`. 

37 

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 """ 

46 

47 ewma_shrink = "ewma_shrink" 

48 sliding_window = "sliding_window" 

49 

50 

51class EwmaShrinkConfig(BaseModel): 

52 """Covariance configuration for the ``ewma_shrink`` mode. 

53 

54 This is the default covariance mode. No additional parameters are required 

55 beyond those already present on `BasanosConfig` (``shrink``, ``corr``). 

56 

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. 

64 

65 Examples: 

66 >>> cfg = EwmaShrinkConfig() 

67 >>> cfg.covariance_mode 

68 <CovarianceMode.ewma_shrink: 'ewma_shrink'> 

69 """ 

70 

71 covariance_mode: Literal[CovarianceMode.ewma_shrink] = CovarianceMode.ewma_shrink 

72 

73 model_config = {"frozen": True} 

74 

75 

76class SlidingWindowConfig(BaseModel): 

77 r"""Covariance configuration for the ``sliding_window`` mode. 

78 

79 Requires both ``window`` (rolling window length) and ``n_factors`` (number 

80 of latent factors for the truncated SVD factor model). 

81 

82 **Effective component count** — at each streaming step the number of SVD 

83 components actually used is 

84 

85 $$ 

86 k_{\text{eff}} = \min(k,\; W,\; n_{\text{valid}},\; k_{\text{max}}) 

87 $$ 

88 

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. 

96 

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). 

111 

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 """ 

126 

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 ) 

163 

164 model_config = {"frozen": True} 

165 

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 

173 

174 

175CovarianceConfig = Annotated[ 

176 EwmaShrinkConfig | SlidingWindowConfig, 

177 Field(discriminator="covariance_mode"), 

178] 

179"""Discriminated union of covariance-mode configurations. 

180 

181Pydantic selects the correct sub-config based on the ``covariance_mode`` 

182discriminator field: 

183 

184* `EwmaShrinkConfig` when ``covariance_mode="ewma_shrink"`` 

185* `SlidingWindowConfig` when ``covariance_mode="sliding_window"`` 

186"""