Verifiably-correct, Polars-native quant toolkit: technical indicators, performance & risk metrics, and PnL accounting.
Project description
pomata
A Polars-native quant toolkit — technical indicators, PnL accounting, and performance & risk metrics. Each is a
composable pl.Expr, so an entire study is one lazy Polars pipeline, from price to performance.
And it doesn't ask you to trust its numbers — it proves them: every function is verified to the float64 floor
against an independent reference, under 100% branch coverage.
Install
pip install pomata
# or
uv add pomata
From source:
git clone https://github.com/ilpomo/pomata
cd pomata && uv sync
Dependencies
- Runtime —
polarsonly (>= 1.40). Nothing else is pulled into your environment. - Python — 3.12, 3.13, 3.14.
- Optional — the
differentialgroup (TA-Lib) powers the cross-reference parity tier; the other groups are the contributor gate. See CONTRIBUTING.md.
The data
Every snippet below runs on the same sample: a quarter of real daily bars for AAPL, GOOG, and NVDA, checked into
the repo at docs/_static/ohlcv_sample.parquet.
Clone the repo to run the snippets as-is, or point read_parquet at your own OHLCV frame:
import polars as pl
ohlcv = pl.read_parquet("docs/_static/ohlcv_sample.parquet")
ohlcv.head(9)
shape: (9, 7)
┌────────────────────────────────┬────────┬────────┬────────┬────────┬────────┬───────────┐
│ datetime ┆ ticker ┆ open ┆ high ┆ low ┆ close ┆ volume │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs, America/New_York] ┆ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 │
╞════════════════════════════════╪════════╪════════╪════════╪════════╪════════╪═══════════╡
│ 2024-01-02 17:00:00 EST ┆ AAPL ┆ 185.06 ┆ 186.33 ┆ 181.83 ┆ 183.56 ┆ 82488700 │
│ 2024-01-02 17:00:00 EST ┆ GOOG ┆ 138.38 ┆ 139.39 ┆ 136.54 ┆ 138.34 ┆ 20071900 │
│ 2024-01-02 17:00:00 EST ┆ NVDA ┆ 49.16 ┆ 49.21 ┆ 47.51 ┆ 48.08 ┆ 411254000 │
│ 2024-01-03 17:00:00 EST ┆ AAPL ┆ 182.16 ┆ 183.8 ┆ 181.38 ┆ 182.19 ┆ 58414500 │
│ 2024-01-03 17:00:00 EST ┆ GOOG ┆ 137.39 ┆ 139.86 ┆ 137.22 ┆ 139.13 ┆ 18974300 │
│ 2024-01-03 17:00:00 EST ┆ NVDA ┆ 47.4 ┆ 48.1 ┆ 47.24 ┆ 47.48 ┆ 320896000 │
│ 2024-01-04 17:00:00 EST ┆ AAPL ┆ 180.11 ┆ 181.04 ┆ 178.86 ┆ 179.87 ┆ 71983600 │
│ 2024-01-04 17:00:00 EST ┆ GOOG ┆ 138.63 ┆ 139.41 ┆ 136.8 ┆ 136.83 ┆ 18253300 │
│ 2024-01-04 17:00:00 EST ┆ NVDA ┆ 47.68 ┆ 48.41 ┆ 47.42 ┆ 47.91 ┆ 306535000 │
└────────────────────────────────┴────────┴────────┴────────┴────────┴────────┴───────────┘
Technical Indicators
75 indicators, each a pl.Expr you compute straight on your price frame, each verified against an independent oracle —
and 58 of the 75 cross-checked against TA-Lib too, at a relative 1e-10 (the other 17 have no TA-Lib twin or a
documented divergence). On a multi-ticker panel, wrap the call in .over("ticker") so each symbol warms up on its own
history (null until the window fills, never a fabricated value):
from pomata.indicators import rsi
ohlcv.with_columns(
rsi=rsi(pl.col("close"), 14).over("ticker").round(2),
).select("datetime", "ticker", "close", "rsi").tail(9)
shape: (9, 4)
┌────────────────────────────────┬────────┬────────┬───────┐
│ datetime ┆ ticker ┆ close ┆ rsi │
│ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs, America/New_York] ┆ str ┆ f64 ┆ f64 │
╞════════════════════════════════╪════════╪════════╪═══════╡
│ 2024-03-26 17:00:00 EDT ┆ AAPL ┆ 168.02 ┆ 37.64 │
│ 2024-03-26 17:00:00 EDT ┆ GOOG ┆ 150.37 ┆ 64.6 │
│ 2024-03-26 17:00:00 EDT ┆ NVDA ┆ 92.4 ┆ 65.66 │
│ 2024-03-27 17:00:00 EDT ┆ AAPL ┆ 171.59 ┆ 45.62 │
│ 2024-03-27 17:00:00 EDT ┆ GOOG ┆ 150.61 ┆ 64.95 │
│ 2024-03-27 17:00:00 EDT ┆ NVDA ┆ 90.09 ┆ 60.06 │
│ 2024-03-28 17:00:00 EDT ┆ AAPL ┆ 169.78 ┆ 42.64 │
│ 2024-03-28 17:00:00 EDT ┆ GOOG ┆ 150.93 ┆ 65.43 │
│ 2024-03-28 17:00:00 EDT ┆ NVDA ┆ 90.2 ┆ 60.24 │
└────────────────────────────────┴────────┴────────┴───────┘
Multi-output indicators (bollinger_bands, macd, stochastic_slow, …) return a single pl.Struct — pick a line
with .struct.field(...) or expand every line with .struct.unnest().
All 75 indicators, by category
- channel (5) —
donchian_channels,ichimoku,keltner_channels,midpoint,midprice - cycle (7) —
dominant_cycle_period,dominant_cycle_phase,hilbert_phasor,hilbert_trendline,mama,sine_wave,trend_mode - directional movement (8) —
adx,adxr,di_minus,di_plus,dm_minus,dm_plus,dx,vortex - momentum (17) —
absolute_price_oscillator,aroon,aroon_oscillator,awesome_oscillator,balance_of_power,cci,chande_momentum_oscillator,fisher_transform,macd,mom,percentage_price_oscillator,roc,rsi,rsi_stochastic,trix,ultimate_oscillator,williams_r - moving average (11) —
dema,ema,hma,kama,rma,sma,t3,tema,trima,vwma,wma - price transform (4) —
price_average,price_median,price_typical,price_weighted_close - statistic (9) —
linear_regression,linear_regression_angle,linear_regression_intercept,linear_regression_slope,standard_deviation_ewma,standard_deviation_rolling,time_series_forecast,variance_ewma,variance_rolling - stochastic (2) —
stochastic_fast,stochastic_slow - trend (2) —
parabolic_sar,supertrend - volatility (4) —
atr,atr_normalized,bollinger_bands,true_range - volume (6) —
accumulation_distribution,accumulation_distribution_oscillator,chaikin_money_flow,money_flow_index,obv,vwap
PnL Accounting
18 functions that turn a signal into money. An indicator becomes a signed weight; returns_gross /
cost_proportional / returns_net turn that into a costed return, and the .shift(1) on the signal is the whole
no-look-ahead story — a decision at the close acts on the next bar. Every degenerate input (null / NaN / 0 /
±inf / warm-up) has a defined, documented, tested behavior:
from pomata.pnl import returns_simple, returns_gross, returns_net, cost_proportional
pnl = (
ohlcv
.with_columns(
weight=(rsi(pl.col("close"), 14) > 50).cast(pl.Float64).shift(1).over("ticker"),
asset_returns=returns_simple(pl.col("close")).over("ticker"),
)
.with_columns(
net=returns_net(
returns_gross(pl.col("weight"), pl.col("asset_returns")),
cost_proportional(pl.col("weight"), rate=0.001).over("ticker"),
),
)
)
pnl.select("datetime", "ticker", "weight", pl.col("net").round(4)).tail(9)
shape: (9, 4)
┌────────────────────────────────┬────────┬────────┬─────────┐
│ datetime ┆ ticker ┆ weight ┆ net │
│ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs, America/New_York] ┆ str ┆ f64 ┆ f64 │
╞════════════════════════════════╪════════╪════════╪═════════╡
│ 2024-03-26 17:00:00 EDT ┆ AAPL ┆ 0.0 ┆ -0.0 │
│ 2024-03-26 17:00:00 EDT ┆ GOOG ┆ 1.0 ┆ 0.0036 │
│ 2024-03-26 17:00:00 EDT ┆ NVDA ┆ 1.0 ┆ -0.0257 │
│ 2024-03-27 17:00:00 EDT ┆ AAPL ┆ 0.0 ┆ 0.0 │
│ 2024-03-27 17:00:00 EDT ┆ GOOG ┆ 1.0 ┆ 0.0016 │
│ 2024-03-27 17:00:00 EDT ┆ NVDA ┆ 1.0 ┆ -0.025 │
│ 2024-03-28 17:00:00 EDT ┆ AAPL ┆ 0.0 ┆ -0.0 │
│ 2024-03-28 17:00:00 EDT ┆ GOOG ┆ 1.0 ┆ 0.0021 │
│ 2024-03-28 17:00:00 EDT ┆ NVDA ┆ 1.0 ┆ 0.0012 │
└────────────────────────────────┴────────┴────────┴─────────┘
All 18 PnL functions
- cash flow —
cost_borrow,cost_fixed,cost_funding,cost_notional,cost_per_share,cumulative_pnl,dividend,pnl_gross,pnl_gross_inverse,pnl_net - returns flow —
cost_proportional,cost_slippage,equity_curve,returns_gross,returns_log,returns_net,returns_simple,turnover
Performance & Risk Metrics
60 reducing pl.Expr — point one at the net returns and it folds the whole history into the figure you report:
Sharpe, Sortino, Calmar, drawdown, VaR/CVaR, capture, benchmark-relative, and a rolling twin for every windowed form. A
null is skipped; a non-null NaN poisons the result loudly, rather than passing a plausible lie downstream:
from pomata.pnl import equity_curve
from pomata.metrics import sharpe_ratio, total_return, max_drawdown
report = (
pnl
.group_by("ticker", maintain_order=True)
.agg(
sharpe=sharpe_ratio(pl.col("net"), periods_per_year=252).round(2),
total_return=total_return(equity_curve(pl.col("net"))).round(4),
max_drawdown=max_drawdown(equity_curve(pl.col("net"))).round(4),
)
)
report
shape: (3, 4)
┌────────┬────────┬──────────────┬──────────────┐
│ ticker ┆ sharpe ┆ total_return ┆ max_drawdown │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 │
╞════════╪════════╪══════════════╪══════════════╡
│ AAPL ┆ -3.97 ┆ -0.0788 ┆ -0.0773 │
│ GOOG ┆ -0.67 ┆ -0.0384 ┆ -0.1359 │
│ NVDA ┆ 4.16 ┆ 0.4727 ┆ -0.087 │
└────────┴────────┴──────────────┴──────────────┘
All 60 metrics
- drawdown —
conditional_drawdown_at_risk,drawdown,drawdown_rolling,max_drawdown,max_drawdown_duration,pain_index,ulcer_index - performance —
cagr,cagr_rolling,stability,total_return,total_return_rolling - ratio —
adjusted_sharpe_ratio,burke_ratio,calmar_ratio,common_sense_ratio,gain_to_pain_ratio,omega_ratio,omega_ratio_rolling,pain_ratio,probabilistic_sharpe_ratio,recovery_ratio,sharpe_ratio,sharpe_ratio_rolling,sortino_ratio,sortino_ratio_rolling,sterling_ratio,ulcer_performance_ratio - relative —
alpha,alpha_rolling,beta,beta_rolling,capture_downside_ratio,capture_ratio,capture_upside_ratio,information_ratio,information_ratio_rolling,modigliani_risk_adjusted_performance,treynor_ratio,treynor_ratio_rolling - risk —
conditional_value_at_risk,downside_deviation,downside_deviation_rolling,kelly_criterion,kurtosis,kurtosis_rolling,payoff_ratio,profit_ratio,risk_of_ruin,skewness,skewness_rolling,tail_ratio,tail_ratio_rolling,value_at_risk,value_at_risk_modified,value_at_risk_parametric,value_at_risk_rolling,volatility,volatility_rolling,win_rate
The whole study, one lazy query
Every step above is a pl.Expr, so the four of them fuse into a single lazy pipeline — no intermediate frames, no glue,
no second dependency between the steps. Run it on .lazy() and .collect() the same three-row verdict:
report = (
ohlcv.lazy()
.with_columns(
weight=(rsi(pl.col("close"), 14) > 50).cast(pl.Float64).shift(1).over("ticker"),
asset_returns=returns_simple(pl.col("close")).over("ticker"),
)
.with_columns(
net=returns_net(
returns_gross(pl.col("weight"), pl.col("asset_returns")),
cost_proportional(pl.col("weight"), rate=0.001).over("ticker"),
),
)
.group_by("ticker", maintain_order=True)
.agg(
sharpe=sharpe_ratio(pl.col("net"), periods_per_year=252).round(2),
total_return=total_return(equity_curve(pl.col("net"))).round(4),
max_drawdown=max_drawdown(equity_curve(pl.col("net"))).round(4),
)
.collect()
)
shape: (3, 4)
┌────────┬────────┬──────────────┬──────────────┐
│ ticker ┆ sharpe ┆ total_return ┆ max_drawdown │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 │
╞════════╪════════╪══════════════╪══════════════╡
│ AAPL ┆ -3.97 ┆ -0.0788 ┆ -0.0773 │
│ GOOG ┆ -0.67 ┆ -0.0384 ┆ -0.1359 │
│ NVDA ┆ 4.16 ┆ 0.4727 ┆ -0.087 │
└────────┴────────┴──────────────┴──────────────┘
Same three numbers, arrived at as one query: the indicator fed the signal, the signal fed the PnL, the PnL fed the
metrics, and the optimizer fused the lot. Momentum paid in a quarter NVDA ran and cost a little where AAPL slid — the
.over("ticker") keeps a three-name panel, or a five-hundred-name one, equally separate.
Correctness
Verified, not asserted. Every function is written twice: the shipped pl.Expr, and a second, independent oracle
that shares no code with it. The two must agree — on fixed series, frozen golden masters, and thousands of fuzzed
inputs, under 100% branch coverage — or the build is red.
Each family is then held to the yardstick that catches its bugs: indicators to the digit against an independent oracle — and, for the 58 of 75 with a twin, against the public TA-Lib reference too; PnL and metrics at the edges, where every degenerate input has a defined, tested behavior.
The full account — the precision guarantee, the receipts, and exactly what is and is not claimed — is on the trust page and in CORRECTNESS.md.
Where pomata fits
pomata is for the quant already working in Polars. Each function is a free-standing pl.Expr with polars as the only
runtime dependency, composable across eager, lazy, single-series, and grouped (.over) contexts — so the everyday
primitives live in one coherent toolkit instead of a wired-together stack.
It is vectorized analytics and accounting: indicators, total mark-to-market PnL, and metrics. It is not an execution engine — no order fills, no event loop, no lot accounting.
Project
- Contributing — see CONTRIBUTING.md; the full gate (lint, three gating type checkers plus an advisory fourth, doctests, 100% branch coverage) runs on every commit.
- License — MIT, see LICENSE.
- Citation — CITATION.cff.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pomata-0.2.2.tar.gz.
File metadata
- Download URL: pomata-0.2.2.tar.gz
- Upload date:
- Size: 555.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
772a141f7142b0b389ae41bae5a51b623277b032ba14dda991a8aaccf56d2ea6
|
|
| MD5 |
f47cdcf68cdbbc9d13b517afef0b138d
|
|
| BLAKE2b-256 |
d46f77b228cb319e3a4b2fc9ef4204830d3de31eb3a199ddf9405bffd5514eb7
|
Provenance
The following attestation bundles were made for pomata-0.2.2.tar.gz:
Publisher:
release.yml on ilpomo/pomata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pomata-0.2.2.tar.gz -
Subject digest:
772a141f7142b0b389ae41bae5a51b623277b032ba14dda991a8aaccf56d2ea6 - Sigstore transparency entry: 2084229428
- Sigstore integration time:
-
Permalink:
ilpomo/pomata@7781ad2fbc6bbdfc026b78712d275a037aa6f366 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/ilpomo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7781ad2fbc6bbdfc026b78712d275a037aa6f366 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pomata-0.2.2-py3-none-any.whl.
File metadata
- Download URL: pomata-0.2.2-py3-none-any.whl
- Upload date:
- Size: 166.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f90c7f891cd8f962622b45745109ce5a1787c23bdc8042196ea9eaaf9299c7b
|
|
| MD5 |
7b4f61cd115802e15945019e20005a16
|
|
| BLAKE2b-256 |
3e5d90b8a2b273099d7a8484e47b6476dba7b5ae094b55617e219aae321b1ab4
|
Provenance
The following attestation bundles were made for pomata-0.2.2-py3-none-any.whl:
Publisher:
release.yml on ilpomo/pomata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pomata-0.2.2-py3-none-any.whl -
Subject digest:
9f90c7f891cd8f962622b45745109ce5a1787c23bdc8042196ea9eaaf9299c7b - Sigstore transparency entry: 2084229433
- Sigstore integration time:
-
Permalink:
ilpomo/pomata@7781ad2fbc6bbdfc026b78712d275a037aa6f366 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/ilpomo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7781ad2fbc6bbdfc026b78712d275a037aa6f366 -
Trigger Event:
release
-
Statement type: