Coverage for src/jquantstats/_protocol.py: 100%
4 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-06 04:52 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-06 04:52 +0000
1"""Shared protocol definitions used across jquantstats subpackages.
3Design rationale
4----------------
5The analytics subpackages (``_stats``, ``_plots``, ``_reports``, ``_utils``)
6must not import the concrete `Data` / `Portfolio` classes at runtime — that
7would create circular imports, since those classes compose the subpackages.
9**This is enforced, not just documented.** ``[tool.importlinter]`` in
10``pyproject.toml`` declares it as a contract that ``make arch`` checks and
11``make test`` runs, so a layering inversion fails CI rather than review. Imports
12under ``if TYPE_CHECKING:`` are excluded from the contract — annotating against
13the concrete class costs nothing at runtime and forms no cycle, so it stays
14allowed. Where a subpackage needs to *construct* a `Data` (rather than annotate
15one), it goes through ``type(self._data)`` instead of importing the class; see
16``_stats/_reporting.py::_summary_frame`` and ``_data_reshape.py::_rebuild``.
18Each consumer annotates against a structural Protocol:
20- `DataLike` and `StatsLike` (this module) are shared by every subpackage —
21 there is exactly one definition of each.
22- ``PortfolioLike`` is deliberately *not* shared: each subpackage declares its
23 own (``_plots/_protocol.py``, ``_reports/_protocol.py``,
24 ``_utils/_protocol.py``) listing only the members it actually consumes
25 (interface segregation). Keep it that way — a merged PortfolioLike would
26 re-couple the subpackages to the full Portfolio surface.
27"""
29from __future__ import annotations
31from collections.abc import Iterator
32from typing import Protocol, runtime_checkable
34import polars as pl
37class StatsLike(Protocol): # pragma: no cover
38 """Structural interface for the statistics facade used by reports."""
40 def summary(self) -> pl.DataFrame:
41 """Full summary DataFrame (one row per metric, one column per asset)."""
42 ...
45@runtime_checkable
46class DataLike(Protocol): # pragma: no cover
47 """Authoritative structural interface for Data consumers.
49 Union of the members required by the stats mixins, plots, reports, and
50 utils — annotating against the superset is harmless for consumers that
51 use only part of it, and keeps a single definition.
52 """
54 @property
55 def returns(self) -> pl.DataFrame:
56 """Return DataFrame (asset columns only, no benchmark or date)."""
57 ...
59 @property
60 def index(self) -> pl.DataFrame:
61 """Date / time index DataFrame."""
62 ...
64 @property
65 def benchmark(self) -> pl.DataFrame | None:
66 """Benchmark DataFrame, or None when no benchmark was provided."""
67 ...
69 @property
70 def all(self) -> pl.DataFrame:
71 """Combined DataFrame of date index, return, and benchmark columns."""
72 ...
74 @property
75 def assets(self) -> list[str]:
76 """Names of the asset return columns."""
77 ...
79 @property
80 def date_col(self) -> list[str]:
81 """Column names used as the date/time index."""
82 ...
84 @property
85 def stats(self) -> StatsLike:
86 """Statistics facade used by reports."""
87 ...
89 @property
90 def _periods_per_year(self) -> float:
91 """Estimated number of return periods per calendar year."""
92 ...
94 def items(self) -> Iterator[tuple[str, pl.Series]]:
95 """Iterate over (asset_name, returns_series) pairs."""
96 ...