Skip to content

Bars

A bar summarises a run of consecutive trades: open, high, low, close, volume, VWAP, and the buy/sell split of its volume. What differs between bar types is only where the boundaries fall, so that decision is a rule and everything else is shared.

Five rules ship with the package:

Rule A new bar every… Threshold
time fixed span of the clock a duration ("1min", pd.Timedelta)
tick N trades a trade count
volume N units of traded size an amount
dollar N units of price × size an amount
imbalance N units of drift in signed size an amount

The last four sample the market by activity rather than by the clock. That is why the quant literature prefers them: a quiet hour and a busy minute produce the same number of bars, so bar returns come much closer to being independent and identically distributed.

See the "Build bars from trades" how-to for a worked example, and Extending for writing a rule of your own.

Functions

bars

bars(
    trades: DataFrame,
    rule: str = "time",
    threshold: Any = None,
    *,
    target_bars: int = 50,
    sign_method: str | None = None,
    quotes: DataFrame | None = None,
) -> pd.DataFrame

Resample trades into bars.

Parameters:

Name Type Description Default
trades DataFrame

Trades with at least timestamp, price and volume — a pipeline result's trades frame, or any frame shaped like it. A direction column ("buy" / "sell", the taker's side) is used when present; otherwise the aggressor side is classified — see sign_method.

required
rule str

Registered rule name: "time" (the default), "tick", "volume", "dollar" or "imbalance". See :func:list_bar_rules.

'time'
threshold optional

How much of the rule's own quantity closes a bar: a duration for "time", a trade count for "tick", an amount for the rest. None (the default) asks the rule for a threshold that yields about target_bars bars.

None
target_bars int

How many bars the default threshold aims at. Ignored when threshold is given.

50
sign_method str or None

How to classify the aggressor side: None (the default) keeps a direction column if the frame has one and otherwise classifies, "tick" or "lee_ready" always classify. See :func:~ob_analytics.trade_sign.resolve_direction.

None
quotes DataFrame

Book snapshots for the Lee–Ready classifier — a frame with timestamp and either a mid or a bid/ask pair, such as a pipeline depth_summary. Only read when the aggressor side has to be classified.

None

Returns:

Type Description
DataFrame

One row per bar, in time order, with the columns in :data:BAR_COLUMNS:

bar 0-based bar number. timestamp_start / timestamp_end First and last trade of the bar. open / high / low / close Trade prices, in the units of the input frame. volume Total size traded. turnover Total price × size. n_trades Number of trades. vwap turnover / volume — the volume-weighted average price. buy_volume / sell_volume / signed_volume Size split by aggressor side, and buys minus sells.

Every bar holds at least one trade: a clock span in which nothing traded produces no row. The last bar is whatever trades were left over, so it may not have reached the threshold; drop it with .iloc[:-1] where an equal-size bar matters.

The frame's attrs carry bar_rule and bar_threshold, so a caller that let the threshold default can still report what cut the bars.

Raises:

Type Description
ConfigError

If required columns are missing, or the threshold does not suit the rule.

KeyError

If rule is not registered; the message lists the registered names.

ObAnalyticsError

If trades is empty.

Notes

Prices and sizes pass through in the units they arrive in. A pipeline result holds prices as whole ticks and sizes as whole lots, so a "dollar" threshold there is in ticks × lots, not in the quote currency. Convert the frame first (see :func:~ob_analytics.visualization.display_result) to work in quote- currency amounts.

Examples:

>>> from ob_analytics import Pipeline, bars, sample_csv_path
>>> result = Pipeline().run(sample_csv_path())
>>> bars(result.trades, "volume", 100)[
...     ["timestamp_end", "open", "close", "vwap", "signed_volume"]
... ]

register_bar_rule

register_bar_rule(rule: BarRule) -> None

Register rule under its own :attr:~ob_analytics.protocols.BarRule.name.

Case-insensitive; overwriting an existing registration is allowed, so a rule of your own may deliberately shadow a built-in one.

list_bar_rules

list_bar_rules() -> list[str]

Return a sorted list of registered bar-rule names.

get_bar_rule

get_bar_rule(name: str) -> BarRule

Return the bar rule registered under name (case-insensitive).

Raises:

Type Description
KeyError

If no rule is registered under name; the message lists the registered names.

The built-in rules

ClockRule

A new bar every fixed span of the clock — classic OHLCV.

The threshold is a fixed duration: a string pandas reads as one ("5s", "1min", "1h") or a :class:pandas.Timedelta. A calendar step with no fixed length (a month, a business day) is not a duration and is rejected.

Bars are aligned to the epoch, so "1min" cuts on the minute whatever the first trade's timestamp. A span in which nothing traded produces no bar: every row :func:bars returns holds at least one trade.

default_threshold

default_threshold(
    frame: DataFrame, target_bars: int
) -> pd.Timedelta

Return the capture's span divided by target_bars (at least 1 ms).

normalize

normalize(threshold: Any) -> pd.Timedelta

Read threshold as a fixed duration.

assign

assign(
    frame: DataFrame, threshold: Timedelta
) -> np.ndarray

Return the bar index of each trade: its timestamp floored to threshold.

TickRule

A new bar every N trades, whatever their size.

The threshold is that count. "Tick" here is the market-data sense of one printed trade, not the price increment.

default_threshold

default_threshold(
    frame: DataFrame, target_bars: int
) -> int

Return the trade count divided by target_bars (at least 1).

normalize

normalize(threshold: Any) -> int

Read threshold as a trade count.

assign

assign(frame: DataFrame, threshold: int) -> np.ndarray

Return the bar index of each trade: its position divided by threshold.

AccumulationRule dataclass

AccumulationRule(
    name: str,
    quantity: Callable[[DataFrame], ndarray],
    signed: bool = False,
)

A new bar every N units of some quantity the trades carry.

One shape covers the three activity rules; they differ only in what they accumulate and in whether that quantity is signed:

  • "volume" — traded size.
  • "dollar" — price × size, in the units of the input frame.
  • "imbalance" — signed size, + for buyer-initiated trades and - for seller-initiated ones.

An unsigned rule closes a bar when the running total reaches the threshold. A signed rule closes one when the running total reaches the threshold in either direction, so a bar ends on a burst of one-sided flow and a balanced stretch of trading stays inside one bar.

The threshold is fixed, not the moving estimate of López de Prado's original imbalance bars: a fixed threshold gives the same bars every time the same trades are read, which is what makes a bar table reproducible.

Attributes:

Name Type Description
name str

Registered rule name.

quantity Callable

Maps the normalized trade frame to the per-trade amount accumulated.

signed bool

Whether that amount carries the aggressor's sign.

default_threshold

default_threshold(
    frame: DataFrame, target_bars: int
) -> float

Return the threshold that cuts frame into about target_bars bars.

An unsigned quantity simply splits its total target_bars ways. A signed one part-cancels, so its total says nothing about how far it drifts: the running sum wanders like a random walk, whose distance after a share of the trades is the root of that share's squared amounts.

normalize

normalize(threshold: Any) -> float

Read threshold as a positive amount.

assign

assign(frame: DataFrame, threshold: float) -> np.ndarray

Return the bar index of each trade by accumulating up to threshold.