Skip to content
All library documents

Bollinger Band Reversion and Order Book Imbalance Strategies

Code NautilusTrader

Summary

The document presents two example automated strategies. The first combines Bollinger Bands with RSI: it enters long when price closes at or below the lower band and RSI is below a configurable threshold, and enters short when price reaches the upper band with RSI above its threshold. Positions are closed when price returns to the middle band. The example uses market orders and allows positions to be closed when the strategy stops.

The second strategy watches top-of-book bid and ask sizes. When the larger size exceeds a minimum and the smaller-to-larger size ratio falls below a threshold, it trades in the direction of the larger displayed side, using a fill-or-kill limit order at the opposing quote. A time interval between triggers and maximum trade size constrain activity. The code includes configurable parameters and basic order and position handling, but supplies no performance results. It does not address broader validation, transaction costs, or whether these signals remain predictive across instruments and market conditions.

Key ideas

  • The mean-reversion example combines Bollinger Band extremes with RSI thresholds for entries.
  • Long and short positions exit when price returns to the Bollinger middle band.
  • The imbalance example compares displayed bid and ask sizes and trades toward the larger side when the gap passes configured thresholds.
  • A trigger delay, trade-size cap, and fill-or-kill limit order shape execution in the imbalance example.
  • The examples specify trading rules but provide no evidence of profitability.

Tags

Full text
# strategies.py


