NautilusTrader
Tutorials

Nightly docs

These docs track unreleased changes and may change without notice. Their code examples can use APIs that the latest release lacks, so run them with a development wheel or switch to the latest release docs.

Gold Perpetual Book Imbalance with Proxy Futures Data (AX Exchange)

This tutorial backtests a top-of-book imbalance strategy on XAU-PERP at AX Exchange using Databento CME gold futures (GC.v.0) mbp-1 quotes as a proxy.

Introduction

Top-of-book imbalance is a microstructure signal: when one side of the BBO holds significantly more resting size than the other, the book is leaning and short-term price often moves toward the thinner side as the heavier side absorbs flow. The AX example OrderBookImbalance strategy fires a fill-or-kill (FOK) limit order that takes the thinner side (buying at the ask when bids are heavier) every time the ratio between sides clears a threshold and a cooldown has elapsed.

Because the strategy only needs the BBO, it works with mbp-1 (market by price, single best bid/ask) quote data rather than the full L2 book. That keeps source costs down for backtesting.

OrderBookImbalance is a teaching strategy and has no edge.

Why proxy data

AX Exchange is new and not yet covered by Databento. CME GC gold futures are the most liquid gold derivatives globally and provide representative microstructure for backtesting gold strategies. We use the continuous contract GC.v.0 so the file stitches across expiries on the highest-volume contract, mirroring how a perpetual chases liquidity. The stype_in="continuous" parameter resolves the symbol through Databento's continuous mapping at request time. The instrument_id override at load time is safe because the continuous contract maps to a single underlying instrument at any moment.

For a deeper read on the predictive power of book imbalance features, see Databento's blog post on HFT signals with sklearn.

Prerequisites

  • Python 3.12+
  • NautilusTrader installed.
  • A clone of the NautilusTrader repository. The snippets read crates/adapters/databento/publishers.json and import the strategy from examples/live/architect_ax, so run them from the repository root:
git clone https://github.com/nautechsystems/nautilus_trader
cd nautilus_trader
  • A Databento API key:
export DATABENTO_API_KEY="your-api-key"
  • The Databento Python client: pip install databento.

Data preparation

Download CME gold futures quotes

import databento as db
from pathlib import Path

data_path = Path("gc_gold_quotes.dbn.zst")

if not data_path.exists():
    client = db.Historical()
    data = client.timeseries.get_range(
        dataset="GLBX.MDP3",
        symbols=["GC.v.0"],
        stype_in="continuous",
        schema="mbp-1",
        start="2024-11-15",
        end="2024-11-16",
    )
    data.to_file(data_path)

This pulls one trading day. The file is reused on subsequent runs.

Load into Nautilus quote ticks

DatabentoDataLoader.load_quotes parses the .dbn.zst archive and emits QuoteTick objects. The instrument_id argument overrides the Databento symbology so every tick appears to come from XAU-PERP.AX. The loader cannot resolve a price precision for that ID, so pass price_precision explicitly; it must match the instrument definition below.

from nautilus_trader.adapters.databento import DatabentoDataLoader
from nautilus_trader.model import InstrumentId

instrument_id = InstrumentId.from_str("XAU-PERP.AX")

publishers_path = Path("crates/adapters/databento/publishers.json")
loader = DatabentoDataLoader(publishers_path)
quotes = loader.load_quotes(
    filepath=data_path,
    instrument_id=instrument_id,
    price_precision=2,
)

Instrument definition

Proxy data needs a manual instrument definition. Price precision, tick size, and margin parameters are backtest assumptions: the 0.01 tick is finer than both the CME GC tick (0.10) and the AX XAU-PERP tick (0.1).

from decimal import Decimal

from nautilus_trader.model import AssetClass
from nautilus_trader.model import Currency
from nautilus_trader.model import PerpetualContract
from nautilus_trader.model import Price
from nautilus_trader.model import Quantity
from nautilus_trader.model import Symbol

USD = Currency.from_str("USD")

XAU_PERP = PerpetualContract(
    instrument_id=instrument_id,
    raw_symbol=Symbol("XAU-PERP"),
    underlying="XAU",
    asset_class=AssetClass.COMMODITY,
    quote_currency=USD,
    settlement_currency=USD,
    is_inverse=False,
    price_precision=2,
    size_precision=0,
    price_increment=Price.from_str("0.01"),
    size_increment=Quantity.from_int(1),
    multiplier=Quantity.from_int(1),
    lot_size=Quantity.from_int(1),
    margin_init=Decimal("0.08"),
    margin_maint=Decimal("0.04"),
    ts_event=0,
    ts_init=0,
)

Fees are explicit backtest assumptions. Check AX documentation for current rates.

Strategy configuration

The strategy subscribes to quotes and compares bid and ask sizes on each QuoteTick. It does not subscribe to L2 book deltas.

