Skip to content
All library documents

Backtesting Order Book Imbalance on Bybit Depth Data

Code NautilusTrader

Summary

This tutorial demonstrates replaying Bybit’s depth-500 order book deltas in a NautilusTrader backtest. It loads a daily archive or a small sample, converts the records into instrument-specific order book events, writes them to a Parquet catalog, and configures a margin venue with starting balances and maker/taker fees. The example strategy watches best bid and ask sizes; when the smaller side falls below a configured fraction of the larger side and other trigger conditions are met, it submits a fill-or-kill limit order against the thinner side.

The document reports sample and full-archive replay activity, including delta and order counts, but does not present evidence of profitable performance. It explicitly characterizes the strategy as instructional and without an edge. Results depend on the selected data window and simulation setup; the tutorial notes that the archive can begin before its nominal date and that a one-million-delta cap captures only a short active interval. It suggests longer replays and cross-venue comparisons as follow-up experiments.

Key ideas

  • Bybit depth-500 delta archives can be converted into replayable order book events for backtesting.
  • The example strategy triggers when top-of-book size is sufficiently imbalanced and a cooldown has elapsed.
  • It submits a fill-or-kill limit buy at the ask when bids are larger, or a sell at the bid otherwise.
  • The backtest configures instrument details, margin balances, and maker/taker fees, then generates fills, positions, and account reports.
  • The tutorial says the strategy has no demonstrated edge, and the sample window is limited.

Tags

Full text
# backtest_orderbook_bybit.py


```py
# %% [markdown]
# # 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](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/tutorials/backtest_orderbook_binance.py),
# different loader and different instrument.
#
# [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/tutorials/backtest_orderbook_bybit.py).

# %% [markdown]
# ## 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
# against the thinner side: a buy at the best ask when bids are larger,
# otherwise a sell at the best bid.
#
# `OrderBookImbalance` is a teaching strategy and has no edge.
#
# ```mermaid
# flowchart LR
#     subgraph Inputs ["Data engine"]
#         Z["ob500 ZIP archive"]
#     end
#
#     subgraph Engine ["BacktestEngine"]
#         L["load_bybit_order_book_deltas"]
#         W["deltas_from_frame"]
#         B["Per-instrument OrderBook"]
#         C["Cache.order_book"]
#     end
#
#     subgraph Strategy ["OrderBookImbalance"]
#         R{{"larger > trigger_min_size<br/>AND smaller/larger < ratio<br/>AND cooldown elapsed"}}
#         D{{"bid_size > ask_size?"}}
#         BUY["Submit FOK BUY at best ask"]
#         SELL["Submit FOK SELL at best bid"]
#     end
#
#     Z --> L --> W --> B --> C
#     C --> R
#     R -->|yes| D
#     D -->|yes| BUY
#     D -->|no| SELL
# ```

# %% [markdown]
# ## Prerequisites
#
# - Python 3.13+
# - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed
#   (`pip install -U --pre nautilus_trader`)
# - pandas (`pip install pandas`). The wheel declares no runtime dependencies.
# - The sibling
#   [`orderbook_data.py`](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/tutorials/orderbook_data.py)
#   and
#   [`orderbook_imbalance.py`](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/tutorials/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`](https://quote-saver.bycsi.com/orderbook/linear/XRPUSDT/2024-12-01_XRPUSDT_ob500.data.zip)
#   (~377 MB) from Bybit's data CDN. Without one the tutorial falls back to a
#   50-message sample of that archive from the NautilusTrader test data,
#   downloaded from GitHub on first run outside a source checkout. The sample
#   runs end to end over a few seconds of the book.

# %%
import os
import shutil
from decimal import Decimal
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.execution import MakerTakerFeeModel
from nautilus_trader.model import (
    AccountType,
    BookType,
    CryptoPerpetual,
    Currency,
    InstrumentId,
    NautilusDataType,
    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,
)

# %% [markdown]
# ## Loading data
#
# Place the daily archive under `NAUTILUS_DATA_DIR/Bybit/` (default
# `~/Downloads/Data/Bybit/`) to replay a full day. The tutorial otherwise reads
# the test data sample.

# %%
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()

# %% [markdown]
# ### 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]

# %% [markdown]
# ### Set up the data catalog
#
# The tutorial writes the catalog to `catalog/` under the working directory
# and replaces that directory on each run.

# %%
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]

# %% [markdown]
# ## Configure the backtest

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

data_configs = [
    BacktestDataConfig(
        catalog_path=str(CATALOG_PATH),
        data_type=NautilusDataType.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,
        fee_model=MakerTakerFeeModel(
            maker_rate=Decimal("0.0002"),
            taker_rate=Decimal("0.00055"),
        ),
    ),
]

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

# %% [markdown]
# ## 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"))

# %% [markdown]
# ## What the run produces
#
# The figures below come from a full-day `ob500` archive. The test data 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](./assets/backtest_orderbook_bybit/panel_a_top_book.png)
#
# **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](./assets/backtest_orderbook_bybit/panel_b_imbalance_dist.png)
#
# **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](./assets/backtest_orderbook_bybit/panel_c_size_landscape.png)
#
# **Figure 3.** *Mid price (top) and best bid/ask size in XRP (bottom)
# across the active window.*
#
# ![Net XRP position trajectory](./assets/backtest_orderbook_bybit/panel_d_position.png)
#
# **Figure 4.** *Cumulative signed XRP position across the FOK fill
# sequence. Each marker is a fill: blue is a buy, orange is a sell.*

# %% [markdown]
# ### 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:
#
# ```bash
# make sync
# NAUTILUS_DATA_DIR=test_data/local \
#     uv run --project python --no-sync \
#         python docs/tutorials/assets/backtest_orderbook_bybit/render_panels.py
# ```

# %% [markdown]
# ## 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.

```

Shown in full with attribution under the source's licence. Licence: LGPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.