```py
# -------------------------------------------------------------------------------------------------
#  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
#  https://nautechsystems.io
#
#  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
#  You may not use this file except in compliance with the License.
#  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
# -------------------------------------------------------------------------------------------------
"""
Example of Architect AX strategies.
"""

from __future__ import annotations

from decimal import Decimal
from typing import Any

from nautilus_trader.common import LogColor
from nautilus_trader.config import StrategyConfig
from nautilus_trader.indicators import BollingerBands
from nautilus_trader.indicators import RelativeStrengthIndex
from nautilus_trader.model import Bar
from nautilus_trader.model import BarType
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import OrderSide
from nautilus_trader.model import Quantity
from nautilus_trader.model import QuoteTick
from nautilus_trader.model import TimeInForce
from nautilus_trader.trading import Strategy


class BBMeanReversionConfig(StrategyConfig):
    """
    Collect bbmean reversion config tests.
    """

    def __init__(
        self,
        *,
        instrument_id: InstrumentId,
        bar_type: BarType,
        trade_size: Decimal,
        bb_period: int = 20,
        bb_std: float = 2.0,
        rsi_period: int = 14,
        rsi_buy_threshold: float = 30.0,
        rsi_sell_threshold: float = 70.0,
        close_positions_on_stop: bool = True,
        **_kwargs: Any,
    ) -> None:
        """
        Initialize the instance.
        """
        super().__init__()
        self.instrument_id = instrument_id
        self.bar_type = bar_type
        self.trade_size = trade_size
        self.bb_period = bb_period
        self.bb_std = bb_std
        self.rsi_period = rsi_period
        self.rsi_buy_threshold = rsi_buy_threshold
        self.rsi_sell_threshold = rsi_sell_threshold
        self.close_positions_on_stop = close_positions_on_stop


class BBMeanReversion(Strategy):
    """
    Trade Bollinger Band mean reversion signals with RSI confirmation.
    """

    def __init__(self, config: BBMeanReversionConfig) -> None:
        """
        Initialize the instance.
        """
        if config.trade_size <= 0:
            raise ValueError("trade_size must be positive")

        super().__init__(config)
        self._instrument_id = config.instrument_id
        self._bar_type = config.bar_type
        self._trade_size = config.trade_size
        self._rsi_buy_threshold = config.rsi_buy_threshold
        self._rsi_sell_threshold = config.rsi_sell_threshold
        self._close_positions_on_stop = config.close_positions_on_stop
        self._instrument: Any | None = None
        self._trade_qty: Quantity | None = None
        self._bb = BollingerBands(config.bb_period, config.bb_std)
        self._rsi = RelativeStrengthIndex(config.rsi_period)

    def on_start(self) -> None:
        """
        On start.
        """
        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._trade_qty = Quantity.from_decimal_dp(
            self._trade_size,
            self._instrument.size_precision,
        )

        if self._trade_qty.as_decimal() <= 0:
            log_msg = f"Trade size {self._trade_size} rounds to zero for {self._instrument_id}"
            self.log.error(log_msg)
            self.stop()
            return

        self.register_indicator_for_bars(self._bar_type, self._bb)
        self.register_indicator_for_bars(self._bar_type, self._rsi)
        self.subscribe_bars(self._bar_type)

    def on_bar(self, bar: Bar) -> None:
        """
        On bar.
        """
        self.log.info(repr(bar), LogColor.CYAN)

        if not self.indicators_initialized():
            return
        if bar.open == bar.high == bar.low == bar.close:
            return

        close = bar.close.as_double()
        if not self._check_exit(close):
            self._check_entry(close)

    def on_stop(self) -> None:
        """
        On stop.
        """
        self.cancel_all_orders(self._instrument_id)
        if self._close_positions_on_stop:
            self.close_all_positions(self._instrument_id)
        self.unsubscribe_bars(self._bar_type)

    def on_reset(self) -> None:
        """
        On reset.
        """
        self._instrument = None
        self._trade_qty = None
        self._bb.reset()
        self._rsi.reset()

    def _check_exit(self, close: float) -> bool:
        if self.portfolio.is_net_long(self._instrument_id) and close >= self._bb.middle:
            self.close_all_positions(self._instrument_id)
            return True
        if self.portfolio.is_net_short(self._instrument_id) and close <= self._bb.middle:
            self.close_all_positions(self._instrument_id)
            return True
        return False

    def _check_entry(self, close: float) -> None:
        if close <= self._bb.lower and self._rsi.value < self._rsi_buy_threshold:
            if self.portfolio.is_net_short(self._instrument_id):
                self.close_all_positions(self._instrument_id)
            if not self.portfolio.is_net_long(self._instrument_id):
                self._submit_market_order(OrderSide.BUY)
        elif close >= self._bb.upper and self._rsi.value > self._rsi_sell_threshold:
            if self.portfolio.is_net_long(self._instrument_id):
                self.close_all_positions(self._instrument_id)
            if not self.portfolio.is_net_short(self._instrument_id):
                self._submit_market_order(OrderSide.SELL)

    def _submit_market_order(self, order_side: OrderSide) -> None:
        if self._trade_qty is None:
            return

        order = self.order_factory.market(
            instrument_id=self._instrument_id,
            order_side=order_side,
            quantity=self._trade_qty,
            time_in_force=TimeInForce.GTC,
        )
        self.submit_order(order)


class OrderBookImbalanceConfig(StrategyConfig):
    """
    Collect order book imbalance config tests.
    """

    def __init__(
        self,
        *,
        instrument_id: InstrumentId,
        max_trade_size: Decimal,
        trigger_min_size: Decimal = Decimal(100),
        trigger_imbalance_ratio: Decimal = Decimal("0.20"),
        min_seconds_between_triggers: float = 1.0,
        dry_run: bool = False,
        **_kwargs: Any,
    ) -> None:
        """
        Initialize the instance.
        """
        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.dry_run = dry_run


class OrderBookImbalance(Strategy):
    """
    Send FOK limit orders when AX top-of-book sizes become imbalanced.
    """

    def __init__(self, config: OrderBookImbalanceConfig) -> None:
        """
        Initialize the instance.
        """
        if config.max_trade_size <= 0:
            raise ValueError("max_trade_size must be positive")
        if config.trigger_min_size <= 0:
            raise ValueError("trigger_min_size must be positive")
        if not Decimal(0) < config.trigger_imbalance_ratio < Decimal(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 = config.instrument_id
        self._max_trade_size = config.max_trade_size
        self._trigger_min_size = config.trigger_min_size
        self._trigger_imbalance_ratio = config.trigger_imbalance_ratio
        self._trigger_interval_ns = int(config.min_seconds_between_triggers * 1_000_000_000)
        self._dry_run = config.dry_run
        self._instrument: Any | None = None
        self._last_trigger_ns: int | None = None

    def on_start(self) -> None:
        """
        On start.
        """
        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_quotes(self._instrument_id)

    def on_quote(self, quote: QuoteTick) -> None:
        """
        On quote.
        """
        bid_size = quote.bid_size.as_decimal()
        ask_size = quote.ask_size.as_decimal()
        if bid_size <= 0 or ask_size <= 0:
            return

        smaller = min(bid_size, ask_size)
        larger = max(bid_size, ask_size)
        ratio = smaller / larger
        if larger <= self._trigger_min_size or ratio >= 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_size > ask_size:
            order_side = OrderSide.BUY
            price = quote.ask_price
            level_size = ask_size
        else:
            order_side = OrderSide.SELL
            price = quote.bid_price
            level_size = bid_size

        self._last_trigger_ns = now
        if self._dry_run or self._instrument is None:
            return

        quantity = Quantity.from_decimal_dp(
            min(level_size, self._max_trade_size),
            self._instrument.size_precision,
        )

        if quantity.as_decimal() <= 0:
            log_msg = f"Trade quantity rounds to zero for {self._instrument_id}"
            self.log.error(log_msg)
            return

        order = self.order_factory.limit(
            instrument_id=self._instrument_id,
            order_side=order_side,
            quantity=quantity,
            price=price,
            time_in_force=TimeInForce.FOK,
            post_only=False,
        )
        self.submit_order(order)

    def on_stop(self) -> None:
        """
        On stop.
        """
        self.cancel_all_orders(self._instrument_id)
        if not self._dry_run:
            self.close_all_positions(self._instrument_id)
        self.unsubscribe_quotes(self._instrument_id)

    def on_reset(self) -> None:
        """
        On reset.
        """
        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.