Skip to main content

pomata

DOI OpenSSF Best Practices OpenSSF Scorecard

CI codecov ruff ty mypy pyright pyrefly

Linux macOS Windows python Polars

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

  • Runtimepolars only (>= 1.40). Nothing else is pulled into your environment.
  • Python — 3.12, 3.13, 3.14.
  • Optional — the differential group (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 flowcost_borrow, cost_fixed, cost_funding, cost_notional, cost_per_share, cumulative_pnl, dividend, pnl_gross, pnl_gross_inverse, pnl_net
  • returns flowcost_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
  • drawdownconditional_drawdown_at_risk, drawdown, drawdown_rolling, max_drawdown, max_drawdown_duration, pain_index, ulcer_index
  • performancecagr, cagr_rolling, stability, total_return, total_return_rolling
  • ratioadjusted_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
  • relativealpha, 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
  • riskconditional_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.
  • CitationCITATION.cff.

Release files for pomata 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pomata 0.3.0
File Size Uploaded
pomata-0.3.0.tar.gz 558.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pomata 0.3.0
File Interpreter ABI Platform
pomata-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 728.5 kB

Release files / pomata-0.3.0.tar.gz

Download URL pomata-0.3.0.tar.gz
Size 558.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0f0f560edd770f5db1c627dc57b5ed94254f6f708ee8a629529d712bb0178339
BLAKE2b-256 checksum
How to use checksums
32754c59668abb0ac5191717468b049007d902bee53e45cbeb939edfd4ebfa00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release files / pomata-0.3.0-py3-none-any.whl

Download URL pomata-0.3.0-py3-none-any.whl
Size 169.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2195ab2015641608240f1ed8aad698350fac66d2e1dd0dacbca62b17a8ea73a6
BLAKE2b-256 checksum
How to use checksums
829a3c4379f87221d9f5f3d00b50c3fe141189af1f0316bcdaac0188c8c1f42d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page