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

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,
)

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 = 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 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=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