Skip to content

Data types

The three first-class market-data containers. Each wraps a NumPy matrix and carries the symbol configuration used by the engines.

KlineData

KlineData dataclass

KlineData(symbol: str, data: ndarray, timeframe: 'int | str', tick_size: float = 0.01, tick_value: 'float | None' = None, contract_size: float = 1.0, digits: int | None = None, avg_spread: float = 0.0, volume_min: float = 0.01, volume_max: float = 100.0, volume_step: float = 0.01)

OHLC candle + turnover data for a symbol.

Attributes:

Name Type Description
symbol str

Symbol name

data ndarray

[N x 7] array with columns: ts, open, high, low, close, volume, turnover (turnover may be NaN if unavailable)

timeframe int | str

Candle duration. Accepts a timeframe string ('1m', '5m', '1h', '1d', etc.) or an integer number of milliseconds, parsed via parse_timeframe(). The standard recommended set is '1m', '15m', '1h', '4h', '1d', '1w', '1mo' ('min'/'wk' are accepted aliases for 'm'/'w'; 'mo' is a fixed 30-day month). The normalized duration in ms is exposed read-only as interval_ms.

tick_size float

Minimum price step

tick_value float

Monetary value per tick. Defaults to tick_size (so PnL per unit equals the price difference, like backtesting.py).

contract_size float

Contract size

digits int

Decimal places for price

avg_spread float

Average spread in ticks

Example

klines_btc = KlineData( symbol='BTCUSDT', data=btc_matrix, timeframe='5m', tick_size=0.01, ) klines_eth = KlineData( symbol='ETHUSDT', data=eth_matrix, timeframe=60_000, tick_size=0.01, ) bt = BacktestEngine.by_klines( MyStrategy(), data = (klines_btc, klines_eth), ) bt.run()

config property

config: SymbolConfig

Build SymbolConfig for passing to broker.add_symbol().

Returns:

Name Type Description
SymbolConfig SymbolConfig

Configuration object with this symbol's parameters

interval_ms property

interval_ms: int

Candle duration in milliseconds (normalized from timeframe).

resample

resample(timeframe) -> 'KlineData'

Resample candles to higher timeframe without mutating self.

Parameters:

Name Type Description Default
timeframe int or str

Target timeframe as a string ('1h', '4h', etc., standard set: '1m', '15m', '1h', '4h', '1d', '1w', '1mo') or an integer number of milliseconds. Must be a multiple of self.interval_ms; if not, adjusted to next multiple (with warning)

required

Returns:

Name Type Description
KlineData 'KlineData'

New KlineData at higher interval, config propagated

Example

klines_1h = klines_1m.resample('1h')

Source code in src/tradetropy/core/data_types.py
def resample(self, timeframe) -> 'KlineData':
    """
    Resample candles to higher timeframe without mutating self.

    Args:
        timeframe (int or str): Target timeframe as a string ('1h', '4h',
                                etc., standard set: '1m', '15m', '1h',
                                '4h', '1d', '1w', '1mo') or an integer
                                number of milliseconds. Must be a multiple
                                of self.interval_ms; if not, adjusted to
                                next multiple (with warning)

    Returns:
        KlineData: New KlineData at higher interval, config propagated

    Example:
        klines_1h = klines_1m.resample('1h')
    """
    from tradetropy.data._klines import resample_klines

    matrix, new_interval = resample_klines(
        self.data, self.interval_ms, timeframe
    )
    return KlineData(
        symbol=self.symbol,
        data=matrix,
        timeframe=new_interval,
        tick_size=self.tick_size,
        tick_value=self.tick_value,
        contract_size=self.contract_size,
        digits=self.digits,
        avg_spread=self.avg_spread,
        volume_min=self.volume_min,
        volume_max=self.volume_max,
        volume_step=self.volume_step,
    )

save

save(path, format=None, **kwargs)

Save klines to file.

Parameters:

Name Type Description Default
path

Destination file path.

required
format

Output format ('csv', 'parquet', 'hdf5', 'npz'). None (default) infers from the extension, falling back to 'npz'.

None
**kwargs

Extra arguments passed to save_klines() (ts_format, hdf5_key, compression, include_partial).

{}

Returns:

Type Description

pd.DataFrame: The saved kline data.

Example

