NautilusTrader
ConceptsBacktesting

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.

Trade-Based Execution

Trade ticks trigger matching by default when a venue has trade_execution=True. A trade provides evidence that liquidity traded at its price, so it can fill resting orders on the passive side.

Set trade_execution=False to use trades as strategy data without treating them as execution liquidity for ordinary resting orders:

from decimal import Decimal

from nautilus_trader.config import BacktestVenueConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.model import AccountType
from nautilus_trader.model import BookType
from nautilus_trader.model import OmsType

venue = BacktestVenueConfig(
    name="SIM",
    oms_type=OmsType.NETTING,
    account_type=AccountType.CASH,
    book_type=BookType.L1_MBP,
    starting_balances=["100_000 USD"],
    trade_execution=False,
    fee_model=MakerTakerFeeModel(
        maker_rate=Decimal("0"),
        taker_rate=Decimal("0"),
    ),
)

When trade execution is disabled, behavior depends on the venue's book type:

  • With L1 data, accepted trade ticks update the L1 book but skip matching and maintenance. Later quote ticks or executable bars drive that work.
  • With L2 or L3 data, accepted trade ticks advance LastPrice and run trailing-stop maintenance for all trigger types. They can trigger LastPrice stop orders, which fill against existing book liquidity. The tick does not match resting limits or trigger stop orders that use other trigger types. It also runs enabled GTD expiry and instrument-expiration checks.

Trade-driven matching

The engine temporarily moves its matching references to the trade price:

  • A SELL trade can match resting BUY orders.
  • A BUY trade can match resting SELL orders.
  • A NO_AGGRESSOR trade can affect both sides because the passive side is unknown.

L1 trades update both simulated top-of-book levels to the trade price and size. L2 and L3 depth books remain unchanged; only the matching core's transient bid, ask, and last prices move for the iteration.

Fill determination

When a trade triggers a limit fill:

  • With L1 data, the engine uses the trade's volume even when the simulated book contains the trade price. Resting maker orders fill at their limit price. Taker orders use the trade price when it satisfies their limit; otherwise, they retain the limit-price fallback.
  • With L2 or L3 data, the engine fills against crossed book levels. If the book does not represent the trade price, it can create a trade-driven fill at the order's limit price.
  • A trade-driven fill is capped at min(order.leaves_qty, trade.size).

With liquidity_consumption=False, the same trade size can support more than one order during an iteration. With liquidity_consumption=True, trade-driven fills share a consumption counter, so their total cannot exceed the unconsumed trade size. Each L1 trade has a fresh budget, including successive trades with the same price and size. Once that budget is exhausted, L1 fills do not fall back to book liquidity.

For example, with L2 or L3 data, a SELL trade at 100.00 can fill a BUY LIMIT at 100.05. If no book level represents that fill, the engine uses 100.05 rather than granting the better trade price.

Matching-state restoration

After the iteration, the engine restores matching references from the available market baseline:

  • With L2 or L3 data, the depth book remains the independent source of bid and ask state.
  • With an L1 quote baseline, the non-aggressor side is restored from the latest quote.
  • With trade-only L1 data, there is no quote baseline to restore, so the latest trade continues to define the available top-of-book state.

This distinction matters when interpreting a stream of trades without quotes. Repeated trades can move the simulated L1 state, but quote-backed L1 matching does not progressively discard the non-aggressor side of the latest quote.

Aggressor sides

The aggressor is the participant that crossed the spread:

  • SELL: A seller hit the bid. The trade can fill a resting BUY order.
  • BUY: A buyer lifted the ask. The trade can fill a resting SELL order.
  • NO_AGGRESSOR: The data does not identify the aggressor. The engine considers both sides where the feature requires a side.

A trade with aggressor side BUY provides evidence for passive SELL orders, not BUY orders. A trade with aggressor side SELL provides evidence for passive BUY orders, not SELL orders.

Combining book and trade data

Book updates establish the spread and visible depth. Trade ticks provide execution evidence between those updates. This is useful when depth snapshots are throttled and a trade occurs at a price that the latest snapshot does not contain.

