BagelQuant

Build quantitative research systems from panels and reusable graph logic.

BagelQuant is a composable framework for quantitative research, portfolio construction, and backtesting.

Core Model

Raw data enters the system as a Panel. Derived objects such as factors, predictions, and portfolio weights are graphs until execution materializes their output panels.

Quick Start

import pandas as pd

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

# Define the domain of our research.
# The `Panel.from_domain` method aligns raw data to the domain's sessions and assets.
domain = Domain(
    calendar=pd.bdate_range("2024-01-01", "2024-12-31"),
    universe=["AAPL", "MSFT"],
)

# Create panels for input data. Replace `pd.DataFrame(...)` with actual data loading logic.
price = Panel.from_domain(pd.DataFrame(...), domain, name="price")
book = Panel.from_domain(pd.DataFrame(...), domain, name="book")
quality = Panel.from_domain(pd.DataFrame(...), domain, name="quality")

# Define a graph for the research logic. Graphs are lazy and can be defined in any order.
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")

# A simple prediction graph that combines the factors. The `weights` argument is passed to the `weighted_sum` composer.
prediction = weighted_sum(
    bm_factor,
    quality_factor,
    weights=[0.5, 0.5],
    name="prediction",
)

# Define a graph for the trading signal. The `rank` transformer is applied to the prediction graph.
signal = rank(prediction, name="signal")
smoothed_signal = rolling_mean(signal, window=20, name="smoothed_signal")

# Execute the graph to materialize the output panel.
# Execution is lazy and only happens when `compute` is called.
# It will compute all upstream graphs as needed, caching intermediate results for efficiency.
smoothed_signal.compute()
result = smoothed_signal.output  # Access the output panel after computation.

result is a Panel. Its underlying frame is available as result.data. Domain never retrieves calendars; callers must provide a sorted calendar.

Custom Operations

Users can define function-style operations in their own modules:

import pandas as pd

from bagelquant_core.composer import composer
from bagelquant_core.transformer import transformer


@transformer
def demean(frame: pd.DataFrame) -> pd.DataFrame:
    return frame.sub(frame.mean(axis=1), axis=0)


@composer
def average(*frames: pd.DataFrame) -> pd.DataFrame:
    return sum(frames) / len(frames)


centered_price = demean(price, name="centered_price")
combined = average(bm_factor, quality_factor, name="combined")

Cached Outputs

Computing a downstream graph materializes outputs for its intermediate graphs:

signal.compute()

prediction_panel = prediction.output
signal_panel = signal.output

Run the complete example:

uv run python example.py

Development

Package source code lives in src/bagelquant_core. The test suite is configured to import from src, so local validation can run from the repository root:

.\.venv\Scripts\python.exe -m pytest

uv run pytest is also expected to work in environments where uv can resolve the project Python executable correctly.

Documentation

License

Apache License 2.0