Skip to main content

polars_bt

polars_bt is a Rust-backed Polars expression plugin with three deliberately separate backtesting engines.

Engine Model State axis Output
pulse T0 quote/signal matching time rows scalar summary
mosaic cross-sectional portfolio dense daily panels daily portfolio rows
tempo multi-time cross-sectional portfolio dense datetime panels timestamp portfolio rows

All three engines execute inside the Polars process. They do not serialize a DataFrame through Arrow IPC to call Rust.

Requirements and installation

  • CPython 3.10, 3.11, or 3.12
  • Python 3.10: Polars >=2.0.0,<2.1
  • Python 3.11–3.12: Polars >=2.0.0,<2.2 (target release: 2.1.0)
  • Prebuilt wheels: Linux x86_64; other platforms require a source build and are not covered by the release test matrix
pip install --upgrade polars_bt
polars_bt version Python Polars
0.2.4 Python 3.10: >=2.0.0,<2.1; Python 3.11–3.12: >=2.0.0,<2.2
0.2.3 >=2.0.0,<2.1
0.2.2 >=1.44.2,<1.45
0.2.1 >=1.43,<1.44

Version 0.2.4 adds Polars 2.1 support without changing the backtest API. Polars 2.1 requires Python >=3.11; dependency markers keep Python 3.10 on Polars 2.0.x. Upgrade polars_bt and Polars together; environments retaining Polars 1.44 should retain polars_bt==0.2.2.

For a local build, install the Rust toolchain specified in rust-toolchain.toml (1.95), then run from the repository root:

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -r requirements.txt
make install-release

The native plugin is built with Rust Polars 0.55.2, pyo3-polars 0.28, and PyO3 0.29. Upgrading the Python Polars major or minor version requires rebuilding and testing the plugin with the corresponding Rust dependencies. CPython's abi3 wheel tag does not guarantee compatibility with a different Polars version. See the upgrade analysis for the 2.0/2.1 verification results and historical 1.44 migration, and ARCHITECTURE.md for current engine contracts.

Polars 2.0 defaults lazy collection to streaming. Prepare and explicitly sort your complete panel before calling a backtest through eager DataFrame.select. For example, use panel.lazy().sort(["datetime", "asset"]).collect() before Tempo; use your asset key with date for Mosaic. Do not rely on join or group-by output order. Direct lazy/streaming backtest execution is outside the supported contract. See the official 2.0 migration guide.

Pulse: T0 quote matching

pulse retains the original quote-by-quote T0 matcher and returns one Struct summary.

import polars as pl
from polars_bt import pulse

quotes = pl.DataFrame(
    {
        "ask": [100.0, 101.0, 102.0],
        "bid": [99.5, 100.5, 101.5],
        "long": [1, 0, 0],
        "short": [0, 1, 0],
        "close_long": [0, 0, 0],
        "close_short": [0, 0, 0],
        "time": [1000, 2000, 3000],
        "limit_down": [90.0] * 3,
        "limit_up": [110.0] * 3,
    }
)

summary = quotes.select(
    pulse(
        "ask",
        "bid",
        "long",
        "short",
        "close_long",
        "close_short",
        "time",
        "limit_down",
        "limit_up",
    ).alias("pulse")
)

Set LOFIEX_MATCHER=easy to use the relaxed matcher; the default matcher keeps the original limit-price checks.

Mosaic: cross-sectional portfolios

mosaic scans a dense, date-major panel in fixed asset_num row blocks. It returns one daily Struct row containing date, cash, nav, turnover, and holding_count.

import polars as pl
from polars_bt import mosaic

panel = pl.DataFrame(
    {
        "date": ["2024-01-02", "2024-01-02", "2024-01-03", "2024-01-03"],
        "weight": [0.4, 0.4, 0.0, 0.5],
        "ovn_ret": [0.0, 0.0, 0.01, -0.01],
        "ind_ret": [0.0, 0.0, 0.0, 0.0],
        "buyable": [True] * 4,
        "sellable": [True] * 4,
        "prev_close": [10.0] * 4,
        "vwap": [10.0] * 4,
        "is_rebalance": [True] * 4,
    }
)

daily = panel.select(
    mosaic(
        date="date",
        weight="weight",
        ovn_ret="ovn_ret",
        ind_ret="ind_ret",
        buyable="buyable",
        sellable="sellable",
        prev_close="prev_close",
        vwap="vwap",
        is_rebalance="is_rebalance",
        asset_num=2,
    ).alias("daily")
).unnest("daily")

Mosaic's input contract is intentionally narrow:

  • rows are already sorted by (date, asset) and every date has exactly asset_num rows;
  • the asset row order is stable across dates, so the engine uses row offsets and performs no joins or asset hashing;
  • callers materialize a complete panel before the call; the wrapper does not sort or fill missing assets;
  • numeric nulls in weight, ovn_ret, ind_ret, prev_close, and vwap are preserved as NaN semantics rather than silently filled with zero;
  • use it as an eager whole-table expression; it changes the output length;
  • fees default to st_fee=6e-4 and lg_fee=1e-4.

Mosaic diagnostics

Enable Polars verbose mode to see bounded Rust-side diagnostics on stderr:

with pl.Config(verbose=True):
    daily = panel.select(
        mosaic(
            date="date",
            weight="weight",
            ovn_ret="ovn_ret",
            ind_ret="ind_ret",
            buyable="buyable",
            sellable="sellable",
            prev_close="prev_close",
            vwap="vwap",
            is_rebalance="is_rebalance",
            asset_num=2,
        ).alias("daily")
    ).unnest("daily")