klines.save('btc_1h.h5') klines.resample('1h').save('btc_1h.parquet', format='parquet')

Source code in src/tradetropy/core/data_types.py
def save(self, path, format=None, **kwargs):
    """
    Save klines to file.

    Args:
        path: Destination file path.
        format: Output format ('csv', 'parquet', 'hdf5', 'npz'). None
            (default) infers from the extension, falling back to 'npz'.
        **kwargs: Extra arguments passed to save_klines() (ts_format,
            hdf5_key, compression, include_partial).

    Returns:
        pd.DataFrame: The saved kline data.

    Example:
        klines.save('btc_1h.h5')
        klines.resample('1h').save('btc_1h.parquet', format='parquet')
    """
    from tradetropy.io.io import save_klines
    metadata = {
        "tradetropy_symbol": self.symbol,
        "tradetropy_interval_ms": self.interval_ms,
        "tradetropy_tick_size": self.tick_size,
        "tradetropy_tick_value": self.tick_value,
        "tradetropy_contract_size": self.contract_size,
        "tradetropy_digits": self.digits,
        "tradetropy_avg_spread": self.avg_spread,
        "tradetropy_volume_min": self.volume_min,
        "tradetropy_volume_max": self.volume_max,
        "tradetropy_volume_step": self.volume_step,
    }
    return save_klines(self.data, path, format=format, metadata=metadata, **kwargs)

head

head(n: int = 5) -> 'KlineData'

Return a new KlineData with the first n rows.

Source code in src/tradetropy/core/data_types.py
def head(self, n: int = 5) -> 'KlineData':
    """Return a new KlineData with the first n rows."""
    return _slice_data(self, self.data[:n])

tail

tail(n: int = 5) -> 'KlineData'

Return a new KlineData with the last n rows.

Source code in src/tradetropy/core/data_types.py
def tail(self, n: int = 5) -> 'KlineData':
    """Return a new KlineData with the last n rows."""
    start = max(0, len(self.data) - n)
    return _slice_data(self, self.data[start:])

filter

filter(mask: 'np.ndarray | Callable | None' = None, *, start: 'str | int | None' = None, end: 'str | int | None' = None, idx_start: 'int | None' = None, idx_end: 'int | None' = None) -> 'KlineData'

Filter klines without mutating the original.

Parameters:

Name Type Description Default
mask 'np.ndarray | Callable | None'

Boolean array [N] or callable lambda d: d[:, col] > val.

None
start 'str | int | None'

Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.

None
end 'str | int | None'

End date (same format), inclusive.

None
idx_start int | None

Positional index range start, inclusive.

None
idx_end int | None

Positional index range end, exclusive.

None

Returns:

Name Type Description
KlineData 'KlineData'

New KlineData with the same metadata.

Example

klines.filter(start='2024-01-01', end='2024-01-31') klines.filter(idx_start=1000, idx_end=2000) klines.filter(mask=klines.data[:, 5] > 100) klines.filter(lambda d: d[:, 4] > 50000)

Source code in src/tradetropy/core/data_types.py
def filter(
    self,
    mask: "np.ndarray | Callable | None" = None,
    *,
    start: "str | int | None" = None,
    end: "str | int | None" = None,
    idx_start: "int | None" = None,
    idx_end: "int | None" = None,
) -> 'KlineData':
    """
    Filter klines without mutating the original.

    Args:
        mask: Boolean array [N] or callable ``lambda d: d[:, col] > val``.
        start: Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.
        end: End date (same format), inclusive.
        idx_start (int | None): Positional index range start, inclusive.
        idx_end (int | None): Positional index range end, exclusive.

    Returns:
        KlineData: New KlineData with the same metadata.

    Example:
        klines.filter(start='2024-01-01', end='2024-01-31')
        klines.filter(idx_start=1000, idx_end=2000)
        klines.filter(mask=klines.data[:, 5] > 100)
        klines.filter(lambda d: d[:, 4] > 50000)
    """
    return _slice_data(
        self, _apply_filter(self.data, mask, start, end, idx_start, idx_end)
    )

to_df

to_df()

Build a pandas DataFrame of the candles (a fresh copy each call).

Returns:

Type Description

pd.DataFrame: Columns OHLCV_TURNOVER_COLS (ts, open, high, low, close, volume, turnover) plus a 'datetime' column from 'ts'.

