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.
One keyword, and Deribit’s own margin formula prices every short option.
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 CashInverseThe 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 optionsThe 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-marginedDeribit'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")
)| Leg | Action | Fill | At expiry |
|---|---|---|---|
| 100k call | bought | 0.063500 BTC | settles 0.070317 BTC, in the money |
| 110k call | sold | 0.034000 BTC | settles 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.