Coverage for src / basanos / math / _config.py: 100%
77 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:47 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:47 +0000
1"""Configuration classes for the Basanos optimizer.
3Extracted from ``optimizer.py`` to keep each module focused on a single concern.
4All public names are re-exported from ``optimizer.py`` so existing imports are
5unaffected.
6"""
8import enum
9import logging
10from typing import TYPE_CHECKING, Annotated, Literal
12from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
14if TYPE_CHECKING:
15 from ._config_report import ConfigReport
17_logger = logging.getLogger(__name__)
19# Sentinel used in BasanosConfig.replace() to distinguish "not provided"
20# (keep existing value) from "explicitly set to None" (clear the field).
21_SENTINEL: object = object()
24class CovarianceMode(enum.StrEnum):
25 r"""Covariance estimation mode for the Basanos optimizer.
27 Attributes:
28 ewma_shrink: EWMA correlation matrix with linear shrinkage toward the
29 identity. Controlled by :attr:`BasanosConfig.shrink`.
30 This is the default mode.
31 sliding_window: Rolling-window factor model. A fixed block of the
32 ``W`` most recent volatility-adjusted returns is decomposed via
33 truncated SVD into ``k`` latent factors, giving the estimator
35 .. math::
37 \\hat{C}_t^{(W,k)} = \\frac{1}{W}
38 \\mathbf{V}_{k,t}\\mathbf{\\Sigma}_{k,t}^2\\mathbf{V}_{k,t}^\\top
39 + \\hat{D}_t
41 where :math:`\\hat{D}_t` is chosen to enforce unit diagonal.
42 The system is solved efficiently via the Woodbury identity
43 (Section 4.3 of basanos.pdf) at :math:`O(k^3 + kn)` per step
44 rather than :math:`O(n^3)`.
45 Configured via :class:`SlidingWindowConfig`.
47 Examples:
48 >>> CovarianceMode.ewma_shrink
49 <CovarianceMode.ewma_shrink: 'ewma_shrink'>
50 >>> CovarianceMode.sliding_window
51 <CovarianceMode.sliding_window: 'sliding_window'>
52 >>> CovarianceMode("sliding_window")
53 <CovarianceMode.sliding_window: 'sliding_window'>
54 """
56 ewma_shrink = "ewma_shrink"
57 sliding_window = "sliding_window"
60class EwmaShrinkConfig(BaseModel):
61 """Covariance configuration for the ``ewma_shrink`` mode.
63 This is the default covariance mode. No additional parameters are required
64 beyond those already present on :class:`BasanosConfig` (``shrink``, ``corr``).
66 .. note::
67 This class is **intentionally minimal**. The only field is the
68 ``covariance_mode`` discriminator, which is required to make Pydantic's
69 discriminated-union dispatch work correctly (see :data:`CovarianceConfig`).
70 Before adding new EWMA-specific fields here, consider whether the field
71 name clashes with existing :class:`BasanosConfig` top-level fields and
72 whether it would constitute a breaking change to the public API.
74 Examples:
75 >>> cfg = EwmaShrinkConfig()
76 >>> cfg.covariance_mode
77 <CovarianceMode.ewma_shrink: 'ewma_shrink'>
78 """
80 covariance_mode: Literal[CovarianceMode.ewma_shrink] = CovarianceMode.ewma_shrink
82 model_config = {"frozen": True}
85class SlidingWindowConfig(BaseModel):
86 r"""Covariance configuration for the ``sliding_window`` mode.
88 Requires both ``window`` (rolling window length) and ``n_factors`` (number
89 of latent factors for the truncated SVD factor model).
91 **Effective component count** — at each streaming step the number of SVD
92 components actually used is
94 .. math::
96 k_{\text{eff}} = \min(k,\; W,\; n_{\text{valid}},\; k_{\text{max}})
98 where :math:`k` = ``n_factors``, :math:`W` = ``window``,
99 :math:`n_{\text{valid}}` is the number of assets with finite prices at that
100 step, and :math:`k_{\text{max}}` = ``max_components`` (or :math:`+\infty`
101 when unset). This ensures the truncated SVD remains well-posed even when
102 assets temporarily drop out of the universe. Setting ``max_components``
103 explicitly caps computational cost in large universes without changing the
104 desired factor count used in batch mode.
106 Args:
107 window: Rolling window length :math:`W \\geq 1`.
108 Rule of thumb: :math:`W \\geq 2n` keeps the sample covariance
109 well-posed before truncation.
110 n_factors: Number of latent factors :math:`k \\geq 1`.
111 :math:`k = 1` recovers the single market-factor model; larger
112 :math:`k` captures finer correlation structure at the cost of
113 higher estimation noise.
114 max_components: Optional hard cap on the number of SVD components used
115 per streaming step. When set, the effective component count is
116 :math:`\\min(k_{\\text{eff}},\\, \\texttt{max\\_components})`.
117 Useful for large universes where only a few factors dominate and
118 you want to limit SVD cost below ``n_factors``. Must be
119 :math:`\\geq 1` when provided. Defaults to ``None`` (no extra cap).
121 Examples:
122 >>> cfg = SlidingWindowConfig(window=60, n_factors=3)
123 >>> cfg.covariance_mode
124 <CovarianceMode.sliding_window: 'sliding_window'>
125 >>> cfg.window
126 60
127 >>> cfg.n_factors
128 3
129 >>> cfg.max_components is None
130 True
131 >>> cfg2 = SlidingWindowConfig(window=60, n_factors=10, max_components=3)
132 >>> cfg2.max_components
133 3
134 """
136 covariance_mode: Literal[CovarianceMode.sliding_window] = CovarianceMode.sliding_window
137 window: int = Field(
138 ...,
139 gt=0,
140 description=(
141 "Sliding window length W (number of most recent observations). "
142 "Rule of thumb: W >= 2 * n_assets to keep the sample covariance well-posed. "
143 "Note: the first W-1 rows of output will have zero/empty positions while the "
144 "sliding window fills up (warm-up period). Account for this when interpreting "
145 "results or sizing positions."
146 ),
147 )
148 n_factors: int = Field(
149 ...,
150 gt=0,
151 description=(
152 "Number of latent factors k for the sliding window factor model. "
153 "k=1 recovers the single market-factor model; larger k captures finer correlation "
154 "structure at the cost of higher estimation noise. "
155 "At each streaming step the actual number of components used is "
156 "min(n_factors, window, n_valid_assets[, max_components]), so the effective "
157 "rank may be lower than n_factors when the number of valid assets or the "
158 "window length is the binding constraint."
159 ),
160 )
161 max_components: int | None = Field(
162 default=None,
163 gt=0,
164 description=(
165 "Optional hard cap on the number of SVD components used per streaming step. "
166 "When set, the effective component count is "
167 "min(n_factors, window, n_valid_assets, max_components). "
168 "Useful for large universes where only a few factors dominate and you want to "
169 "limit SVD cost below n_factors. Must be >= 1 when provided. Defaults to None."
170 ),
171 )
173 model_config = {"frozen": True}
175 @model_validator(mode="after")
176 def _validate_max_components(self) -> "SlidingWindowConfig":
177 """Validate that max_components does not exceed n_factors."""
178 if self.max_components is not None and self.max_components > self.n_factors:
179 msg = f"max_components ({self.max_components}) must not exceed n_factors ({self.n_factors})"
180 raise ValueError(msg)
181 return self
184CovarianceConfig = Annotated[
185 EwmaShrinkConfig | SlidingWindowConfig,
186 Field(discriminator="covariance_mode"),
187]
188"""Discriminated union of covariance-mode configurations.
190Pydantic selects the correct sub-config based on the ``covariance_mode``
191discriminator field:
193* :class:`EwmaShrinkConfig` when ``covariance_mode="ewma_shrink"``
194* :class:`SlidingWindowConfig` when ``covariance_mode="sliding_window"``
195"""
198class BasanosConfig(BaseModel):
199 r"""Configuration for correlation-aware position optimization.
201 The required parameters (``vola``, ``corr``, ``clip``, ``shrink``, ``aum``)
202 must be supplied by the caller. The optional parameters carry
203 carefully chosen defaults whose rationale is described below.
205 Shrinkage methodology
206 ---------------------
207 ``shrink`` controls linear shrinkage of the EWMA correlation matrix toward
208 the identity:
210 .. math::
212 C_{\\text{shrunk}} = \\lambda \\cdot C_{\\text{EWMA}} + (1 - \\lambda) \\cdot I_n
214 where :math:`\\lambda` = ``shrink`` and :math:`I_n` is the identity.
215 Shrinkage regularises the matrix when assets are few relative to the
216 lookback (high concentration ratio :math:`n / T`), reducing the impact of
217 extreme sample eigenvalues and improving the condition number of the matrix
218 passed to the linear solver.
220 **When to prefer strong shrinkage (low** ``shrink`` **/ high** ``1-shrink``\\ **):**
222 * Fewer than ~30 assets with a ``corr`` lookback shorter than 100 days.
223 * High-volatility or crisis regimes where correlations spike and the sample
224 matrix is less representative of the true structure.
225 * Portfolios where estimation noise is more costly than correlation bias
226 (e.g., when the signal-to-noise ratio of ``mu`` is low).
228 **When to prefer light shrinkage (high** ``shrink``\\ **):**
230 * Many assets with a long lookback (low concentration ratio).
231 * The EWMA correlation structure carries genuine diversification information
232 that you want the solver to exploit.
233 * Out-of-sample testing shows that position stability is not a concern.
235 **Practical starting points (daily return data):**
237 Here *n* = number of assets and *T* = ``cfg.corr`` (EWMA lookback).
239 +-----------------------+-------------------+--------------------------------+
240 | n (assets) / T (corr) | Suggested shrink | Notes |
241 +=======================+===================+================================+
242 | n > 20, T < 40 | 0.3 - 0.5 | Near-singular matrix likely; |
243 | | | strong regularisation needed. |
244 +-----------------------+-------------------+--------------------------------+
245 | n ~ 10, T ~ 60 | 0.5 - 0.7 | Balanced regime. |
246 +-----------------------+-------------------+--------------------------------+
247 | n < 10, T > 100 | 0.7 - 0.9 | Well-conditioned sample; |
248 | | | light shrinkage for stability. |
249 +-----------------------+-------------------+--------------------------------+
251 See :func:`~basanos.math._signal.shrink2id` for the full theoretical
252 background and academic references (Ledoit & Wolf, 2004; Chen et al., 2010).
254 Default rationale
255 -----------------
256 ``denom_tol = 1e-12``
257 Positions are zeroed when the normalisation denominator
258 ``inv_a_norm(μ, Σ)`` falls at or below this threshold. The
259 value 1e-12 provides ample headroom above float64 machine
260 epsilon (~2.2e-16) while remaining negligible relative to any
261 economically meaningful signal magnitude.
263 ``position_scale = 1e6``
264 The dimensionless risk position is multiplied by this factor
265 before being passed to :class:`~jquantstats.Portfolio`.
266 A value of 1e6 means positions are expressed in units of one
267 million of the base currency, a conventional denomination for
268 institutional-scale portfolios where AUM is measured in hundreds
269 of millions.
271 ``min_corr_denom = 1e-14``
272 The EWMA correlation denominator ``sqrt(var_x * var_y)`` is
273 compared against this threshold; when at or below it the
274 correlation is set to NaN rather than dividing by a near-zero
275 value. The default 1e-14 is safely above float64 underflow
276 while remaining negligible for any realistic return series.
277 Advanced users may tighten this guard (larger value) when
278 working with very-low-variance synthetic data.
280 ``max_nan_fraction = 0.9``
281 :class:`~basanos.exceptions.ExcessiveNullsError` is raised
282 during construction when the null fraction in any asset price
283 column **strictly exceeds** this threshold. The default 0.9
284 permits up to 90 % missing prices (e.g., illiquid or recently
285 listed assets in a long history) while rejecting columns that
286 are almost entirely null and would contribute no useful
287 information. Callers who want a stricter gate can lower this
288 value; callers running on sparse data can raise it toward 1.0.
290 Sliding-window mode
291 -------------------
292 When ``covariance_config`` is a :class:`SlidingWindowConfig`, the EWMA
293 correlation estimator is replaced by a rolling-window factor model
294 (Section 4.4 of basanos.pdf). At each timestamp *t* the
295 :math:`W \\times n` submatrix of the :math:`W` most recent
296 volatility-adjusted returns is decomposed via truncated SVD to extract
297 :math:`k` latent factors. The resulting correlation estimate is
299 .. math::
301 \\hat{C}_t^{(W,k)}
302 = \\frac{1}{W}\\mathbf{V}_{k,t}\\mathbf{\\Sigma}_{k,t}^2
303 \\mathbf{V}_{k,t}^\\top + \\hat{D}_t
305 where :math:`\\hat{D}_t` enforces unit diagonal. The linear system
306 :math:`\\hat{C}_t^{(W,k)}\\mathbf{x}_t = \\boldsymbol{\\mu}_t` is solved
307 via the Woodbury identity (:func:`~basanos.math._factor_model.FactorModel.solve`)
308 at cost :math:`O(k^3 + kn)` per step rather than :math:`O(n^3)`.
310 ``covariance_config``
311 Pass a :class:`SlidingWindowConfig` instance to enable this mode.
312 The required sub-parameters are:
314 ``window``
315 Rolling window length :math:`W \\geq 1`. Rule of thumb: :math:`W
316 \\geq 2n` keeps the sample covariance well-posed before truncation.
318 ``n_factors``
319 Number of latent factors :math:`k \\geq 1`. :math:`k = 1`
320 recovers the single market-factor model; larger :math:`k` captures
321 finer correlation structure at the cost of higher estimation noise.
323 Examples:
324 >>> cfg = BasanosConfig(vola=32, corr=64, clip=3.0, shrink=0.5, aum=1e8)
325 >>> cfg.vola
326 32
327 >>> cfg.corr
328 64
329 >>> sw_cfg = BasanosConfig(
330 ... vola=16, corr=32, clip=3.0, shrink=0.5, aum=1e6,
331 ... covariance_config=SlidingWindowConfig(window=60, n_factors=3),
332 ... )
333 >>> sw_cfg.covariance_mode
334 <CovarianceMode.sliding_window: 'sliding_window'>
335 """
337 vola: int = Field(..., gt=0, description="EWMA lookback for volatility normalization.")
338 corr: int = Field(..., gt=0, description="EWMA lookback for correlation estimation.")
339 clip: float = Field(..., gt=0.0, description="Clipping threshold for volatility adjustment.")
340 shrink: float = Field(
341 ...,
342 ge=0.0,
343 le=1.0,
344 description=(
345 "Retention weight λ for linear shrinkage of the EWMA correlation matrix toward "
346 "the identity: C_shrunk = λ·C_ewma + (1-λ)·I. "
347 "λ=1.0 uses the raw EWMA matrix (no shrinkage); λ=0.0 replaces it entirely "
348 "with the identity (maximum shrinkage, positions are treated as uncorrelated). "
349 "Values in [0.3, 0.8] are typical for daily financial return data. "
350 "Lower values improve numerical stability when assets are many relative to the "
351 "lookback (high concentration ratio n/T). See shrink2id() for full guidance. "
352 "Only used when covariance_mode='ewma_shrink'."
353 ),
354 )
355 aum: float = Field(..., gt=0.0, description="Assets under management for portfolio scaling.")
356 denom_tol: float = Field(
357 default=1e-12,
358 gt=0.0,
359 description=(
360 "Minimum normalisation denominator; positions are zeroed at or below this value. "
361 "The default 1e-12 is well above float64 machine epsilon (~2.2e-16) while "
362 "remaining negligible for any economically meaningful signal."
363 ),
364 )
365 position_scale: float = Field(
366 default=1e6,
367 gt=0.0,
368 description=(
369 "Multiplicative scaling factor applied to dimensionless risk positions to obtain "
370 "cash positions in base-currency units. Defaults to 1e6 (one million), a "
371 "conventional denomination for institutional portfolios."
372 ),
373 )
374 min_corr_denom: float = Field(
375 default=1e-14,
376 gt=0.0,
377 description=(
378 "Guard threshold for the EWMA correlation denominator sqrt(var_x * var_y). "
379 "When the denominator is at or below this value the correlation is set to NaN "
380 "instead of dividing by a near-zero number. "
381 "The default 1e-14 is safely above float64 underflow while being negligible for "
382 "any realistic return variance."
383 ),
384 )
385 max_nan_fraction: float = Field(
386 default=0.9,
387 gt=0.0,
388 lt=1.0,
389 description=(
390 "Maximum tolerated fraction of null values in any asset price column. "
391 "ExcessiveNullsError is raised during construction when the null fraction "
392 "strictly exceeds this threshold. "
393 "The default 0.9 allows up to 90 % missing prices while rejecting columns "
394 "that are almost entirely null."
395 ),
396 )
397 covariance_config: CovarianceConfig = Field(
398 default_factory=EwmaShrinkConfig,
399 description=(
400 "Covariance estimation configuration. "
401 "Pass EwmaShrinkConfig() (default) for EWMA correlation with linear shrinkage "
402 "toward the identity, or SlidingWindowConfig(window=W, n_factors=k) for a "
403 "rolling-window factor model. See Section 4.4 of basanos.pdf."
404 ),
405 )
406 cost_per_unit: float = Field(
407 default=0.0,
408 ge=0.0,
409 description=(
410 "One-way trading cost per unit of position change. "
411 "At each period, the cost deduction is sum(|x_t - x_{t-1}|) * cost_per_unit "
412 "where x_t is the cash position vector. Defaults to 0.0 (no cost). "
413 "The resulting net-of-cost NAV is exposed via Portfolio.net_cost_nav."
414 ),
415 )
416 max_turnover: float | None = Field(
417 default=None,
418 gt=0.0,
419 description=(
420 "Optional turnover budget per period in cash-position units. "
421 "When set, the L1 norm of position changes sum(|x_t - x_{t-1}|) is capped "
422 "at this value at every solve step by proportionally scaling the position "
423 "delta toward the previous position. Must be strictly positive when provided. "
424 "Defaults to None (no turnover constraint)."
425 ),
426 )
428 model_config = {"frozen": True, "extra": "forbid"}
430 @model_validator(mode="before")
431 @classmethod
432 def _reject_legacy_flat_kwargs(cls, data: dict[str, object]) -> dict[str, object]:
433 """Raise an informative TypeError when the pre-v0.4 flat kwargs are used.
435 Before v0.4 callers passed ``covariance_mode``, ``n_factors``, and
436 ``window`` as top-level keyword arguments to :class:`BasanosConfig`.
437 Those fields were replaced by the nested discriminated union
438 ``covariance_config``. Without this validator Pydantic raises a
439 generic ``extra_forbidden`` error that gives no migration guidance.
441 Examples:
442 >>> BasanosConfig(
443 ... vola=10, corr=20, clip=3.0, shrink=0.5, aum=1e6,
444 ... covariance_mode="sliding_window", window=30, n_factors=2,
445 ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
446 Traceback (most recent call last):
447 ...
448 TypeError: ...
449 """
450 legacy_keys = {"covariance_mode", "n_factors", "window"}
451 found = legacy_keys & data.keys()
452 if found:
453 found_str = ", ".join(f"'{k}'" for k in sorted(found))
454 msg = (
455 f"BasanosConfig received legacy keyword argument(s): {found_str}. "
456 "These flat fields were removed in v0.4. "
457 "Migrate to the nested covariance_config API:\n\n"
458 " # Before (v0.3 and earlier):\n"
459 " BasanosConfig(..., covariance_mode='sliding_window', window=30, n_factors=2)\n\n"
460 " # After (v0.4+):\n"
461 " from basanos.math import SlidingWindowConfig\n"
462 " BasanosConfig(..., covariance_config=SlidingWindowConfig(window=30, n_factors=2))\n\n"
463 "For the default EWMA-shrink mode no covariance_config argument is needed."
464 )
465 raise TypeError(msg)
466 return data
468 def replace(
469 self,
470 *,
471 vola: int | None = None,
472 corr: int | None = None,
473 clip: float | None = None,
474 shrink: float | None = None,
475 aum: float | None = None,
476 denom_tol: float | None = None,
477 position_scale: float | None = None,
478 min_corr_denom: float | None = None,
479 max_nan_fraction: float | None = None,
480 covariance_config: "CovarianceConfig | None" = None,
481 cost_per_unit: float | None = None,
482 max_turnover: float | None = _SENTINEL, # type: ignore[assignment]
483 ) -> "BasanosConfig":
484 """Return a new :class:`BasanosConfig` with selected fields replaced.
486 Unlike :meth:`model_copy`, this method uses explicit constructor kwarg
487 forwarding so that any new required field added to
488 :class:`BasanosConfig` surfaces immediately as a type or lint error at
489 the call site, rather than silently failing at runtime.
491 All parameters default to ``None``, meaning *keep the existing value*.
492 Pass a non-``None`` value for every field you want to change.
494 Args:
495 vola: EWMA lookback for volatility normalisation.
496 corr: EWMA lookback for correlation estimation.
497 clip: Clipping threshold for volatility adjustment.
498 shrink: Retention weight λ ∈ [0, 1] for linear shrinkage.
499 aum: Assets under management for portfolio scaling.
500 denom_tol: Minimum normalisation denominator.
501 position_scale: Multiplicative scaling factor for cash positions.
502 min_corr_denom: Guard threshold for the EWMA correlation denominator.
503 max_nan_fraction: Maximum tolerated null fraction per price column.
504 covariance_config: Covariance estimation configuration.
505 cost_per_unit: One-way trading cost per unit of position change.
506 max_turnover: Optional turnover budget per period in cash-position
507 units. Pass ``None`` explicitly to clear an existing budget.
509 Returns:
510 A new :class:`BasanosConfig` with the specified fields replaced and
511 all other fields copied from ``self``.
513 Examples:
514 >>> cfg = BasanosConfig(vola=10, corr=20, clip=3.0, shrink=0.5, aum=1e6)
515 >>> cfg2 = cfg.replace(shrink=0.8)
516 >>> cfg2.shrink
517 0.8
518 >>> cfg2.vola == cfg.vola
519 True
520 >>> cfg3 = cfg.replace(cost_per_unit=0.001, max_turnover=1e5)
521 >>> cfg3.cost_per_unit
522 0.001
523 >>> cfg3.max_turnover
524 100000.0
525 """
526 new_max_turnover: float | None = self.max_turnover if max_turnover is _SENTINEL else max_turnover
527 return BasanosConfig(
528 vola=self.vola if vola is None else vola,
529 corr=self.corr if corr is None else corr,
530 clip=self.clip if clip is None else clip,
531 shrink=self.shrink if shrink is None else shrink,
532 aum=self.aum if aum is None else aum,
533 denom_tol=self.denom_tol if denom_tol is None else denom_tol,
534 position_scale=self.position_scale if position_scale is None else position_scale,
535 min_corr_denom=self.min_corr_denom if min_corr_denom is None else min_corr_denom,
536 max_nan_fraction=self.max_nan_fraction if max_nan_fraction is None else max_nan_fraction,
537 covariance_config=self.covariance_config if covariance_config is None else covariance_config,
538 cost_per_unit=self.cost_per_unit if cost_per_unit is None else cost_per_unit,
539 max_turnover=new_max_turnover,
540 )
542 @property
543 def covariance_mode(self) -> CovarianceMode:
544 """Covariance mode derived from :attr:`covariance_config`."""
545 return self.covariance_config.covariance_mode
547 @property
548 def window(self) -> int | None:
549 """Sliding window length, or ``None`` when not in ``sliding_window`` mode."""
550 if isinstance(self.covariance_config, SlidingWindowConfig):
551 return self.covariance_config.window
552 return None
554 @property
555 def n_factors(self) -> int | None:
556 """Number of latent factors, or ``None`` when not in ``sliding_window`` mode."""
557 if isinstance(self.covariance_config, SlidingWindowConfig):
558 return self.covariance_config.n_factors
559 return None
561 @property
562 def report(self) -> "ConfigReport":
563 """Return a :class:`~basanos.math._config_report.ConfigReport` facade for this config.
565 Generates a self-contained HTML report summarising all configuration
566 parameters, a shrinkage-guidance table, and a theory section on
567 Ledoit-Wolf shrinkage.
569 To also include a lambda-sweep chart (Sharpe vs λ), use
570 :attr:`BasanosEngine.config_report` instead, which requires price and
571 signal data.
573 Returns:
574 basanos.math._config_report.ConfigReport: Report facade with
575 ``to_html()`` and ``save()`` methods.
577 Examples:
578 >>> from basanos.math import BasanosConfig
579 >>> cfg = BasanosConfig(vola=10, corr=20, clip=3.0, shrink=0.5, aum=1e6)
580 >>> report = cfg.report
581 >>> html = report.to_html()
582 >>> "Parameters" in html
583 True
584 """
585 from ._config_report import ConfigReport
587 return ConfigReport(config=self)
589 @field_validator("corr")
590 @classmethod
591 def corr_greater_than_vola(cls, v: int, info: ValidationInfo) -> int:
592 """Optionally enforce corr ≥ vola for stability.
594 Pydantic v2 passes ValidationInfo; use info.data to access other fields.
595 """
596 vola = info.data.get("vola") if hasattr(info, "data") else None
597 if vola is not None and v < vola:
598 raise ValueError
599 return v