mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
release: v0.19.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""Crypto options -- Deribit contracts that actually expire.
|
||||
|
||||
Demonstrates:
|
||||
- mbt.ingest(provider="deribit", ...) -- no API key, expired contracts included
|
||||
- Contract terms (strike, expiry, side, settlement) recorded alongside the bars
|
||||
- Cash settlement at expiration, at intrinsic value
|
||||
- config.option_underlyings -- which price series settles the contract
|
||||
- Per-leg sizing with col("symbol_id"), for multi-leg structures
|
||||
|
||||
Why Deribit and not Binance: Deribit serves the history of contracts that have
|
||||
already expired, which is the only data an option backtest can run on. Binance's
|
||||
options API answers HTTP 400 for anything past its expiration date.
|
||||
|
||||
Three things worth knowing before reading the numbers:
|
||||
|
||||
- **Everything is in BTC.** A Deribit BTC option is quoted, margined and
|
||||
settled in BTC, so `initial_capital` below is 10 BTC, not 10 dollars. The
|
||||
payoff of a call is `max(0, S - K) / S` BTC per contract.
|
||||
|
||||
- **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 here; the basis between the two is small
|
||||
but real, and it is not modelled.
|
||||
|
||||
- **Positions are counted in units of the underlying.** On Deribit a contract
|
||||
is one unit, so the two are the same thing. On a 100-multiplier listed
|
||||
option, holding one contract means a position of 100.
|
||||
|
||||
Data: self-contained (network) — ingested on each run from a free connector
|
||||
|
||||
Usage:
|
||||
python examples/23_deribit_options.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import manifoldbt as mbt
|
||||
from manifoldbt.indicators import col
|
||||
from manifoldbt.helpers import time_range, Interval
|
||||
|
||||
# Two contracts that expired on 2025-06-27, and the series that settles them.
|
||||
UNDERLYING, UNDERLYING_ID = "BTC-PERPETUAL", 1
|
||||
CALL_100K, CALL_ID = "BTC-27JUN25-100000-C", 2
|
||||
PUT_90K, PUT_ID = "BTC-27JUN25-90000-P", 3
|
||||
START, END = "2025-05-01T00:00:00Z", "2025-07-01T00:00:00Z"
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
common = dict(
|
||||
start=START,
|
||||
end=END,
|
||||
interval="1d",
|
||||
data_root=os.path.join(tmp, "data"),
|
||||
metadata_db=os.path.join(tmp, "meta.sqlite"),
|
||||
)
|
||||
|
||||
# -- 1. The settlement reference, then the contracts ---------------------------
|
||||
store = mbt.ingest(
|
||||
provider="deribit", symbol=UNDERLYING, symbol_id=UNDERLYING_ID,
|
||||
asset_class="crypto_perp", **common
|
||||
)
|
||||
store = mbt.ingest(
|
||||
provider="deribit", symbol=CALL_100K, symbol_id=CALL_ID,
|
||||
asset_class="option", **common
|
||||
)
|
||||
store = mbt.ingest(
|
||||
provider="deribit", symbol=PUT_90K, symbol_id=PUT_ID,
|
||||
asset_class="option", **common
|
||||
)
|
||||
|
||||
# The connector asked Deribit for the terms and the store kept them; nothing
|
||||
# below is inferred from the instrument name.
|
||||
for symbol_id, terms in sorted(store.option_contracts().items()):
|
||||
print(f"{symbol_id}: {terms['option_type']} {terms['strike']:.0f}, "
|
||||
f"settles {terms['settlement']}")
|
||||
|
||||
# -- 2. A risk reversal: long the 100k call, short the 90k put -----------------
|
||||
# Legs are told apart by `col("symbol_id")`. Do NOT discriminate on price level
|
||||
# (e.g. "close < 100"): a premium that crosses the threshold silently flips the
|
||||
# leg to zero and the strategy closes its own position.
|
||||
size = (
|
||||
mbt.when(col("symbol_id") == float(CALL_ID), 1.0, 0.0)
|
||||
+ mbt.when(col("symbol_id") == float(PUT_ID), -1.0, 0.0)
|
||||
)
|
||||
strategy = (
|
||||
mbt.Strategy.create("risk_reversal")
|
||||
.signal("leg", col("symbol_id"))
|
||||
.size(size)
|
||||
.describe("Long the 100k call, short the 90k put, both held to expiration")
|
||||
)
|
||||
|
||||
start, end = time_range("2025-05-01", "2025-07-01")
|
||||
config = mbt.BacktestConfig(
|
||||
universe=[UNDERLYING_ID, CALL_ID, PUT_ID],
|
||||
time_range_start=start,
|
||||
time_range_end=end,
|
||||
bar_interval=Interval.days(1),
|
||||
initial_capital=10.0, # 10 BTC
|
||||
currency="BTC",
|
||||
option_underlyings={CALL_ID: UNDERLYING_ID, PUT_ID: UNDERLYING_ID},
|
||||
option_margin_model="deribit", # the short put pays margin
|
||||
execution=mbt.ExecutionConfig(position_sizing_mode="Units", allow_short=True),
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = mbt.run(strategy, config, store)
|
||||
trades = result.trades.to_pandas()
|
||||
|
||||
print("\nTrades on the option legs:")
|
||||
print(
|
||||
trades[trades.symbol_id != UNDERLYING_ID][
|
||||
["symbol_id", "side", "quantity", "fill_price", "exit_reason"]
|
||||
].to_string(index=False)
|
||||
)
|
||||
# exit_reason 5 is OptionExpiry: the venue settled the contract, the
|
||||
# strategy did not sell it. The put settles at 0 because BTC finished far
|
||||
# above its 90k strike.
|
||||
print("\nFinal equity: %.6f BTC" % float(result.equity_curve[-1]))
|
||||
if result.warnings:
|
||||
print("Warnings:", result.warnings)
|
||||
Reference in New Issue
Block a user