Backtesting an Option Purchase Through Expiration and Exercise
Summary
This example demonstrates how to model an option purchase across expiration in a backtest. A strategy subscribes to option quotes and futures bars, then submits a market buy order for one option when it receives the first eligible quote. The sample replays bundled market data for a futures contract and a call option, with futures bar closing prices converted into trade ticks to provide underlying-price observations used in determining exercise.
The example configures a simulated margin account and venue, loads instrument definitions and quote, bar, and trade data, runs the engine, and prints account, fill, and position reports. It illustrates the mechanics of exercising an expiring option in a historical simulation, rather than presenting a trading signal or tested strategy result. The sample is limited to one option purchase and bundled data; it gives no analysis of alternative exercise policies, broader market conditions, or performance beyond the generated reports.
Key ideas
- The example buys one option on the first eligible quote and holds it through expiration.
- Futures bar closes are converted to trade ticks to supply underlying-price observations for exercise handling.
- The backtest loads option and futures definitions, quotes, and bars into a simulated margin account.
- Account, order-fill, and position reports are printed after the replay.
- The sample demonstrates mechanics and provides no comparative strategy or performance analysis.
Tags
Full text
# databento_option_exercise.py
```py
"""
Example of databento option exercise.
"""
# ---
# 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]
# # Option exercise at expiry
#
# Replay the bundled Databento option and futures samples across expiry. The
# strategy buys one option before expiry. Futures bar closes are converted to
# trade ticks that supply the underlying price used to determine exercise.
# %%
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 AggressorSide
from nautilus_trader.model import BarType
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 TradeId
from nautilus_trader.model import TraderId
from nautilus_trader.model import TradeTick
from nautilus_trader.model import Venue
from nautilus_trader.trading import Strategy
from nautilus_trader.trading import StrategyConfig
class OptionExerciseConfig(StrategyConfig):
"""
Collect option exercise config tests.
"""
def __init__(
self,
*,
future_id: InstrumentId,
option_id: InstrumentId,
**_kwargs: object,
) -> None:
"""
Initialize the instance.
"""
super().__init__()
self.future_id = future_id
self.option_id = option_id
class OptionExerciseStrategy(Strategy):
"""
Collect option exercise strategy tests.
"""
def __init__(self, config: OptionExerciseConfig) -> None:
"""
Initialize the instance.
"""
super().__init__(config)
self._option_id = config.option_id
self.order_submitted = False
self.bar_type = BarType.from_str(f"{config.future_id}-1-MINUTE-LAST-EXTERNAL")
def on_start(self) -> None:
"""
On start.
"""
self.subscribe_quotes(self._option_id)
self.subscribe_bars(self.bar_type)
def on_quote(self, quote: QuoteTick) -> None:
"""
On quote.
"""
if quote.instrument_id != self._option_id or self.order_submitted:
return
order = self.order_factory.market(
instrument_id=self._option_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" / "options_exercise" / "databento"
loader = DatabentoDataLoader(
repo_root / "crates" / "adapters" / "databento" / "publishers.json",
)
futures = loader.load_instruments(
data_dir / "futures_definition.dbn.zst",
use_exchange_as_venue=True,
)
options = loader.load_instruments(
data_dir / "options_definition.dbn.zst",
use_exchange_as_venue=True,
)
bars = loader.load_bars(data_dir / "futures_ohlcv-1m_2026-01-09T20-55_2026-01-09T21-05.dbn.zst")
quotes = loader.load_bbo_quotes(
data_dir / "options_bbo-1m_2026-01-09T20-55_2026-01-09T21-05.dbn.zst",
)
trades = [
TradeTick(
instrument_id=bar.bar_type.instrument_id,
price=bar.close,
size=Quantity.from_int(1),
aggressor_side=AggressorSide.NO_AGGRESSOR,
trade_id=TradeId(f"BAR-{index}"),
ts_event=bar.ts_event,
ts_init=bar.ts_init,
)
for index, bar in enumerate(bars)
]
future_id = InstrumentId.from_str("ESH6.XCME")
option_id = InstrumentId.from_str("EW2F6 C7000.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 futures + options:
engine.add_instrument(instrument)
engine.add_data(quotes + bars + trades)
engine.add_strategy(
OptionExerciseStrategy(
OptionExerciseConfig(future_id=future_id, option_id=option_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.