Skip to content
All library documents

Trading Best-Level Order Book Imbalances with Capped FOK Orders

Code NautilusTrader

Summary

This strategy reacts to a large size imbalance between the best bid and best ask in a limit order book. It ignores missing, crossed or incomplete book data and requires both displayed sizes to be positive. A trigger occurs only when the larger queue exceeds a minimum size and the smaller queue is less than a configured fraction of the larger one. The defaults set the size threshold to 100 units and the ratio to 0.20, with at least one second between triggers.

When bids are larger, the strategy submits a buy at the best ask; when asks are larger, it submits a sell at the best bid. Order quantity is capped by both the opposing best-level size and a maximum trade size. Orders are fill-or-kill limits, and new orders are suppressed while one is in flight. The code includes no backtest or evidence of profitability. Its signals depend on displayed top-of-book liquidity, and the excerpt does not describe adverse-selection controls or broader risk limits.

Key ideas

  • The trigger compares displayed size at the best bid and ask against a minimum size and imbalance ratio.
  • A bid-side imbalance leads to a buy at the best ask, while an ask-side imbalance leads to a sell at the best bid.
  • Order quantity is capped by the available opposing top-level size and a configured maximum.
  • The strategy uses fill-or-kill limit orders and enforces a minimum interval between triggers.
  • The code provides no performance evidence or comprehensive account-level risk controls.

Tags

Full text
# orderbook_imbalance.py


```py
# %% [markdown]
# # Order Book Imbalance
#
# Define the reusable order book imbalance strategy used by the Binance and Bybit
# order book backtest tutorials.

# %%
from __future__ import annotations

from decimal import Decimal

from nautilus_trader.config import StrategyConfig
from nautilus_trader.model import (
    BookType,
    InstrumentId,
    OrderBookDeltas,
    OrderSide,
    Quantity,
    TimeInForce,
)
from nautilus_trader.trading import Strategy


class OrderBookImbalanceConfig(StrategyConfig):
    def __init__(
        self,
        *,
        instrument_id: str,
        max_trade_size: str,
        trigger_min_size: float = 100.0,
        trigger_imbalance_ratio: float = 0.20,
        min_seconds_between_triggers: float = 1.0,
        book_type: str = "L2_MBP",
        **_kwargs: object,
    ) -> None:
        super().__init__()
        self.instrument_id = instrument_id
        self.max_trade_size = max_trade_size
        self.trigger_min_size = trigger_min_size
        self.trigger_imbalance_ratio = trigger_imbalance_ratio
        self.min_seconds_between_triggers = min_seconds_between_triggers
        self.book_type = book_type


class OrderBookImbalance(Strategy):
    def __init__(self, config: OrderBookImbalanceConfig) -> None:
        if not 0 < config.trigger_imbalance_ratio < 1:
            raise ValueError("trigger_imbalance_ratio must be between 0 and 1")
        if config.min_seconds_between_triggers < 0:
            raise ValueError("min_seconds_between_triggers must be non-negative")

        super().__init__(config)
        self._instrument_id = InstrumentId.from_str(config.instrument_id)
        self._book_type = BookType.from_str(config.book_type)
        self._max_trade_size = Decimal(config.max_trade_size)
        self._trigger_min_size = Decimal(str(config.trigger_min_size))
        self._trigger_imbalance_ratio = Decimal(str(config.trigger_imbalance_ratio))
        self._trigger_interval_ns = int(config.min_seconds_between_triggers * 1_000_000_000)
        self._instrument = None
        self._last_trigger_ns: int | None = None

    def on_start(self) -> None:
        self._instrument = self.cache.instrument(self._instrument_id)
        if self._instrument is None:
            log_msg = f"Could not find instrument for {self._instrument_id}"
            self.log.error(log_msg)
            self.stop()
            return

        self.subscribe_book_deltas(self._instrument_id, self._book_type, managed=True)

    def on_book_deltas(self, _deltas: OrderBookDeltas) -> None:
        book = self.cache.order_book(self._instrument_id)
        if book is None or not book.spread():
            return

        bid_size = book.best_bid_size()
        ask_size = book.best_ask_size()
        if bid_size is None or bid_size <= 0 or ask_size is None or ask_size <= 0:
            return

        bid = bid_size.as_decimal()
        ask = ask_size.as_decimal()
        smaller = min(bid, ask)
        larger = max(bid, ask)
        if larger <= self._trigger_min_size or smaller / larger >= self._trigger_imbalance_ratio:
            return

        now = self.clock.timestamp_ns()
        if (
            self._last_trigger_ns is not None
            and now - self._last_trigger_ns < self._trigger_interval_ns
        ):
            return
        if self.cache.orders_inflight(strategy_id=self.strategy_id):
            return

        if bid > ask:
            side = OrderSide.BUY
            price = book.best_ask_price()
            level_size = ask
        else:
            side = OrderSide.SELL
            price = book.best_bid_price()
            level_size = bid

        if price is None or self._instrument is None:
            return

        self._last_trigger_ns = now
        order = self.order_factory.limit(
            instrument_id=self._instrument_id,
            order_side=side,
            quantity=Quantity.from_decimal_dp(
                min(level_size, self._max_trade_size),
                self._instrument.size_precision,
            ),
            price=price,
            time_in_force=TimeInForce.FOK,
            post_only=False,
        )
        self.submit_order(order)

    def on_stop(self) -> None:
        self.cancel_all_orders(self._instrument_id)
        self.close_all_positions(self._instrument_id)

    def on_reset(self) -> None:
        self._instrument = None
        self._last_trigger_ns = None

```

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.