ParameterValueDescription
max_trade_size10Cap on contracts per FOK order.
trigger_min_size1Larger side must hold more than one contract.
trigger_imbalance_ratio0.10Trigger when smaller / larger < 10%.
min_seconds_between_triggers5.0Cooldown between consecutive triggers.

The AX examples define the strategy in examples/live/architect_ax/strategies.py. From the repository root:

import sys
from pathlib import Path

sys.path.insert(0, str(Path("examples/live/architect_ax")))
from strategies import OrderBookImbalance
from strategies import OrderBookImbalanceConfig

strategy = OrderBookImbalance(
    OrderBookImbalanceConfig(
        instrument_id=instrument_id,
        max_trade_size=Decimal(10),
        trigger_min_size=Decimal(1),
        trigger_imbalance_ratio=Decimal("0.10"),
        min_seconds_between_triggers=5.0,
    ),
)

Backtest setup

from decimal import Decimal

from nautilus_trader.common import LogLevel
from nautilus_trader.backtest import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import LoggerConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.model import AccountType
from nautilus_trader.model import Money
from nautilus_trader.model import OmsType
from nautilus_trader.model import TraderId
from nautilus_trader.model import Venue

engine = BacktestEngine(
    BacktestEngineConfig(
        trader_id=TraderId.from_str("BACKTESTER-001"),
        logging=LoggerConfig(stdout_level=LogLevel.INFO),
    ),
)

AX = Venue("AX")
engine.add_venue(
    venue=AX,
    oms_type=OmsType.NETTING,
    account_type=AccountType.MARGIN,
    base_currency=USD,
    starting_balances=[Money.from_str("100000 USD")],
    fee_model=MakerTakerFeeModel(
        maker_rate=Decimal("0.0002"),
        taker_rate=Decimal("0.0005"),
    ),
)

engine.add_instrument(XAU_PERP)
engine.add_data(quotes)
engine.add_strategy(strategy)
engine.run()

Reports are on the engine:

print(engine.generate_account_report(venue=AX))
print(engine.generate_order_fills_report())
print(engine.generate_positions_report())

engine.reset()
engine.dispose()

The runnable example is at architect_ax_book_imbalance.py.

What the run produces

Replaying 2024-11-15 GC.v.0 mbp-1 (one trading day) through OrderBookImbalance(0.10, 1.0, 5s) prints 2,378 FOK fills net into 5 closed position cycles. Cumulative realized pnl ends at -4,170 USD: the strategy bleeds steadily across the day, mostly through spread cost on incremental FOK fills that add to existing positions.

GC.v.0 top of book around an active cycle

Figure 1. GC.v.0 top of book around the cycle that opened with a short entry near 09

and exited near 09
, then re-entered long until 09
. Triangles are entries from flat, crosses are returns to flat, open circles are incremental FOK fills that grew the position.

Imbalance ratio distribution

Figure 2. smaller / larger BBO size ratio across all sampled top-of-book snapshots, with the 0.10 trigger threshold marked. The mass left of the threshold is the addressable trigger region.

Mid and top-of-book size across the day

Figure 3. Mid price (top) and best bid/ask size in contracts (bottom) across the trading day. Top-of-book sizes flicker between roughly two and fifty contracts; the mid traverses about a fifteen-dollar range.

Cumulative realized pnl per closed position

Figure 4. Cumulative realized USD pnl across the five closed position cycles. The slope is consistently negative and the per-cycle pnl is dominated by spread.

Regenerate the panels

A self-contained renderer re-runs the backtest with a quote-sampling actor and writes PNGs to the asset directory using the nautilus_dark tearsheet theme.

After building NautilusTrader from source, run these commands from the repository root:

make sync
GC_DBN=gc_gold_quotes.dbn.zst \
    uv run --project python --no-sync \
        python docs/tutorials/assets/gold_book_imbalance_ax/render_panels.py

Next steps

  • Stricter trigger. Lower trigger_imbalance_ratio to 0.05 or raise trigger_min_size to 5 to require more conviction before firing.
  • Different sessions. Replay regular trading hours (RTH) only or roll through several days to see how the strategy behaves across regimes.
  • Other instruments. AX offers FX perpetuals (EURUSD-PERP, GBPUSD-PERP) and silver (XAG-PERP). The same proxy approach works with the corresponding CME futures.
  • Go live on the AX sandbox. See the AX Exchange integration guide once the backtest behaves.

Running live

The same OrderBookImbalance strategy runs live against the AX sandbox. The launch script swaps the BacktestEngine for a LiveNode with the AX data and execution clients configured for AxEnvironment.SANDBOX. See the live example: ax_book_imbalance.py.

For connection setup and API key configuration, see the AX Exchange integration guide.

Further reading

On this page