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 +21 -0
- pomata/_expr.py +230 -0
- pomata/indicators/__init__.py +181 -0
- pomata/indicators/channel.py +563 -0
- pomata/indicators/cycle.py +754 -0
- pomata/indicators/directional_movement.py +971 -0
- pomata/indicators/momentum.py +1994 -0
- pomata/indicators/moving_average.py +1245 -0
- pomata/indicators/price_transform.py +437 -0
- pomata/indicators/statistic.py +787 -0
- pomata/indicators/stochastic.py +300 -0
- pomata/indicators/trend.py +408 -0
- pomata/indicators/volatility.py +503 -0
- pomata/indicators/volume.py +864 -0
- pomata/metrics/__init__.py +149 -0
- pomata/metrics/drawdown.py +570 -0
- pomata/metrics/performance.py +451 -0
- pomata/metrics/ratio.py +1522 -0
- pomata/metrics/relative.py +1408 -0
- pomata/metrics/risk.py +2006 -0
- pomata/pnl/__init__.py +68 -0
- pomata/pnl/accounting.py +916 -0
- pomata/pnl/costs.py +674 -0
- pomata/pnl/returns.py +183 -0
- pomata/py.typed +0 -0
- pomata-0.1.0.dist-info/METADATA +235 -0
- pomata-0.1.0.dist-info/RECORD +29 -0
- pomata-0.1.0.dist-info/WHEEL +4 -0
- pomata-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|