NautilusTrader
Tutorials

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 BybitOrderBookDeltaDataLoader reads the daily ZIP directly 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 installed (pip install nautilus_trader)
  • A daily Bybit ob500 ZIP, e.g. 2024-12-01_XRPUSDT_ob500.data.zip from public.bybit.com.
import os
import shutil
from decimal import Decimal
from pathlib import Path

import pandas as pd

from nautilus_trader.adapters.bybit.loaders import BybitOrderBookDeltaDataLoader
from nautilus_trader.backtest.node import BacktestDataConfig
from nautilus_trader.backtest.node import BacktestEngineConfig
from nautilus_trader.backtest.node import BacktestNode
from nautilus_trader.backtest.node import BacktestRunConfig
from nautilus_trader.backtest.node import BacktestVenueConfig
from nautilus_trader.config import ImportableStrategyConfig
from nautilus_trader.config import LoggingConfig
from nautilus_trader.core.datetime import dt_to_unix_nanos
from nautilus_trader.model import OrderBookDelta
from nautilus_trader.persistence.catalog import ParquetDataCatalog
from nautilus_trader.persistence.wranglers import OrderBookDeltaDataWrangler
from nautilus_trader.test_kit.providers import TestInstrumentProvider

Loading data

DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "Bybit"
data_path = DATA_DIR
raw_files = [f for f in data_path.iterdir() if f.is_file()]
assert raw_files, f"Unable to find any data files in directory {data_path}"
raw_files
# Read the first 1M deltas; the full file is larger.
path_update = data_path / "2024-12-01_XRPUSDT_ob500.data.zip"
nrows = 1_000_000
df_raw = BybitOrderBookDeltaDataLoader.load(path_update, nrows=nrows)
df_raw.head()

Process deltas using a wrangler

XRPUSDT_BYBIT = TestInstrumentProvider.xrpusdt_linear_bybit()
wrangler = OrderBookDeltaDataWrangler(XRPUSDT_BYBIT)

deltas = wrangler.process(df_raw)
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(CATALOG_PATH)
catalog.write_data([XRPUSDT_BYBIT])
catalog.write_data(deltas)
catalog.instruments()
start = dt_to_unix_nanos(pd.Timestamp("2022-11-01", tz="UTC"))
end = dt_to_unix_nanos(pd.Timestamp("2024-12-04", tz="UTC"))

deltas = catalog.order_book_deltas(start=start, end=end)
print(len(deltas))
deltas[:10]

Configure the backtest

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

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

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

strategies = [
    ImportableStrategyConfig(
        strategy_path="nautilus_trader.examples.strategies.orderbook_imbalance:OrderBookImbalance",
        config_path="nautilus_trader.examples.strategies.orderbook_imbalance:OrderBookImbalanceConfig",
        config={
            "instrument_id": instrument.id,
            "book_type": book_type,
            "max_trade_size": Decimal("1.000"),
            "min_seconds_between_triggers": 1.0,
        },
    ),
]

config = BacktestRunConfig(
    engine=BacktestEngineConfig(
        strategies=strategies,
        logging=LoggingConfig(log_level="ERROR"),
    ),
    data=data_configs,
    venues=venues_configs,
)

config

Run the backtest

node = BacktestNode(configs=[config])

result = node.run()
result
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.model import Venue


engine: BacktestEngine = node.get_engine(config.id)

engine.trader.generate_order_fills_report()
engine.trader.generate_positions_report()
engine.trader.generate_account_report(Venue("BYBIT"))

What the run produces

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.

uv sync --extra visualization
NAUTILUS_DATA_DIR=tests/test_data/local \
    python3 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