Skip to main content

polars_bt

polars_bt is a Rust-backed Polars expression plugin with two 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

Both 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
  • Polars 1.43.x
pip install polars_bt

For a local release build:

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python 'polars>=1.43,<1.44' maturin pytest
.venv/bin/maturin develop --release

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;
  • preprocessing materializes a complete panel before the call;
  • 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.

Development

make install-release
make pre-commit
.venv/bin/python benchmarks/benchmark_mosaic.py

The accepted benchmark is 2,500 days by 5,000 assets (12.5 million rows), with a five-second hard limit measured only around the Mosaic expression call.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polars_bt-0.2.1.tar.gz (85.3 kB view details)

Uploaded Source

Built Distribution

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

polars_bt-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.7 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

File details

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

File metadata

  • Download URL: polars_bt-0.2.1.tar.gz
  • Upload date:
  • Size: 85.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for polars_bt-0.2.1.tar.gz
Algorithm Hash digest
SHA256 d08f4fbae35e0d1b9c26d93ec887e03e690c6a3ffc86e3a7060ae67255b977d6
MD5 4225114f3e648df6f1d8a88f2b9c8456
BLAKE2b-256 e6291b8c6bbfff5b2fc0984341140a9084567a06f96a9e166f06b88f4bd85a51

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_bt-0.2.1.tar.gz:

Publisher: release.yml on huangbogeng/polars_bt_extension

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

File details

Details for the file polars_bt-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for polars_bt-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b7af3940e876c2cb229790a24460da301af713fb2577e76dccba10552b51d12b
MD5 b89e0ddf41f41ccea8ef6fb076633f15
BLAKE2b-256 134d44226817a083838e9db9886c689d794f6a62dcb625d51eac14d4284e5f9f

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_bt-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on huangbogeng/polars_bt_extension

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

Release history Release notifications | RSS feed

0.2.2

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.2

2 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