Use the two feeds with care:

  • A trade must have the opposite aggressor side to fill a resting order.
  • A book update can cross an order independently of a trade.
  • A fill at a missing trade-price level uses the trade-driven quantity cap.
  • With consumption enabled, the engine accounts for trade volume already removed from an L2 or L3 book before triggered orders consume the remaining depth.

Queue position tracking

Set queue_position=True with trade_execution=True to track displayed quantity ahead of each LIMIT order:

from decimal import Decimal

from nautilus_trader.execution import MakerTakerFeeModel

venue = BacktestVenueConfig(
    name="SIM",
    oms_type=OmsType.NETTING,
    account_type=AccountType.MARGIN,
    book_type=BookType.L2_MBP,
    starting_balances=["100_000 USD"],
    trade_execution=True,
    queue_position=True,
    fee_model=MakerTakerFeeModel(
        maker_rate=Decimal("0"),
        taker_rate=Decimal("0"),
    ),
)

Sandbox paper trading uses the same matching-engine flags. Pass them on SandboxExecutionClientConfig (defaults remain off, matching current sandbox behavior):

from decimal import Decimal

from nautilus_trader.adapters.sandbox import SandboxExecutionClientConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.model import BookType
from nautilus_trader.model import Money
from nautilus_trader.model import Venue

config = SandboxExecutionClientConfig(
    venue=Venue("BINANCE"),
    starting_balances=[Money.from_str("10_000 USDT")],
    book_type=BookType.L2_MBP,
    trade_execution=True,
    queue_position=True,
    liquidity_consumption=True,
    fee_model=MakerTakerFeeModel(
        maker_rate=Decimal("0.001"),
        taker_rate=Decimal("0.001"),
    ),
)

The sandbox venue must match the data client's instrument venue, and the strategy must subscribe to trades (and L2/L3 deltas when using depth).

Queue lifecycle

  1. On acceptance, a LIMIT order snapshots same-side displayed size at its price.
  2. Correct-side trades at that price reduce the quantity ahead.
  3. The order becomes fill-eligible when the quantity ahead reaches zero.
  4. Only trade volume beyond the cleared queue is available to fill on that tick.

For example:

  1. The bid at 100.00 contains 100 units.
  2. A BUY LIMIT for 50 units joins with 100 units ahead.
  3. A SELL trade for 80 units reduces the queue ahead to 20.
  4. A SELL trade for 30 units clears the queue and leaves 10 units available to fill.
  5. The next correct-side trade can fill the remaining order quantity.

Book changes

For L2 books and aggregate L3 updates:

  • A DELETE clears the price level and its queue.
  • An UPDATE caps quantity ahead at the level's new displayed size.
  • A completed book snapshot rebases each tracked queue position against the new visible quantity at its price: quantity ahead is capped at the snapshot size, while newly added liquidity does not move an existing simulated order further back. Snapshot batches may start with a F_SNAPSHOT clear and finish with a later F_LAST delta.
  • A BookDepth replacement applies the same rebase rule after the full depth replacement.

For L3 MBO books:

  • A per-order DELETE advances the queue by that order's remaining tracked size.
  • A size decrease advances the queue by the difference.
  • A size increase keeps the larger order ahead.
  • A price change removes the book order from the tracked queue.
  • A completed book snapshot retains only surviving tracked order IDs ahead, each capped at its previous quantity.

Changing a simulated order's price resets its queue position at the new level. A quantity-only change retains the progress already made.

L1 queue tracking

With BookType.L1_MBP, trade ticks reduce quantity ahead while quotes provide price-move and displayed-size evidence:

  • A move away through the order's price clears the queue.
  • A move toward the order preserves the queue.
  • A return to a previously visible level caps quantity ahead at the new displayed size.
  • A quote at the order's price caps quantity ahead at the same-side displayed size.
  • A displayed-size increase preserves queue progress.
  • An order behind the BBO remains pending until a quote reaches its price or a trade crosses it.

Limitations

  • Queue tracking applies only to LIMIT orders.
  • Each simulated order has an independent queue estimate.
  • The initial estimate is limited to book state visible at acceptance.
  • Historical data cannot reveal hidden orders or every venue-specific priority rule.

Unknown aggressor side

NO_AGGRESSOR trades reduce queues on both sides. This can clear a queue and fill an order earlier than reality, so it is optimistic from the strategy's execution perspective.

On this page