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 much more resting size than the other, the book leans toward that side. As the heavier side absorbs flow, short-term price often moves toward the thinner side.
The AX example OrderBookImbalance strategy submits a fill-or-kill (FOK) limit order that takes the thinner
side. For example, it buys at the ask when bids hold more size. It submits an order each time the ratio
between sides is less than the threshold and the cooldown has elapsed.
The strategy only needs the BBO, so it uses mbp-1 (market by price, single best bid/ask) quote data. It does
not need the full L2 book, which reduces data costs for backtesting.
OrderBookImbalance is a teaching strategy and has no edge.
Why proxy data
Databento does not yet cover AX Exchange. CME GC gold futures are the most liquid gold derivatives globally.
They provide representative microstructure for backtests of gold strategies. The continuous contract
GC.v.0 joins data across expiries from the highest-volume contract. This follows liquidity as a perpetual
does.
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 this mapping selects a single underlying
instrument at any moment.
For more information about the predictive power of book imbalance features, see Databento's blog post on HFT signals with sklearn.
Prerequisites
- Python 3.13+
- NautilusTrader installed.
- A clone of the NautilusTrader repository. The snippets read
crates/adapters/databento/publishers.jsonand import the strategy fromexamples/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 request downloads one trading day. Subsequent runs use the same file.
Load into Nautilus quote ticks
DatabentoDataLoader.load_quotes parses the .dbn.zst archive and emits QuoteTick objects. The
instrument_id argument overrides Databento symbology, so every tick uses XAU-PERP.AX. The loader cannot
resolve price precision for that ID. 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.
| Parameter | Value | Description |
|---|---|---|
max_trade_size | 10 | Cap on contracts per FOK order. |
trigger_min_size | 1 | Larger side must hold more than one contract. |
trigger_imbalance_ratio | 0.10 | Trigger when smaller / larger < 10%. |
min_seconds_between_triggers | 5.0 | Cooldown 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()Get reports from 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
A replay of 2024-11-15 GC.v.0 mbp-1 (one trading day) through OrderBookImbalance(0.10, 1.0, 5s) produces
2,378 FOK fills. These fills net into 5 closed position cycles. Cumulative realized pnl ends at -4,170
USD. The strategy loses money steadily across the day, mostly from spread costs on incremental FOK fills
that increase existing positions.

Figure 1. GC.v.0 top of book around a short entry near 09 and exit near 09, then a long re-entry until 09. Triangles are entries from flat, crosses are returns to flat, open circles are incremental FOK fills that grew the position.

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.

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.

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 runs the backtest again with a quote-sampling actor. It writes PNGs to the asset
directory with 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.pyNext steps
- Stricter trigger: Lower
trigger_imbalance_ratioto0.05or raisetrigger_min_sizeto5to require a stronger signal before order submission. - Different sessions: Replay regular trading hours (RTH) only or several days to compare behavior 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. - 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 uses a LiveNode
in place of the BacktestEngine. It configures AX data and execution clients 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
Mean Reversion with Proxy FX Data (AX Exchange)
This tutorial backtests a Bollinger-band mean-reversion strategy on EURUSD-PERP at AX Exchange using TrueFX EUR/USD spot ticks as a proxy.
On-Chain Grid Market Making with Short-Term Orders (dYdX)
This tutorial runs the shipped GridMarketMaker strategy on dYdX v4 through the Rust LiveNode. The strategy places symmetric limit orders around the mid and...