bagelquant-core is the Polars-native panel and graph layer for BagelQuant.

Panel data is long-form and keyed by time and asset_id. Numeric panels use a single value column:

import polars as pl

from bagelquant_core import Domain, Panel
from bagelquant_core.composer import div
from bagelquant_core.transformer import rank, zscore

static_domain = Domain(
    calendar=["2024-01-02", "2024-01-03"],
    universe=["AAA", "BBB"],
)
dynamic_domain = Domain(
    calendar=["2024-01-02", "2024-01-03"],
    universe=pl.DataFrame(
        {
            "time": ["2024-01-02", "2024-01-03"],
            "asset_id": ["AAA", "BBB"],
            "active": [True, True],
        }
    ),
)
domain = static_domain
price = Panel.from_domain(
    pl.DataFrame(
        {
            "time": ["2024-01-02", "2024-01-02"],
            "asset_id": ["AAA", "BBB"],
            "value": [100.0, 50.0],
        }
    ),
    domain,
    name="price",
)
book = Panel.from_domain(
    pl.DataFrame(
        {
            "time": ["2024-01-02", "2024-01-02"],
            "asset_id": ["AAA", "BBB"],
            "value": [40.0, 25.0],
        }
    ),
    domain,
    name="book",
)

factor = rank(zscore(div(book, price)), name="factor")
factor.compute()

print(factor.output.data)

Time-series operations group by asset_id and order by time. Cross-sectional operations group by time. Composer operations join inputs on (time, asset_id). Universes can be static lists/Series or sparse dynamic membership frames with time, asset_id, and boolean active; missing dynamic rows are inactive and are not forward-filled.

Development

uv run ruff check .
uv run python -m pytest