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.
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 installed
(
pip install nautilus_trader) - The sibling
orderbook_data.pyandorderbook_imbalance.pyfiles. Keep them next to this tutorial when downloading or converting it with Jupytext. - A daily Bybit
ob500ZIP, e.g.2024-12-01_XRPUSDT_ob500.data.zipfrom public.bybit.com.
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,
)
configRun the backtest
node = BacktestNode(configs=[config])
node.build()
node.add_strategy_from_config(config.id, strategy_config)
result = node.run()resultnode.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.

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

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

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

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.pyNext steps
- Tighter trigger. Drop
trigger_imbalance_ratioto 0.10 to require a ten-to-one lean. - Longer window. Bump
nrowsto 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.