NautilusTrader
Tutorials
These docs track the unreleased nightly build and may change without notice. Switch to the latest stable docs.

Backtest with Order Book Depth Data (Bybit)

Replay Bybit ob500 order book deltas through BacktestNode and run the OrderBookImbalance strategy. Same shape as the Binance variant, different loader and different instrument.

View source on GitHub.

Introduction

Bybit publishes a single per-symbol L2 deltas archive at depth 500. The tutorial reads the daily ZIP into a DataFrame. The strategy is the same OrderBookImbalance as in the Binance tutorial: when the smaller side of the BBO drops below trigger_imbalance_ratio of the larger, fire a single FOK limit order on the thicker side.

OrderBookImbalance is a teaching strategy and has no edge.

Prerequisites

  • Python 3.12+
  • NautilusTrader 2.x installed (pip install -U --pre nautilus_trader)
  • pandas (pip install pandas). The wheel declares no runtime dependencies.
  • The sibling orderbook_data.py and orderbook_imbalance.py files. Keep them next to this tutorial when downloading or converting it with Jupytext.
  • Optionally, a daily Bybit ob500 ZIP, e.g. 2024-12-01_XRPUSDT_ob500.data.zip from public.bybit.com. Without one the tutorial falls back to a bundled 50-message sample of that archive, which runs end to end over a few seconds of the book.
import os
import shutil
from pathlib import Path

import pandas as pd
from nautilus_trader.backtest import BacktestNode
from nautilus_trader.common import LogLevel
from nautilus_trader.config import (
    BacktestDataConfig,
    BacktestEngineConfig,
    BacktestRunConfig,
    BacktestVenueConfig,
    ImportableStrategyConfig,
    LoggerConfig,
)
from nautilus_trader.core.datetime import dt_to_unix_nanos
from nautilus_trader.model import (
    AccountType,
    BookType,
    CryptoPerpetual,
    Currency,
    InstrumentId,
    OmsType,
    Price,
    Quantity,
    Symbol,
    Venue,
)
from nautilus_trader.persistence import ParquetDataCatalog

from orderbook_data import (
    deltas_from_frame,
    load_bybit_order_book_deltas,
    sample_data_path,
)

Loading data

Place the daily archive under NAUTILUS_DATA_DIR/Bybit/ to replay a full day. The tutorial otherwise reads the bundled sample so it runs without a download.

DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "Bybit"
path_update = DATA_DIR / "2024-12-01_XRPUSDT_ob500.data.zip"
if not path_update.is_file():
    path_update = sample_data_path("bybit/xrpusdt-ob500.data.zip")

path_update
# Read the first 1M deltas; the full file is larger.
nrows = 1_000_000
df_raw = load_bybit_order_book_deltas(path_update, nrows=nrows)
df_raw.head()

Build current model objects

XRPUSDT_BYBIT = CryptoPerpetual(
    instrument_id=InstrumentId(Symbol("XRPUSDT-LINEAR"), Venue("BYBIT")),
    raw_symbol=Symbol("XRPUSDT"),
    base_currency=Currency.from_str("XRP"),
    quote_currency=Currency.from_str("USDT"),
    settlement_currency=Currency.from_str("USDT"),
    is_inverse=False,
    price_precision=4,
    size_precision=0,
    price_increment=Price(0.0001, precision=4),
    size_increment=Quantity(1, precision=0),
    ts_event=0,
    ts_init=0,
)

deltas = deltas_from_frame(df_raw, XRPUSDT_BYBIT)
deltas.sort(key=lambda x: x.ts_init)
deltas[:10]

Set up the data catalog

CATALOG_PATH = Path.cwd() / "catalog"
if CATALOG_PATH.exists():
    shutil.rmtree(CATALOG_PATH)
CATALOG_PATH.mkdir()

catalog = ParquetDataCatalog(str(CATALOG_PATH))
catalog.write_instruments([XRPUSDT_BYBIT])
catalog.write_order_book_deltas(deltas)
catalog.instruments()
start = dt_to_unix_nanos(pd.Timestamp("2024-11-30", tz="UTC"))
end = dt_to_unix_nanos(pd.Timestamp("2024-12-04", tz="UTC"))

deltas = catalog.query_order_book_deltas(
    identifiers=[str(XRPUSDT_BYBIT.id)],
    start=start,
    end=end,
)
print(len(deltas))
deltas[:10]

Configure the backtest

instrument = catalog.instruments()[0]
book_type = BookType.L2_MBP

data_configs = [
    BacktestDataConfig(
        catalog_path=str(CATALOG_PATH),
        data_type="OrderBookDelta",
        instrument_id=instrument.id,
    ),
]

venues_configs = [
    BacktestVenueConfig(
        name="BYBIT",
        oms_type=OmsType.NETTING,
        account_type=AccountType.MARGIN,
        base_currency=None,
        starting_balances=["200000 XRP", "100000 USDT"],
        book_type=book_type,
    ),
]

strategy_config = ImportableStrategyConfig(
    strategy_path="orderbook_imbalance:OrderBookImbalance",
    config_path="orderbook_imbalance:OrderBookImbalanceConfig",
    config={
        "instrument_id": str(instrument.id),
        "book_type": book_type.name,
        "max_trade_size": "1",
        "min_seconds_between_triggers": 1.0,
    },
)

config = BacktestRunConfig(
    engine=BacktestEngineConfig(
        logging=LoggerConfig(stdout_level=LogLevel.ERROR),
    ),
    data=data_configs,
    venues=venues_configs,
    dispose_on_completion=False,
)

config

Run the backtest

node = BacktestNode(configs=[config])
node.build()
node.add_strategy_from_config(config.id, strategy_config)

result = node.run()
result
node.generate_order_fills_report(config.id)
node.generate_positions_report(config.id)
node.generate_account_report(config.id, venue=Venue("BYBIT"))

What the run produces

The figures below come from a full-day ob500 archive. The bundled sample replays 3,967 deltas and fires 2 of these orders.

The Bybit ob500 archive sometimes starts a minute before the file's nominal date, so the first trades land just before midnight UTC and the rest inside the file's day. With a 1M delta cap, the active window is roughly the first minute. The strategy fires 43 FOK orders during that window.

Top of book during the active minute with FOK fills

Figure 1. XRPUSDT mid, best bid, and best ask during the trigger window. Triangles are entries (up = long, down = short), crosses are closing fills.

Imbalance ratio distribution

Figure 2. smaller / larger BBO size ratio across all sampled top-of-book snapshots, with the 0.20 trigger threshold marked.

Top of book size and mid

Figure 3. Mid price (top) and best bid/ask size in XRP (bottom) across the active window.

Net XRP position trajectory

Figure 4. Cumulative signed XRP position across the FOK fill sequence. Each marker is a fill: blue is a buy, orange is a sell.

Regenerate the panels

A self-contained renderer re-runs the backtest with a sampling actor that captures top of book once per second, then writes PNG panels to the asset directory using the shared nautilus_dark tearsheet theme.

After building NautilusTrader from source, run these commands from the repository root:

make sync
NAUTILUS_DATA_DIR=test_data/local \
    uv run --project python --no-sync \
        python docs/tutorials/assets/backtest_orderbook_bybit/render_panels.py

Next steps

  • Tighter trigger. Drop trigger_imbalance_ratio to 0.10 to require a ten-to-one lean.
  • Longer window. Bump nrows to ten or twenty million for a multi-hour replay.
  • Cross-venue replay. Run the same strategy in two engines (one Bybit, one Binance) and compare imbalance distributions.

On this page