Files

102 lines
4.0 KiB
Python
Raw Permalink Normal View History

2026-08-23 13:31:37 +00:00
"""Option strategy -- a bull call spread, held to expiration.
Demonstrates:
- A two-leg option structure: long a low strike, short a higher one
- Per-leg sizing with col("symbol_id")
- A SHORT option paying margin under the venue's own formula
- Both legs cash-settled at expiry, which is what caps the payoff
The structure: buy the 100k call, sell the 110k call, same expiration. The
short leg pays for part of the long one, and in exchange it caps the gain at
the distance between the strikes. Classic, and the cheapest way to see the
engine settle two contracts on the same day with different outcomes.
**A currency trap worth knowing.** Every leg of a strategy has to be quoted in
the same currency, because the engine carries one cash balance. On Deribit an
option is quoted in BTC, but `BTC-PERPETUAL` is quoted in USD. So a covered
call (long the perpetual, short a call) would add dollars to bitcoin in a single
number and produce a meaningless equity curve. A spread has both legs in BTC,
which is why this example is a spread. The perpetual appears below only as the
settlement reference, never as a position.
Data: self-contained (network) — ingested on each run from a free connector
Usage:
python examples/24_option_spread.py
"""
import os
import tempfile
import manifoldbt as mbt
from manifoldbt.indicators import col
from manifoldbt.helpers import time_range, Interval
# Both legs expired on 2025-06-27, so the whole life of the trade is history.
UNDERLYING, UNDERLYING_ID = "BTC-PERPETUAL", 1
LONG_LEG, LONG_ID = "BTC-27JUN25-100000-C", 2
SHORT_LEG, SHORT_ID = "BTC-27JUN25-110000-C", 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"),
)
store = mbt.ingest(
provider="deribit", symbol=UNDERLYING, symbol_id=UNDERLYING_ID,
asset_class="crypto_perp", **common
)
for symbol, symbol_id in ((LONG_LEG, LONG_ID), (SHORT_LEG, SHORT_ID)):
store = mbt.ingest(
provider="deribit", symbol=symbol, symbol_id=symbol_id,
asset_class="option", **common
)
# -- The strategy --------------------------------------------------------------
# Legs are told apart by symbol id. Never discriminate on price level: a premium
# crossing the threshold would flip its own leg to zero and close the position.
size = (
mbt.when(col("symbol_id") == float(LONG_ID), 1.0, 0.0) # buy the 100k call
+ mbt.when(col("symbol_id") == float(SHORT_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")
)
start, end = time_range("2025-05-01", "2025-07-01")
config = mbt.BacktestConfig(
universe=[UNDERLYING_ID, LONG_ID, SHORT_ID],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.days(1),
initial_capital=10.0, # 10 BTC: everything here is in BTC
currency="BTC",
option_underlyings={LONG_ID: UNDERLYING_ID, SHORT_ID: UNDERLYING_ID},
option_margin_model="deribit", # the short leg posts 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()
names = {LONG_ID: "long 100k", SHORT_ID: "short 110k"}
print("\nTrades, by leg:")
for _, t in trades[trades.symbol_id != UNDERLYING_ID].iterrows():
what = "settled" if t.exit_reason == 5 else "traded"
print(f" {names[t.symbol_id]:<12} {what:<8} {t.quantity:>4.1f} @ {t.fill_price:.6f} BTC")
equity = float(result.equity_curve[-1])
print(f"\nFinal equity: {equity:.6f} BTC ({equity - 10.0:+.6f})")
# The short leg expiring worthless is what the spread pays for: it financed
# part of the long call, and capped the gain at the strike distance.
if result.warnings:
print("Warnings:", result.warnings)