pl.Config.set_verbose(True) and the process-level POLARS_VERBOSE=1 switch enable the same plugin diagnostics. Records use a stable prefix and compact key/value format:

[polars-bt][mosaic][INFO] event=start rows=12500000 days=2500 assets=5000
[polars-bt][mosaic][WARN] event=input_summary nan_weight=32 mixed_date_blocks=1
[polars-bt][mosaic][WARN] event=halt reason=NEGATIVE_CASH day_index=1902 cash=-0.0021
[polars-bt][mosaic][INFO] event=finish completed_days=1903 expected_days=2500

Verbose diagnostics add no result fields and do not change tolerated-input semantics. Non-finite portfolio state is always a hard error with day, asset, and calculation-stage context. Diagnostic reports retain only counts and the first location for each category, so memory use does not grow with the number of anomalies. Nullable returns remain visible as NAN_OVN_RET or NAN_IND_RET, and nullable prices remain visible as INVALID_PREV_CLOSE or INVALID_VWAP.

Mosaic cannot detect cross-day asset-order changes because asset identifiers are intentionally absent from its row-offset protocol. Callers must continue to provide a stable asset order for every date.

Tempo: intraday and cross-day portfolios

tempo extends the dense row-offset model to arbitrary timestamps. Every datetime contains a complete target cross-section, while is_rebalance controls whether that timestamp only marks the existing portfolio or also trades toward the supplied weights.

from datetime import datetime

import polars as pl
from polars_bt import tempo

panel = pl.DataFrame(
    {
        "datetime": [
            datetime(2024, 1, 2, 9, 31),
            datetime(2024, 1, 2, 9, 31),
            datetime(2024, 1, 2, 14, 30),
            datetime(2024, 1, 2, 14, 30),
        ],
        "asset": ["A", "B", "A", "B"],
        "weight": [0.4, 0.4, 0.0, 0.8],
        "period_ret": [0.0, 0.0, 0.01, -0.01],
        "buyable": [True] * 4,
        "sellable": [True] * 4,
        "is_rebalance": [True] * 4,
    }
)

path = panel.select(
    tempo(
        "datetime",
        "asset",
        "weight",
        "period_ret",
        "buyable",
        "sellable",
        "is_rebalance",
        t1=True,
    ).alias("path")
).unnest("path")

Tempo's contract is:

  • rows are sorted by (datetime, asset) and every datetime contains the same assets in the same order;
  • the first timestamp defines the canonical asset vector; Rust validates every later timestamp before running the backtest and never sorts or joins;
  • period_ret is the return from the preceding timestamp to the current one, and is applied before the current rebalance;
  • is_rebalance=False still marks holdings and emits a snapshot but does not trade;
  • use it as an eager whole-table expression; it changes the output length;
  • numeric nulls become NaN; NaN weights mean zero target and NaN returns mean zero return, with bounded warnings available through Polars verbose mode;
  • t1=True freezes same-day purchases until the date derived from datetime changes, while t1=False allows same-day sales;
  • output contains one row per datetime with datetime, cash, nav, timestamp turnover, and holding_count.

Tempo supports long-only weights. buyable and sellable describe market constraints at the current timestamp; Rust separately tracks the partially sellable quantity required by T+1.

Development

make install-release
make fmt
make pre-commit
.venv/bin/python examples/basic_usage.py
.venv/bin/python benchmarks/benchmark_mosaic.py
.venv/bin/python benchmarks/benchmark_tempo.py

The accepted benchmark scale is 12.5 million rows. Mosaic uses 2,500 days by 5,000 assets; Tempo uses 500 days by five timestamps by 5,000 assets. Both have a five-second hard limit measured only around the expression call.

make fmt formats Rust and Python sources; make pre-commit checks formatting, Clippy, Rust tests, Python tests, and Ruff. CI repeats the quality checks and tests the installed wheel outside the source checkout on Python 3.10–3.12. See CONTRIBUTING.md for the release process and CHANGELOG.md for version history.

License

MIT. See LICENSE.

Metadata

Release files for polars-bt 0.2.4

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

Source distribution (sdist)

Source distribution for polars-bt 0.2.4
File Size Uploaded Provenance Actions
polars_bt-0.2.4.tar.gz 116.0 kB With provenance Details for polars_bt-0.2.4.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for polars-bt 0.2.4
File Interpreter ABI Platform Actions
polars_bt-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for polars_bt-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Total release size: 5.9 MB

Release files / polars_bt-0.2.4.tar.gz

Download URL polars_bt-0.2.4.tar.gz
Size 116.0 kB
Tags Source
SHA-256 checksum
How to use checksums
0737430aca2a2431f98ab2f943b7869551b43a4098eaf9798bc53b569bbd5a70
BLAKE2b-256 checksum
How to use checksums
e78ec653e60444b705d405a2d419253c5252242c8c69c1d21249c50d76fd60ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Oct 30, 2026.

Transparency log

Release files / polars_bt-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL polars_bt-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 5.7 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
bf3aa3cd28245adc8b635223141a6c96c9404fb9d7225417a42b447361c00067
BLAKE2b-256 checksum
How to use checksums
9f3d6fede887c8b5e12d2381cf12c4473b0fe42e518fc4c78e8baabe738a7a70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Oct 30, 2026.

Transparency log

Security for polars-bt 0.2.4

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for polars_bt-0.2.4.tar.gz

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 Oct 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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