Skip to content
All library documents

Backtesting Futures Settlement Across an Expiry

Code NautilusTrader

Summary

This example demonstrates how to model a futures contract reaching expiry in a backtest. It loads instrument definitions and best bid and offer quote data for two consecutive E-mini S&P 500 contracts, then submits a market buy for one expiring-contract unit when that contract first receives a quote. Quotes for the next contract continue advancing the simulation beyond settlement.

The example configures a margin account, a netting order management system, and zero maker and taker fees, then reports account balances, fills, and positions after the run. It illustrates a way to exercise an engine’s expiry settlement handling with bundled market data. It does not state the resulting settlement value or provide a comparison with exchange settlement records, and its simplified single-order setup does not evaluate a trading strategy’s profitability or broader expiry risks.

Key ideas

  • A backtest can place a position in an expiring futures contract and advance time using quotes from the next contract.
  • The sample submits a single market buy when the expiring contract first receives a quote.
  • Account, fill, and position reports provide outputs for inspecting settlement behavior.
  • The example uses zero fees and does not report or independently validate settlement results.

Tags

Full text
# databento_futures_settlement.py


```py
"""
Example of databento futures settlement.
"""

# ---
# jupyter:
#   jupytext:
#     formats: py:percent
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.18.1
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---


# %% [markdown]
# # Futures settlement at expiry
#
# Replay the bundled Databento BBO sample across the ESZ5 expiry. The strategy
# opens one ESZ5 contract before expiry while ESH6 quotes advance the clock past
# settlement.

# %%
from decimal import Decimal
from pathlib import Path

import pandas as pd

from nautilus_trader.adapters.databento import DatabentoDataLoader
from nautilus_trader.backtest import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.model import AccountType
from nautilus_trader.model import Currency
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import Money
from nautilus_trader.model import OmsType
from nautilus_trader.model import OrderSide
from nautilus_trader.model import Quantity
from nautilus_trader.model import QuoteTick
from nautilus_trader.model import TraderId
from nautilus_trader.model import Venue
from nautilus_trader.trading import Strategy
from nautilus_trader.trading import StrategyConfig


class FuturesSettlementConfig(StrategyConfig):
    """
    Collect futures settlement config tests.
    """

    def __init__(
        self,
        *,
        future_id: InstrumentId,
        next_future_id: InstrumentId,
        **_kwargs: object,
    ) -> None:
        """
        Initialize the instance.
        """
        super().__init__()
        self.future_id = future_id
        self.next_future_id = next_future_id


class FuturesSettlementStrategy(Strategy):
    """
    Collect futures settlement strategy tests.
    """

    def __init__(self, config: FuturesSettlementConfig) -> None:
        """
        Initialize the instance.
        """
        super().__init__(config)
        self._future_id = config.future_id
        self._next_future_id = config.next_future_id
        self.order_submitted = False

    def on_start(self) -> None:
        """
        On start.
        """
        self.subscribe_quotes(self._future_id)
        self.subscribe_quotes(self._next_future_id)

    def on_quote(self, quote: QuoteTick) -> None:
        """
        On quote.
        """
        if quote.instrument_id != self._future_id or self.order_submitted:
            return

        order = self.order_factory.market(
            instrument_id=self._future_id,
            order_side=OrderSide.BUY,
            quantity=Quantity.from_int(1),
        )
        self.submit_order(order)
        self.order_submitted = True


# %%
if __name__ == "__main__":
    repo_root = Path(__file__).resolve().parents[3]
    data_dir = repo_root / "test_data" / "databento" / "futures_settlement" / "databento"
    loader = DatabentoDataLoader(
        repo_root / "crates" / "adapters" / "databento" / "publishers.json",
    )

    instruments = loader.load_instruments(
        data_dir / "futures_settlement_definition.dbn.zst",
        use_exchange_as_venue=True,
    )
    quotes = loader.load_bbo_quotes(
        data_dir / "futures_settlement_bbo-1m_2025-12-19T14-25-00_2025-12-19T14-35-00.dbn.zst",
    )

    future_id = InstrumentId.from_str("ESZ5.XCME")
    next_future_id = InstrumentId.from_str("ESH6.XCME")
    engine = BacktestEngine(
        BacktestEngineConfig(trader_id=TraderId.from_str("BACKTESTER-001")),
    )
    XCME = Venue("XCME")
    USD = Currency.from_str("USD")
    engine.add_venue(
        venue=XCME,
        oms_type=OmsType.NETTING,
        account_type=AccountType.MARGIN,
        base_currency=USD,
        starting_balances=[Money(1_000_000, USD)],
        fee_model=MakerTakerFeeModel(
            maker_rate=Decimal(0),
            taker_rate=Decimal(0),
        ),
    )

    for instrument in instruments:
        engine.add_instrument(instrument)
    engine.add_data(quotes)
    engine.add_strategy(
        FuturesSettlementStrategy(
            FuturesSettlementConfig(
                future_id=future_id,
                next_future_id=next_future_id,
            ),
        ),
    )
    engine.run()

    with pd.option_context("display.max_columns", None, "display.width", 300):
        print(engine.generate_account_report(XCME))
        print(engine.generate_order_fills_report())
        print(engine.generate_positions_report())

    engine.reset()
    engine.dispose()

```

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.