Testing a Top-of-Book Imbalance Strategy on Gold Proxy Data
Summary
This tutorial describes a short-term order book imbalance strategy for a gold perpetual contract. It uses CME gold futures best bid and ask quotes as a proxy for the exchange contract, then triggers fill-or-kill orders toward the thinner side when the bid and ask sizes meet an imbalance threshold and a cooldown has elapsed. Since the strategy uses only the best quotes, the tutorial explains how to build the backtest from single-level market-by-price data rather than full depth.
A one-day replay produced thousands of fills across five closed position cycles and ended with a substantial realized loss. The accompanying figures attribute the persistent decline mainly to spread costs as fills added to existing positions. The example is explicitly presented as educational and without an edge. Its results are limited to one day of proxy futures data; instrument precision, tick size, margins, and fees are assumptions, and proxy order-book behavior may differ from the target perpetual market.
Key ideas
- The signal compares best bid and ask sizes and trades toward the thinner side when imbalance and cooldown conditions are met.
- Single-level quote data is sufficient for this strategy, reducing the data requirements compared with full-depth order book data.
- CME gold futures serve as a proxy for the target gold perpetual because direct data coverage is unavailable.
- The one-day backtest lost money, with spread costs cited as a major drag.
- Proxy data and assumed instrument and fee settings limit how confidently the results transfer to live trading.
Tags
Full text
# Gold Perpetual Book Imbalance with Proxy Futures Data (AX Exchange)
# 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](https://architect.exchange) using
[Databento](https://databento.com) 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.
```mermaid
flowchart LR
subgraph Inputs ["Data"]
D["Databento mbp-1 quotes"]
end
subgraph Engine ["BacktestEngine"]
L["DatabentoDataLoader"]
Q["QuoteTick stream"]
B["QuoteTick BBO"]
end
subgraph Strategy ["OrderBookImbalance"]
R{{"larger > trigger_min_size<br/>AND smaller/larger < ratio<br/>AND cooldown elapsed"}}
D2{{"bid_size > ask_size?"}}
BUY["Submit FOK BUY at best ask"]
SELL["Submit FOK SELL at best bid"]
end
D --> L --> Q --> B
B --> R
R -->|yes| D2
D2 -->|yes| BUY
D2 -->|no| SELL
```
### 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](https://databento.com/blog/hft-sklearn-python).
## Prerequisites
- Python 3.13+
- [NautilusTrader installed](../getting_started/installation.md).
- 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:
```bash
git clone https://github.com/nautechsystems/nautilus_trader
cd nautilus_trader
```
- A Databento API key:
```bash
export DATABENTO_API_KEY="your-api-key"
```
- The Databento Python client: `pip install databento`.
## Data preparation
### Download CME gold futures quotes
```python
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.
```python
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`).
```python
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](https://docs.architect.exchange/) 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`](https://github.com/nautechsystems/nautilus_trader/blob/develop/examples/live/architect_ax/strategies.py).
From the repository root:
```python
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
```python
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:
```python
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`](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/backtest/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.

**Figure 1.** *GC.v.0 top of book around the cycle that opened with a short
entry near 09:26 and exited near 09:31, then re-entered long until 09:35.
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 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:
```bash
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](../integrations/architect_ax.md) 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`](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/live/architect_ax/ax_book_imbalance.py).
For connection setup and API key configuration, see the
[AX Exchange integration guide](../integrations/architect_ax.md).
## Further reading
- [`OrderBookImbalance` strategy source](https://github.com/nautechsystems/nautilus_trader/blob/develop/examples/live/architect_ax/strategies.py)
- [Mean Reversion with Proxy FX Data tutorial](fx_mean_reversion_ax.md)
- [Architect Exchange documentation](https://docs.architect.exchange/)
- [Databento: HFT signals with sklearn](https://databento.com/blog/hft-sklearn-python)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.