Skip to main content

Verifiably-correct, Polars-native quant toolkit: technical indicators, performance & risk metrics, and PnL accounting.

Project description

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.

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

pomata-0.2.2.tar.gz (555.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pomata-0.2.2-py3-none-any.whl (166.6 kB view details)

Uploaded Python 3

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

Hashes for pomata-0.2.2.tar.gz
Algorithm Hash digest
SHA256 772a141f7142b0b389ae41bae5a51b623277b032ba14dda991a8aaccf56d2ea6
MD5 f47cdcf68cdbbc9d13b517afef0b138d
BLAKE2b-256 d46f77b228cb319e3a4b2fc9ef4204830d3de31eb3a199ddf9405bffd5514eb7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pomata-0.2.2.tar.gz:

Publisher: release.yml on ilpomo/pomata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pomata-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9f90c7f891cd8f962622b45745109ce5a1787c23bdc8042196ea9eaaf9299c7b
MD5 7b4f61cd115802e15945019e20005a16
BLAKE2b-256 3e5d90b8a2b273099d7a8484e47b6476dba7b5ae094b55617e219aae321b1ab4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pomata-0.2.2-py3-none-any.whl:

Publisher: release.yml on ilpomo/pomata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page