OptionsSettlementMargin

A contract, not a price series

An option carries its strike, expiry, size and settlement style, cash-settles at intrinsic value on schedule, and posts margin under a named model. Manifold-BT’s engine models all of it, proven end to end by a real multi-leg spread, both legs settled on live Deribit data.

>>>mbt.BacktestConfig(option_margin_model="deribit")

One keyword, and Deribit’s own margin formula prices every short option.

2 styles
settlement styles: CashLinear for listed options, CashInverse for Deribit's coin-margined contracts
10 → 10.0408 BTC
a real bull call spread, both legs cash-settled at expiry, on live Deribit data
Multi-leg spreads
long and short legs of the same strategy, tracked and settled independently

The contract, not the price

An option carries an underlying, a direction, a strike, an expiry, a size and a settlement style. The connector records all of it alongside the bars, nothing is inferred from the instrument’s name.

import manifoldbt as mbt

store = mbt.ingest(
    provider="deribit", symbol="BTC-27JUN25-100000-C",
    symbol_id=2, asset_class="option",
    start="2025-05-01T00:00:00Z", end="2025-07-01T00:00:00Z", interval="1d",
)

# The connector asked Deribit for the terms; nothing here is inferred
# from the instrument name.
for symbol_id, terms in store.option_contracts().items():
    print(symbol_id, terms["option_type"], terms["strike"], terms["settlement"])
# 2 call 100000 CashInverse

The general engine loop settles before it values or trades anything else on a bar: a contract that has expired cash-settles at intrinsic value instead of staying marked at its last quote forever.

The underlying has to be in the backtest universe, or the run fails outright. Settling an option against its own stale last price, on an illiquid strike days old, is exactly the kind of silently wrong number a backtest shouldn’t be allowed to produce.

Two ways to settle

Same contract shape, different currency for everything: the quote, the margin, and the payout.

CashLinear

listed options

The payoff is denominated in the contract's own quote currency: a strike, a multiplier, cash settlement at intrinsic value, the way an equity, index or futures option settles today. Databento and Massive attach these terms to the bars they ingest for CME, NASDAQ and OPRA symbols.

CashInverse

Deribit, coin-margined

Deribit's own convention: a BTC option is quoted, margined and settled in BTC, not dollars. A call pays max(0, S − K) / S in the underlying itself. Same contract shape as CashLinear, with the currency of everything, quote, margin and payout, rotated onto the coin.

Margin, named and off by default

Selling an option doesn’t pay cash, it consumes margin. option_margin_model="deribit" implements that venue’s own published formula, not a number invented for a demo. Initial margin is required before a short can open, and a margin call closes the heaviest positions first once maintenance margin exceeds equity.

It defaults to None: turning it on is one keyword, and no backtest that never sells an option changes its result by adding it.

config = mbt.BacktestConfig(
    universe=[UNDERLYING_ID, LONG_CALL_ID, SHORT_CALL_ID],
    initial_capital=10.0,                 # 10 BTC, not 10 dollars
    currency="BTC",
    option_underlyings={LONG_CALL_ID: UNDERLYING_ID, SHORT_CALL_ID: UNDERLYING_ID},
    option_margin_model="deribit",        # the short leg posts margin
    execution=mbt.ExecutionConfig(position_sizing_mode="Units", allow_short=True),
)

The currency trap

The engine carries one cash balance, so every leg of a strategy has to share a currency. On Deribit an option is quoted in BTC, but a perpetual future on the same underlying is quoted in USD. A covered call, long the perpetual, short a call, would add dollars to bitcoin inside a single number, and the equity curve that comes out means nothing.

A spread, both legs BTC-quoted, has no such problem, which is why the worked example below is a spread and not a covered call.

Worked example: a bull call spread held to expiry

Long the 100k call, short the 110k call, same expiry, both on Deribit, both in BTC. The short leg finances part of the long one and caps the gain at the distance between the strikes, which the settlement of both contracts on the same day, with different outcomes, makes visible directly.

from manifoldbt.indicators import col

# Legs are told apart by symbol_id, never by price level: a premium
# crossing a threshold would otherwise flip a leg's own sign.
size = (
    mbt.when(col("symbol_id") == float(LONG_CALL_ID), 1.0, 0.0)      # buy the 100k call
    + mbt.when(col("symbol_id") == float(SHORT_CALL_ID), -1.0, 0.0)  # sell the 110k call
)
strategy = (
    mbt.Strategy.create("bull_call_spread")
    .signal("leg", col("symbol_id"))
    .size(size)
    .describe("Long the 100k call, short the 110k call, held to expiration")
)
LegActionFillAt expiry
100k callbought0.063500 BTCsettles 0.070317 BTC, in the money
110k callsold0.034000 BTCsettles 0, out of the money

Final equity: 10.000000 → 10.040817 BTC (+0.040817).

Both legs are told apart by symbol_id, never by price level. A premium crossing a threshold would otherwise flip a leg’s own sign and the strategy would close its own position without meaning to.

BTC finished at 107,563 on expiry day: below the 110k strike, so the short call settles at zero, exactly what it is supposed to do. The 100k call settles in the money and pays the difference. Neither leg was sold before expiry, both simply settled, which is the mechanism this page is about, not a special case.

What this doesn’t do yet

Three honest limits, worth knowing before the mechanism above surprises anyone.

No fast kernel, no GPU

The fast-lite and GPU kernels don’t track expiry or margin state, so they would keep an expired contract marked at its last quoted price: a wrong number, not a slow one. Options run on the general engine only, so a sweep over an option strategy doesn’t get the GPU speedup a linear one does.

You choose the settlement reference

Deribit settles against its own BTC_USD index, whose ticker matches no series you can ingest. BTC-PERPETUAL stands in for it in the example above; the basis between the two is small but real, and it isn’t modeled.

Positions are counted in units

On Deribit a contract is one unit of the underlying, so the two are the same thing. On a 100-multiplier listed option, holding one contract is a position of 100: get the multiplier wrong and the P&L is off by it, not the strategy.

Why Deribit, and not Binance

An option backtest has to hold a contract to expiry, which means it needs the history of contracts that have already expired. Binance’s options API answers HTTP 400 for anything past its expiration date. Deribit serves that history, needs no API key, and is the only venue in the product that does. Databento and Massive cover the listed side, equities, indices and futures options, on Pro.

FAQ

Settle a contract instead of quoting one forever

Options are in the free version. Deribit needs no API key, so the example above runs end to end from a fresh install.