Coverage for src/jquantstats/_protocol.py: 100%

4 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-23 06:13 +0000

1"""Shared protocol definitions used across jquantstats subpackages. 

2 

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. 

8Instead, each consumer annotates against a structural Protocol: 

9 

10- `DataLike` and `StatsLike` (this module) are shared by every subpackage — 

11 there is exactly one definition of each. 

12- ``PortfolioLike`` is deliberately *not* shared: each subpackage declares its 

13 own (``_plots/_protocol.py``, ``_reports/_protocol.py``, 

14 ``_utils/_protocol.py``) listing only the members it actually consumes 

15 (interface segregation). Keep it that way — a merged PortfolioLike would 

16 re-couple the subpackages to the full Portfolio surface. 

17""" 

18 

19from __future__ import annotations 

20 

21from collections.abc import Iterator 

22from typing import Protocol, runtime_checkable 

23 

24import polars as pl 

25 

26 

27class StatsLike(Protocol): # pragma: no cover 

28 """Structural interface for the statistics facade used by reports.""" 

29 

30 def summary(self) -> pl.DataFrame: 

31 """Full summary DataFrame (one row per metric, one column per asset).""" 

32 ... 

33 

34 

35@runtime_checkable 

36class DataLike(Protocol): # pragma: no cover 

37 """Authoritative structural interface for Data consumers. 

38 

39 Union of the members required by the stats mixins, plots, reports, and 

40 utils — annotating against the superset is harmless for consumers that 

41 use only part of it, and keeps a single definition. 

42 """ 

43 

44 @property 

45 def returns(self) -> pl.DataFrame: 

46 """Return DataFrame (asset columns only, no benchmark or date).""" 

47 ... 

48 

49 @property 

50 def index(self) -> pl.DataFrame: 

51 """Date / time index DataFrame.""" 

52 ... 

53 

54 @property 

55 def benchmark(self) -> pl.DataFrame | None: 

56 """Benchmark DataFrame, or None when no benchmark was provided.""" 

57 ... 

58 

59 @property 

60 def all(self) -> pl.DataFrame: 

61 """Combined DataFrame of date index, return, and benchmark columns.""" 

62 ... 

63 

64 @property 

65 def assets(self) -> list[str]: 

66 """Names of the asset return columns.""" 

67 ... 

68 

69 @property 

70 def date_col(self) -> list[str]: 

71 """Column names used as the date/time index.""" 

72 ... 

73 

74 @property 

75 def stats(self) -> StatsLike: 

76 """Statistics facade used by reports.""" 

77 ... 

78 

79 @property 

80 def _periods_per_year(self) -> float: 

81 """Estimated number of return periods per calendar year.""" 

82 ... 

83 

84 def items(self) -> Iterator[tuple[str, pl.Series]]: 

85 """Iterate over (asset_name, returns_series) pairs.""" 

86 ...