pomata 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pomata/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ pomata — a verifiably-correct, Polars-native quant toolkit.
3
+
4
+ Three families of atomic, composable building blocks:
5
+
6
+ - ``pomata.indicators`` — technical-analysis indicators as ``pl.Expr`` factories.
7
+ - ``pomata.metrics`` — performance & risk metrics.
8
+ - ``pomata.pnl`` — profit-and-loss accounting and transaction-cost models.
9
+
10
+ Every public function returns a free-standing Polars expression (or a small, explicit object); nothing forces a
11
+ dataframe shape on the caller.
12
+ """
13
+
14
+ from importlib.metadata import PackageNotFoundError, version
15
+
16
+ try:
17
+ __version__ = version("pomata")
18
+ except PackageNotFoundError: # running from a source tree that was never installed
19
+ __version__ = "0.0.0"
20
+
21
+ __all__: tuple[str, ...] = ()
pomata/_expr.py ADDED
@@ -0,0 +1,230 @@
1
+ """
2
+ Shared input-normalization helper for the indicator and pnl factories.
3
+
4
+ A leaf module: it imports only ``polars`` and nothing in the package imports back into it, so it never participates in a
5
+ cycle. Every public factory routes each of its ``pl.Expr`` inputs through :func:`float64_expr` at the top of its body,
6
+ which gives the package two guarantees uniformly: a clear, early error when a caller passes a bare column name instead
7
+ of an expression, and a single output dtype (``Float64``) regardless of the input's numeric dtype.
8
+ """
9
+
10
+ import math
11
+ from typing import cast
12
+
13
+ import polars as pl
14
+
15
+
16
+ def float64_expr(
17
+ expr: pl.Expr,
18
+ ) -> pl.Expr:
19
+ """
20
+ Normalize a factory input to a ``Float64`` expression.
21
+
22
+ Validates that ``expr`` is a Polars expression (the factories take ``pl.Expr`` only, never string column names) and
23
+ casts it to ``Float64`` so that every indicator and pnl factory has one predictable output dtype: an ``Int64`` or
24
+ ``Float32`` input yields a ``Float64`` result, exactly as a ``Float64`` input would, with no value drift.
25
+
26
+ Args:
27
+ expr: Input series, typically a price column (e.g. ``pl.col("close")``).
28
+
29
+ Returns:
30
+ ``expr`` cast to ``Float64``.
31
+
32
+ Raises:
33
+ TypeError: If ``expr`` is not a ``pl.Expr`` (e.g. a bare ``str`` column name was passed).
34
+
35
+ Note:
36
+ The cast is a no-op when the input is already ``Float64``. A non-numeric column (e.g. a string column referenced
37
+ by a valid ``pl.Expr``) still fails later, at collection time, where Polars reports the dtype error.
38
+ """
39
+ # Widen to ``object`` so the runtime guard against a caller passing a bare column-name string is a genuine narrowing
40
+ # (not a check the type system deems unnecessary), while the public signature still promises ``pl.Expr``.
41
+ candidate = cast("object", expr)
42
+ if not isinstance(candidate, pl.Expr):
43
+ raise TypeError(
44
+ f'expected a Polars expression (pl.Expr), got {type(candidate).__name__}; pass pl.col("close"), '
45
+ f"not a bare column name"
46
+ )
47
+ return candidate.cast(pl.Float64)
48
+
49
+
50
+ def validate_window(
51
+ window: int,
52
+ minimum: int = 1,
53
+ *,
54
+ name: str = "window",
55
+ ) -> None:
56
+ """
57
+ Validate a lookback ``window`` against its minimum, raising the canonical message on failure.
58
+
59
+ Centralizes the ``window >= minimum`` guard every factory shares, so the bound and the error message stay identical
60
+ across the package and cannot drift as new indicators are added. Most factories accept ``minimum=1``; pass
61
+ ``minimum=2`` where a single-observation window degenerates (e.g. :func:`dema`, the linear-regression family). Pass
62
+ ``name`` to validate a differently-named lookback (e.g. ``window_fast``, ``window_k``) with the same message shape.
63
+
64
+ Args:
65
+ window: Number of observations in the moving window.
66
+ minimum: Smallest accepted window.
67
+ name: The parameter name, for the error message.
68
+
69
+ Raises:
70
+ ValueError: If ``window < minimum``.
71
+ """
72
+ if window < minimum:
73
+ raise ValueError(f"{name} must be >= {minimum}, got {window}")
74
+
75
+
76
+ def validate_ddof(
77
+ ddof: int,
78
+ window: int,
79
+ ) -> None:
80
+ """
81
+ Validate that the delta degrees of freedom leave a positive divisor, raising the canonical message on failure.
82
+
83
+ Centralizes the ``ddof < window`` guard the rolling-variance factories share, so the divisor ``window - ddof``
84
+ stays positive and the bound and its message stay identical across the package.
85
+
86
+ Args:
87
+ ddof: Delta degrees of freedom; the divisor is ``window - ddof``.
88
+ window: Number of observations in the moving window.
89
+
90
+ Raises:
91
+ ValueError: If ``ddof >= window``.
92
+ """
93
+ if ddof >= window:
94
+ raise ValueError(f"ddof must be < window, got ddof={ddof}, window={window}")
95
+
96
+
97
+ def validate_window_order(
98
+ window_fast: int,
99
+ window_slow: int,
100
+ *,
101
+ fast_name: str = "window_fast",
102
+ slow_name: str = "window_slow",
103
+ ) -> None:
104
+ """
105
+ Validate that a fast/slow window pair is ordered, raising the canonical message on failure.
106
+
107
+ Centralizes the ``window_fast <= window_slow`` guard the dual-window factories share (the oscillator pairs and
108
+ KAMA's fast/slow smoothing windows), so the bound and the message stay identical across the package and cannot
109
+ drift. Pass ``fast_name`` / ``slow_name`` to validate a differently-named pair with the same message shape
110
+ (mirroring :func:`validate_window`'s ``name``).
111
+
112
+ Args:
113
+ window_fast: The shorter (faster) window.
114
+ window_slow: The longer (slower) window.
115
+ fast_name: The faster parameter's name, for the error message.
116
+ slow_name: The slower parameter's name, for the error message.
117
+
118
+ Raises:
119
+ ValueError: If ``window_fast > window_slow``.
120
+ """
121
+ if window_fast > window_slow:
122
+ raise ValueError(
123
+ f"{fast_name} must be <= {slow_name}, got {fast_name}={window_fast}, {slow_name}={window_slow}"
124
+ )
125
+
126
+
127
+ def validate_periods_per_year(
128
+ periods_per_year: int,
129
+ ) -> None:
130
+ """
131
+ Validate the annualization factor, raising the canonical message on failure.
132
+
133
+ Centralizes the ``periods_per_year >= 1`` guard shared by every annualized metric, so the bound and the message
134
+ stay identical across the package and cannot drift.
135
+
136
+ Args:
137
+ periods_per_year: Observations per year used to annualize.
138
+
139
+ Raises:
140
+ ValueError: If ``periods_per_year < 1``.
141
+ """
142
+ if periods_per_year < 1:
143
+ raise ValueError(f"periods_per_year must be >= 1, got {periods_per_year}")
144
+
145
+
146
+ def per_period_rate(
147
+ annual_rate: float,
148
+ periods_per_year: int,
149
+ ) -> float:
150
+ """
151
+ Convert an annualized rate to its per-period equivalent geometrically.
152
+
153
+ The geometric (compounding) conversion ``(1 + annual_rate)^(1 / P) - 1`` the annualized ratios share for their
154
+ risk-free-rate handling (Sharpe, Sortino, alpha, Treynor, ...), so the convention stays identical across the
155
+ package. The caller validates ``periods_per_year`` separately (via :func:`validate_periods_per_year`).
156
+
157
+ Args:
158
+ annual_rate: The annualized rate (e.g. a risk-free rate).
159
+ periods_per_year: Observations per year used to de-annualize.
160
+
161
+ Returns:
162
+ The per-period rate.
163
+ """
164
+ return math.pow(1.0 + annual_rate, 1.0 / periods_per_year) - 1.0
165
+
166
+
167
+ def validate_finite(
168
+ value: float,
169
+ name: str,
170
+ ) -> None:
171
+ """
172
+ Validate a scalar tuning parameter is a finite number, raising the canonical message on failure.
173
+
174
+ Shared by the metric factories that take a finite scalar knob (``threshold``, ``risk_free_rate``), so a ``NaN`` or
175
+ ``inf`` is rejected at the call site rather than silently poisoning the result.
176
+
177
+ Args:
178
+ value: The scalar parameter value.
179
+ name: The parameter name, for the error message.
180
+
181
+ Raises:
182
+ ValueError: If ``value`` is not finite.
183
+ """
184
+ if not math.isfinite(value):
185
+ raise ValueError(f"{name} must be a finite number, got {value}")
186
+
187
+
188
+ def validate_positive(
189
+ value: float,
190
+ name: str,
191
+ *,
192
+ allow_zero: bool = False,
193
+ ) -> None:
194
+ """
195
+ Validate a scalar tuning parameter is a finite, positive number, raising the canonical message on failure.
196
+
197
+ Shared by the factories that take a finite magnitude knob (``multiplier``, ``rate``, ``fee``, ``acceleration``, …),
198
+ so a ``NaN``, ``inf``, or out-of-sign value is rejected at the call site rather than silently poisoning the result.
199
+ Pass ``allow_zero=True`` where zero is a valid no-op (e.g. a zero cost ``rate``).
200
+
201
+ Args:
202
+ value: The scalar parameter value.
203
+ name: The parameter name, for the error message.
204
+ allow_zero: Whether zero is accepted (``>= 0`` instead of ``> 0``).
205
+
206
+ Raises:
207
+ ValueError: If ``value`` is not finite, or is negative (non-positive when ``allow_zero`` is ``False``).
208
+ """
209
+ if not math.isfinite(value) or (value < 0.0 if allow_zero else value <= 0.0):
210
+ bound = ">= 0" if allow_zero else "> 0"
211
+ raise ValueError(f"{name} must be a finite number {bound}, got {value}")
212
+
213
+
214
+ def validate_confidence(
215
+ confidence: float,
216
+ ) -> None:
217
+ """
218
+ Validate a tail confidence level lies strictly inside ``(0, 1)``, raising the canonical message on failure.
219
+
220
+ Shared by the historical tail-risk metrics (value-at-risk, conditional value-at-risk), so the open-interval bound
221
+ and its message stay identical across the package.
222
+
223
+ Args:
224
+ confidence: The tail confidence level.
225
+
226
+ Raises:
227
+ ValueError: If ``confidence`` is not in the open interval ``(0, 1)``.
228
+ """
229
+ if not 0.0 < confidence < 1.0:
230
+ raise ValueError(f"confidence must be in the open interval (0, 1), got {confidence}")
@@ -0,0 +1,181 @@
1
+ """
2
+ Technical-analysis indicators as ``pl.Expr`` factories.
3
+
4
+ Source is organized into theme modules for maintainability; this package re-exports a flat public API.
5
+ """
6
+
7
+ from pomata.indicators.channel import (
8
+ donchian_channels,
9
+ ichimoku,
10
+ keltner_channels,
11
+ midpoint,
12
+ midprice,
13
+ )
14
+ from pomata.indicators.cycle import (
15
+ dominant_cycle_period,
16
+ dominant_cycle_phase,
17
+ hilbert_phasor,
18
+ hilbert_trendline,
19
+ mama,
20
+ sine_wave,
21
+ trend_mode,
22
+ )
23
+ from pomata.indicators.directional_movement import (
24
+ adx,
25
+ adxr,
26
+ di_minus,
27
+ di_plus,
28
+ dm_minus,
29
+ dm_plus,
30
+ dx,
31
+ vortex,
32
+ )
33
+ from pomata.indicators.momentum import (
34
+ absolute_price_oscillator,
35
+ aroon,
36
+ aroon_oscillator,
37
+ awesome_oscillator,
38
+ balance_of_power,
39
+ cci,
40
+ chande_momentum_oscillator,
41
+ fisher_transform,
42
+ macd,
43
+ mom,
44
+ percentage_price_oscillator,
45
+ roc,
46
+ rsi,
47
+ rsi_stochastic,
48
+ trix,
49
+ ultimate_oscillator,
50
+ williams_r,
51
+ )
52
+ from pomata.indicators.moving_average import (
53
+ dema,
54
+ ema,
55
+ hma,
56
+ kama,
57
+ rma,
58
+ sma,
59
+ t3,
60
+ tema,
61
+ trima,
62
+ vwma,
63
+ wma,
64
+ )
65
+ from pomata.indicators.price_transform import (
66
+ price_average,
67
+ price_median,
68
+ price_typical,
69
+ price_weighted_close,
70
+ )
71
+ from pomata.indicators.statistic import (
72
+ linear_regression,
73
+ linear_regression_angle,
74
+ linear_regression_intercept,
75
+ linear_regression_slope,
76
+ standard_deviation_ewma,
77
+ standard_deviation_rolling,
78
+ time_series_forecast,
79
+ variance_ewma,
80
+ variance_rolling,
81
+ )
82
+ from pomata.indicators.stochastic import (
83
+ stochastic_fast,
84
+ stochastic_slow,
85
+ )
86
+ from pomata.indicators.trend import (
87
+ parabolic_sar,
88
+ supertrend,
89
+ )
90
+ from pomata.indicators.volatility import (
91
+ atr,
92
+ atr_normalized,
93
+ bollinger_bands,
94
+ true_range,
95
+ )
96
+ from pomata.indicators.volume import (
97
+ accumulation_distribution,
98
+ accumulation_distribution_oscillator,
99
+ chaikin_money_flow,
100
+ money_flow_index,
101
+ obv,
102
+ vwap,
103
+ )
104
+
105
+ __all__ = (
106
+ "absolute_price_oscillator",
107
+ "accumulation_distribution",
108
+ "accumulation_distribution_oscillator",
109
+ "adx",
110
+ "adxr",
111
+ "aroon",
112
+ "aroon_oscillator",
113
+ "atr",
114
+ "atr_normalized",
115
+ "awesome_oscillator",
116
+ "balance_of_power",
117
+ "bollinger_bands",
118
+ "cci",
119
+ "chaikin_money_flow",
120
+ "chande_momentum_oscillator",
121
+ "dema",
122
+ "di_minus",
123
+ "di_plus",
124
+ "dm_minus",
125
+ "dm_plus",
126
+ "dominant_cycle_period",
127
+ "dominant_cycle_phase",
128
+ "donchian_channels",
129
+ "dx",
130
+ "ema",
131
+ "fisher_transform",
132
+ "hilbert_phasor",
133
+ "hilbert_trendline",
134
+ "hma",
135
+ "ichimoku",
136
+ "kama",
137
+ "keltner_channels",
138
+ "linear_regression",
139
+ "linear_regression_angle",
140
+ "linear_regression_intercept",
141
+ "linear_regression_slope",
142
+ "macd",
143
+ "mama",
144
+ "midpoint",
145
+ "midprice",
146
+ "mom",
147
+ "money_flow_index",
148
+ "obv",
149
+ "parabolic_sar",
150
+ "percentage_price_oscillator",
151
+ "price_average",
152
+ "price_median",
153
+ "price_typical",
154
+ "price_weighted_close",
155
+ "rma",
156
+ "roc",
157
+ "rsi",
158
+ "rsi_stochastic",
159
+ "sine_wave",
160
+ "sma",
161
+ "standard_deviation_ewma",
162
+ "standard_deviation_rolling",
163
+ "stochastic_fast",
164
+ "stochastic_slow",
165
+ "supertrend",
166
+ "t3",
167
+ "tema",
168
+ "time_series_forecast",
169
+ "trend_mode",
170
+ "trima",
171
+ "trix",
172
+ "true_range",
173
+ "ultimate_oscillator",
174
+ "variance_ewma",
175
+ "variance_rolling",
176
+ "vortex",
177
+ "vwap",
178
+ "vwma",
179
+ "williams_r",
180
+ "wma",
181
+ )