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, shipped with these docs. Load it once — 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 — checked against TA-Lib to the float64 floor. 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 the public TA-Lib reference; 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.1.tar.gz (553.2 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.1-py3-none-any.whl (164.7 kB view details)

Uploaded Python 3

File details

Details for the file pomata-0.2.1.tar.gz.

File metadata

  • Download URL: pomata-0.2.1.tar.gz
  • Upload date:
  • Size: 553.2 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.1.tar.gz
Algorithm Hash digest
SHA256 477aca3c15339fcbb1cd0290dea3606b35a18b0206e67ebda4a127d5cad2982b
MD5 736da95466c944e1d18421087f6acbe6
BLAKE2b-256 7a2855aa4c1adc363fa49953fdc0fa60882b1a6a1513b3cd354c2056ff87aa5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pomata-0.2.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: pomata-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 164.7 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b25eab4799b338f1023ffe0ffbbcbd93888cd95b4af6b3c34ec638128bc1860e
MD5 a4f0b4ef9ab4b9d2356a88dca5b4c7c3
BLAKE2b-256 f7858e5823b9960941157b436c73edde7215972e93263e9d9443243ee05f59a7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pomata-0.2.1-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