Example

df = klines.to_df()

Source code in src/tradetropy/core/data_types.py
def to_df(self):
    """
    Build a pandas DataFrame of the candles (a fresh copy each call).

    Returns:
        pd.DataFrame: Columns OHLCV_TURNOVER_COLS (ts, open, high, low,
            close, volume, turnover) plus a 'datetime' column from 'ts'.

    Example:
        df = klines.to_df()
    """
    import pandas as pd
    from tradetropy.core.constants import OHLCV_TURNOVER_COLS
    df = pd.DataFrame(self.data, columns=list(OHLCV_TURNOVER_COLS))
    df["datetime"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
    return df

TickData

TickData dataclass

TickData(symbol: str, data: ndarray, tick_size: float = 0.01, tick_value: 'float | None' = None, contract_size: float = 1.0, digits: int | None = None, avg_spread: float = 0.0, volume_min: float = 0.01, volume_max: float = 100.0, volume_step: float = 0.01)

Tick data for a symbol.

Attributes:

Name Type Description
symbol str

Symbol name (e.g. 'BTCUSDT', 'MES')

data ndarray

[N x 7] array with columns: ts, bid, ask, volume, flags, volume_real, price

tick_size float

Minimum price step (e.g. 0.25 for MES, 0.01 for forex)

tick_value float

Monetary value per tick per contract (e.g. 1.25 for MES). Defaults to tick_size (so PnL per unit equals the price difference, like backtesting.py).

contract_size float

Contract size (1 for most)

digits int

Decimal places for price normalization

avg_spread float

Average spread in ticks

Example

ticks = TickData( symbol='BTCUSDT', data=tick_matrix, tick_size=0.01, ) bt = BacktestEngine.by_ticks(MyStrategy(), data=(ticks,)) bt.run()

config property

config: SymbolConfig

Build SymbolConfig for passing to broker.add_symbol().

Returns:

Name Type Description
SymbolConfig SymbolConfig

Configuration object with this symbol's parameters

to_klines

to_klines(interval_ms, *, include_partial: bool = False, price_source: str = 'price', volume_source: str = 'volume') -> 'KlineData'

Convert ticks to KlineData without mutating self.

Parameters:

Name Type Description Default
interval_ms int or str

Target duration in milliseconds or string format. Accepts ms (int) or str ('5m', '1h')

required
include_partial bool

Include the current incomplete candle (default False)

False
price_source str

Price column - 'price' (default), 'mid' for (bid+ask)/2, or 'trade' to use the price column but first drop quote-only ticks (volume == 0), keeping only real trades

'price'
volume_source str

Volume column - 'volume' (default) or 'volume_real'

'volume'

Returns:

Name Type Description
KlineData 'KlineData'

New KlineData with candles, symbol config propagated

Example

klines = ticks.to_klines('5m')

Source code in src/tradetropy/core/data_types.py
def to_klines(
    self,
    interval_ms,
    *,
    include_partial: bool = False,
    price_source: str = 'price',
    volume_source: str = 'volume',
) -> 'KlineData':
    """
    Convert ticks to KlineData without mutating self.

    Args:
        interval_ms (int or str): Target duration in milliseconds or string
                                  format. Accepts ms (int) or str ('5m', '1h')
        include_partial (bool): Include the current incomplete candle
                                (default False)
        price_source (str): Price column - 'price' (default), 'mid' for
                            (bid+ask)/2, or 'trade' to use the price column
                            but first drop quote-only ticks (volume == 0),
                            keeping only real trades
        volume_source (str): Volume column - 'volume' (default) or
                             'volume_real'

    Returns:
        KlineData: New KlineData with candles, symbol config propagated

    Example:
        klines = ticks.to_klines('5m')
    """
    # Import local: avoids core -> data cycle
    from tradetropy.core.constants import parse_timeframe
    from tradetropy.data._klines import ticks_to_klines

    resolved_ms = parse_timeframe(interval_ms)
    matrix = ticks_to_klines(
        self.data,
        resolved_ms,
        include_partial=include_partial,
        price_source=price_source,
        volume_source=volume_source,
    )
    return KlineData(
        symbol=self.symbol,
        data=matrix,
        timeframe=resolved_ms,
        tick_size=self.tick_size,
        tick_value=self.tick_value,
        contract_size=self.contract_size,
        digits=self.digits,
        avg_spread=self.avg_spread,
        volume_min=self.volume_min,
        volume_max=self.volume_max,
        volume_step=self.volume_step,
    )

save

save(path, format=None, **kwargs)

Save ticks to file.

Parameters:

Name Type Description Default
path

Destination file path.

required
format

Output format ('csv', 'parquet', 'hdf5', 'npz'). None (default) infers from the extension, falling back to 'npz'.

None
**kwargs

Extra arguments passed to save_ticks() (ts_format, hdf5_key, compression).

{}

Returns:

Type Description

pd.DataFrame: The saved tick data.

Example

ticks.save('btc_ticks.h5') ticks.save('btc_ticks.parquet', format='parquet')

Source code in src/tradetropy/core/data_types.py
def save(self, path, format=None, **kwargs):
    """
    Save ticks to file.

    Args:
        path: Destination file path.
        format: Output format ('csv', 'parquet', 'hdf5', 'npz'). None
            (default) infers from the extension, falling back to 'npz'.
        **kwargs: Extra arguments passed to save_ticks() (ts_format,
            hdf5_key, compression).

    Returns:
        pd.DataFrame: The saved tick data.

    Example:
        ticks.save('btc_ticks.h5')
        ticks.save('btc_ticks.parquet', format='parquet')
    """
    from tradetropy.io.io import save_ticks
    metadata = {
        "tradetropy_symbol": self.symbol,
        "tradetropy_tick_size": self.tick_size,
        "tradetropy_tick_value": self.tick_value,
        "tradetropy_contract_size": self.contract_size,
        "tradetropy_digits": self.digits,
        "tradetropy_avg_spread": self.avg_spread,
        "tradetropy_volume_min": self.volume_min,
        "tradetropy_volume_max": self.volume_max,
        "tradetropy_volume_step": self.volume_step,
    }
    return save_ticks(self.data, path, format=format, metadata=metadata, **kwargs)

head

head(n: int = 5) -> 'TickData'

Return a new TickData with the first n rows.

Source code in src/tradetropy/core/data_types.py
def head(self, n: int = 5) -> 'TickData':
    """Return a new TickData with the first n rows."""
    return _slice_data(self, self.data[:n])

tail

tail(n: int = 5) -> 'TickData'

Return a new TickData with the last n rows.

Source code in src/tradetropy/core/data_types.py
def tail(self, n: int = 5) -> 'TickData':
    """Return a new TickData with the last n rows."""
    start = max(0, len(self.data) - n)
    return _slice_data(self, self.data[start:])

filter

filter(mask: 'np.ndarray | Callable | None' = None, *, start: 'str | int | None' = None, end: 'str | int | None' = None, idx_start: 'int | None' = None, idx_end: 'int | None' = None) -> 'TickData'

Filter ticks without mutating the original.

Parameters:

Name Type Description Default
mask 'np.ndarray | Callable | None'

Boolean array [N] or callable lambda d: d[:, col] > val.

None
start 'str | int | None'

Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.

None
end 'str | int | None'

End date (same format), inclusive.

None
idx_start int | None

Positional index range start, inclusive.

None
idx_end int | None

Positional index range end, exclusive.

None

Returns:

Name Type Description
TickData 'TickData'

New TickData with the same metadata.

Example

ticks.filter(start='2024-01-01', end='2024-01-31') ticks.filter(start='2024-01-01 10:00:00') ticks.filter(idx_start=1000, idx_end=2000) ticks.filter(mask=ticks.data[:, 3] > 0) ticks.filter(lambda d: d[:, 1] > 50000)

Source code in src/tradetropy/core/data_types.py
def filter(
    self,
    mask: "np.ndarray | Callable | None" = None,
    *,
    start: "str | int | None" = None,
    end: "str | int | None" = None,
    idx_start: "int | None" = None,
    idx_end: "int | None" = None,
) -> 'TickData':
    """
    Filter ticks without mutating the original.

    Args:
        mask: Boolean array [N] or callable ``lambda d: d[:, col] > val``.
        start: Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.
        end: End date (same format), inclusive.
        idx_start (int | None): Positional index range start, inclusive.
        idx_end (int | None): Positional index range end, exclusive.

    Returns:
        TickData: New TickData with the same metadata.

    Example:
        ticks.filter(start='2024-01-01', end='2024-01-31')
        ticks.filter(start='2024-01-01 10:00:00')
        ticks.filter(idx_start=1000, idx_end=2000)
        ticks.filter(mask=ticks.data[:, 3] > 0)
        ticks.filter(lambda d: d[:, 1] > 50000)
    """
    return _slice_data(
        self, _apply_filter(self.data, mask, start, end, idx_start, idx_end)
    )

to_df

to_df()

Build a pandas DataFrame of the ticks (a fresh copy each call).

Returns:

Type Description

pd.DataFrame: Columns TICK_COLS (ts, bid, ask, volume, flags, volume_real, price) plus a 'datetime' column derived from 'ts'.

Example

df = ticks.to_df()

Source code in src/tradetropy/core/data_types.py
def to_df(self):
    """
    Build a pandas DataFrame of the ticks (a fresh copy each call).

    Returns:
        pd.DataFrame: Columns TICK_COLS (ts, bid, ask, volume, flags,
            volume_real, price) plus a 'datetime' column derived from 'ts'.

    Example:
        df = ticks.to_df()
    """
    import pandas as pd
    from tradetropy.core.constants import TICK_COLS
    df = pd.DataFrame(self.data, columns=list(TICK_COLS))
    df["datetime"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
    return df

BookData

BookData dataclass

BookData(symbol: str, data: ndarray, levels: int, tick_size: float = 0.01)

L2 order-book (depth) data for a symbol.

Stored as a flat [N x (2 + 4*levels)] matrix (mirroring TickData/KlineData's flat layout) so it persists and replays the same way. Each row is one book event in the layout described by :func:book_flat_columns.

Attributes:

Name Type Description
symbol str

Symbol name.

data ndarray

[N x (2 + 4*levels)] flat book rows.

levels int

Number of levels K retained per side.

tick_size float

Minimum price step.

Example

book = BookData('BTCUSDT', rows, levels=10, tick_size=0.01) book.bid_px[-1] # best-N bid prices of the last event

ts property

ts: ndarray

Event timestamps [N] (ms).

kind property

kind: ndarray

Event kind [N]: 0 = snapshot, 1 = delta.

bid_px property

bid_px: ndarray

Bid prices [N x K], level 0 = best.

bid_sz property

bid_sz: ndarray

Bid sizes [N x K], level 0 = best.

ask_px property

ask_px: ndarray

Ask prices [N x K], level 0 = best.

ask_sz property

ask_sz: ndarray

Ask sizes [N x K], level 0 = best.

best_bid property

best_bid: ndarray

Best (top-of-book) bid price per event [N].

Level 0 is the best level in the flat layout, so this is the first bid column. NaN where the bid side is empty. Mirrors OrderbookProxy.best_bid as a full series.

best_ask property

best_ask: ndarray

Best (top-of-book) ask price per event [N].

Level 0 is the best level in the flat layout, so this is the first ask column. NaN where the ask side is empty. Mirrors OrderbookProxy.best_ask as a full series.

mid property

mid: ndarray

Mid price per event [N]: (best_bid + best_ask) / 2.

NaN where either side is missing (NumPy NaN propagation). Mirrors OrderbookProxy.mid as a full series.

spread property

spread: ndarray

Spread per event [N]: best_ask - best_bid.

NaN where either side is missing (NumPy NaN propagation). Mirrors OrderbookProxy.spread as a full series.

save

save(path, format=None, **kwargs)

Save order-book rows to file.

Parameters:

Name Type Description Default
path

Destination file path.

required
format

Output format ('csv', 'parquet', 'hdf5', 'npz'). None (default) infers from the extension, falling back to 'npz'.

None
**kwargs

Extra arguments passed to save_book() (layout, ts_format, hdf5_key, compression, metadata).

{}

Returns:

Type Description

pd.DataFrame: The saved book data.

Example

book.save('btc_book.h5') book.save('btc_book.parquet', format='parquet') book.save('btc_bookdepth.csv', format='csv', layout='long')

Source code in src/tradetropy/core/data_types.py
def save(self, path, format=None, **kwargs):
    """
    Save order-book rows to file.

    Args:
        path: Destination file path.
        format: Output format ('csv', 'parquet', 'hdf5', 'npz'). None
            (default) infers from the extension, falling back to 'npz'.
        **kwargs: Extra arguments passed to save_book() (layout, ts_format,
            hdf5_key, compression, metadata).

    Returns:
        pd.DataFrame: The saved book data.

    Example:
        book.save('btc_book.h5')
        book.save('btc_book.parquet', format='parquet')
        book.save('btc_bookdepth.csv', format='csv', layout='long')
    """
    from tradetropy.io.io import save_book
    metadata = {
        "tradetropy_symbol": self.symbol,
        "tradetropy_levels": self.levels,
        "tradetropy_tick_size": self.tick_size,
    }
    return save_book(
        self, path, format=format, metadata=metadata, **kwargs
    )

to_df

to_df()

Build a pandas DataFrame of the book events (a fresh copy each call).

Returns:

Type Description

pd.DataFrame: Columns from book_flat_columns(self.levels) (ts, kind, bid_px_/bid_sz_/ask_px_/ask_sz_) plus a 'datetime' column derived from 'ts'.

Example

df = book.to_df()

Source code in src/tradetropy/core/data_types.py
def to_df(self):
    """
    Build a pandas DataFrame of the book events (a fresh copy each call).

    Returns:
        pd.DataFrame: Columns from book_flat_columns(self.levels) (ts, kind,
            bid_px_*/bid_sz_*/ask_px_*/ask_sz_*) plus a 'datetime' column
            derived from 'ts'.

    Example:
        df = book.to_df()
    """
    import pandas as pd
    df = pd.DataFrame(self.data, columns=book_flat_columns(self.levels))
    df["datetime"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
    return df

head

head(n: int = 5) -> 'BookData'

Return a new BookData with the first n events.

Source code in src/tradetropy/core/data_types.py
def head(self, n: int = 5) -> 'BookData':
    """Return a new BookData with the first n events."""
    return _slice_data(self, self.data[:n])

tail

tail(n: int = 5) -> 'BookData'

Return a new BookData with the last n events.

Source code in src/tradetropy/core/data_types.py
def tail(self, n: int = 5) -> 'BookData':
    """Return a new BookData with the last n events."""
    start = max(0, len(self.data) - n)
    return _slice_data(self, self.data[start:])

filter

filter(mask: 'np.ndarray | Callable | None' = None, *, start: 'str | int | None' = None, end: 'str | int | None' = None, idx_start: 'int | None' = None, idx_end: 'int | None' = None) -> 'BookData'

Filter book events without mutating the original.

Parameters:

Name Type Description Default
mask 'np.ndarray | Callable | None'

Boolean array [N] or callable lambda d: d[:, col] > val.

None
start 'str | int | None'

Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.

None
end 'str | int | None'

End date (same format), inclusive.

None
idx_start int | None

Positional index range start, inclusive.

None
idx_end int | None

Positional index range end, exclusive.

None

Returns:

Name Type Description
BookData 'BookData'

New BookData with the same metadata (levels, tick_size).

Example

book.filter(start='2024-01-01', end='2024-01-31') book.filter(idx_start=1000, idx_end=2000) book.filter(lambda d: d[:, 1] == 0) # snapshots only

Source code in src/tradetropy/core/data_types.py
def filter(
    self,
    mask: "np.ndarray | Callable | None" = None,
    *,
    start: "str | int | None" = None,
    end: "str | int | None" = None,
    idx_start: "int | None" = None,
    idx_end: "int | None" = None,
) -> 'BookData':
    """
    Filter book events without mutating the original.

    Args:
        mask: Boolean array [N] or callable ``lambda d: d[:, col] > val``.
        start: Start date ('2024-01-01', '2024-01-01 10:30') or ms timestamp.
        end: End date (same format), inclusive.
        idx_start (int | None): Positional index range start, inclusive.
        idx_end (int | None): Positional index range end, exclusive.

    Returns:
        BookData: New BookData with the same metadata (levels, tick_size).

    Example:
        book.filter(start='2024-01-01', end='2024-01-31')
        book.filter(idx_start=1000, idx_end=2000)
        book.filter(lambda d: d[:, 1] == 0)   # snapshots only
    """
    return _slice_data(
        self, _apply_filter(self.data, mask, start, end, idx_start, idx_end)
    )