bagelquant-core is the panel and graph foundation for BagelQuant research. Use it when raw research inputs are already available as long-form Polars data and you want reproducible factor logic.

Install

uv add bagelquant-core

For local development from this repository:

uv run python example.py

Build A Domain

A Domain defines the trading sessions and asset universe used by every input panel. The package does not download calendars or security masters; callers provide them from a data layer.

import polars as pl

from bagelquant_core import Domain

domain = Domain(
    calendar=pl.date_range(
        pl.date(2024, 1, 1),
        pl.date(2024, 12, 31),
        interval="1d",
        eager=True,
    ),
    universe=["AAPL", "MSFT"],
)

The universe may be static, as above, or dynamic over time. Dynamic universes use a sparse long-form Polars frame with time, asset_id, and boolean active columns. Missing (time, asset_id) rows are inactive, and membership is not carried forward.

dynamic_domain = Domain(
    calendar=["2024-01-02", "2024-01-03"],
    universe=pl.DataFrame(
        {
            "time": ["2024-01-02", "2024-01-03"],
            "asset_id": ["AAPL", "MSFT"],
            "active": [True, True],
        }
    ),
)

Create Panels

Panel.from_domain aligns raw frames to the domain. Public panel data is long-form with time, asset_id, and value columns.

from bagelquant_core import Panel

price = Panel.from_domain(
    pl.DataFrame(
        {
            "time": ["2024-01-02", "2024-01-02", "2024-01-03", "2024-01-03"],
            "asset_id": ["AAPL", "MSFT", "AAPL", "MSFT"],
            "value": [185.0, 370.0, 187.0, 372.0],
        }
    ),
    domain,
    name="price",
)
book = Panel.from_domain(book_df, domain, name="book")
quality = Panel.from_domain(quality_df, domain, name="quality")

Compose A Factor Graph

Transformers are unary operations. Composers combine one or more inputs. Both return lazy Graph objects.

from bagelquant_core.composer import div, weighted_sum
from bagelquant_core.transformer import rank, rolling_mean, winsorize, zscore

bm_ratio = div(book, price, name="bm_ratio")
bm_factor = rank(zscore(winsorize(bm_ratio)), name="bm_factor")
quality_factor = rank(zscore(quality), name="quality_factor")

prediction = weighted_sum(
    bm_factor,
    quality_factor,
    weights=[0.5, 0.5],
    name="prediction",
)

signal = rolling_mean(rank(prediction), window=20, name="signal")

Execute

Call compute() on the downstream graph. The execution runtime evaluates upstream dependencies once and caches intermediate panels for the current run.

signal.compute()
result = signal.output
frame = result.data

Use frame as input to downstream portfolio construction or backtesting.