Skip to main content

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.

CI coverage ruff ty mypy pyright pyrefly

Linux macOS Windows python Polars license

Alpha. The API is not frozen until 1.0; expect refinement. The correctness bar holds at every commit regardless.

From price to performance, in one query

Signal, PnL, and metrics are all plain pl.Expr, so an entire study is a single Polars pipeline — no glue code, no DataFrame ping-pong, no second dependency between the steps:

import polars as pl
from pomata.indicators import rsi
from pomata.pnl import returns_simple, returns_gross, returns_net, cost_proportional, equity_curve
from pomata.metrics import sharpe_ratio, max_drawdown

report = (
    frame  # a DataFrame (or LazyFrame) with a "close" column
    .with_columns(
        weight=(rsi(pl.col("close"), 14) < 30).cast(pl.Float64).shift(1),  # go long when oversold, act next bar
        asset_returns=returns_simple(pl.col("close")),
    )
    .with_columns(
        net=returns_net(
            returns_gross(pl.col("weight"), pl.col("asset_returns")),
            cost_proportional(pl.col("weight"), rate=0.001),
        ),
    )
    .select(
        sharpe=sharpe_ratio(pl.col("net"), periods_per_year=252),
        max_drawdown=max_drawdown(equity_curve(pl.col("net"))),
    )
)

The indicator feeds the signal, the signal feeds the PnL, the PnL feeds the metrics — every arrow is a pl.Expr, so it all fuses into one Polars query (eager or lazy, single series or a multi-asset panel via .over). The .shift(1) is the whole no-look-ahead story: a signal computed at the close acts on the next bar, by construction.

Install

The only runtime dependency is Polars; Python 3.12+.

# from source (today)
git clone https://github.com/ilpomo/pomata
cd pomata && uv sync

Once published to PyPI, the install will be pip install pomata (or uv add pomata).

Every function is a free-standing pl.Expr factory — name it, compose it, run it in any Polars context. Warm-up rows are null until the window fills, never a fabricated value:

import polars as pl
from pomata.indicators import rsi

frame = pl.DataFrame({"close": [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03,
                                45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45]})
frame.select(rsi(pl.col("close"), 14).round(2).alias("rsi"))["rsi"].to_list()
# [None, None, ..., 57.92, 62.88, 63.21, 56.01, 62.34]

What's inside

Three families, one package. They share a grammar (pure pl.Expr factories, one canonical name per concept) and a handoff: pnl emits exactly the return and equity series metrics consumes.

indicators — 75 functions

The technical-analysis layer, each indicator a pl.Expr checked against TA-Lib to the float64 floor — most bar-for-bar from the first emitted value, a documented minority only on the converged tail (the differential tier is non-gating). Multi-output indicators (bollinger_bands, macd, stochastic_slow, …) return a single pl.Struct — pick a line with .struct.field(...) or expand with .struct.unnest().

from pomata.indicators import bollinger_bands
frame.select(bollinger_bands(pl.col("close"), 20).alias("bb")).unnest("bb")
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 — 18 functions

Profit-and-loss accounting in two flows: returns (a signed weight of capital and asset returns) and cash (a quantity of units and a price), with composable transaction-cost models, dividends, and inverse contracts. Every degenerate input (null / NaN / 0 / ±inf / warm-up) has a defined, documented, tested behavior.

from pomata.pnl import returns_net, returns_gross, cost_proportional, equity_curve
gross = returns_gross(pl.col("weight"), pl.col("asset_returns"))
frame.select(equity_curve(returns_net(gross, cost_proportional(pl.col("weight"), rate=0.001))))
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

metrics — 60 functions

Performance & risk statistics as reducing pl.Expr: point one at a return series (e.g. pomata.pnl.returns_net) or an equity curve (e.g. pomata.pnl.equity_curve). Sharpe, Sortino, Calmar, drawdown, VaR/CVaR, capture, benchmark-relative (alpha/beta/Treynor/information ratio), and a rolling twin for every windowed form.

from pomata.metrics import sharpe_ratio, max_drawdown
frame.select(sharpe_ratio(pl.col("returns"), periods_per_year=252))
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

Correctness

Verified, not asserted. Every function is checked against an independent reference — a second code path that shares nothing with the implementation — plus frozen golden-master values and property-based invariants, under 100% branch coverage. A function ships only when that suite is green.

For indicators there is also a public reference to meet: TA-Lib. Here is one figure to every digit a float64 holds — rsi(14), the last value of a deterministic 400-bar series:

pomata      85.20908701341023
reference   85.20908701341023   ← independent reimplementation: identical, to the last bit
TA-Lib      85.20908701341024   ← fifteen figures identical; differs only at the float64 floor

The same five indicators on the same series — most reproduce the reference exactly, the rest land at the noise floor:

indicator pomata vs reimplementation vs TA-Lib
sma(20) 105.15146076264764 exact 1e-13
ema(20) 107.7299930892346 1e-13 1e-14
rsi(14) 85.20908701341023 exact 1e-14
atr(14) 1.904174462198776 9e-16 4e-15
macd(12,26,9) 2.523444380829531 1e-13 1e-14

The pomata and reference columns are pinned in the test suite; regenerate the full table — including the TA-Lib column (which needs the optional differential dependency) — from a fresh clone with uv run --group differential python scripts/precision_table.py.

pnl and metrics are proven on a different axis — every degenerate input has a defined behavior, matched against an independent reference oracle — because their math is simple and their correctness lives at the edges, not in the digits. The full method (the precision guarantee, the test-sizing derivations, exactly what is and is not claimed) is 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

  • Requirements — Python ≥ 3.12, Polars ≥ 1.40.
  • 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.1.1.tar.gz (547.1 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.1.1-py3-none-any.whl (162.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pomata-0.1.1.tar.gz
  • Upload date:
  • Size: 547.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pomata-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6354632c4a084102f4d6b3df13d36cb285359c2780868d243b4a9c32304c2eef
MD5 69247cbd3dc5146d115de59f455fd54d
BLAKE2b-256 e3c4f2f5c23ccdbf98340736ed72c61c1304047d240e62a658e560a565fbd3bf

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pomata-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 162.4 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.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 708aa8f5227bdb243e229283594a446d7be88d9a111b578e86c515392ccdf860
MD5 daca7a080a17394bb5e1a4cfd74dfe45
BLAKE2b-256 dbbe0f624592d45d4c97a753231083c03e79b517abfd2662034e276e0a8789ba

See more details on using hashes here.

Provenance

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