Know it before you buy it.
This is the exact Parquet schema you receive - generated from the definition the collector writes against, so it cannot drift from the files themselves. Null rates are measured from a real recent file, not estimated.
| Column | Type | Null | Null rate | Zero rate | Note |
|---|---|---|---|---|---|
| timestamp | timestamp[us, tz=UTC] | required | 0.0% | - | snapshot instant, UTC |
| underlying | string | required | 0.0% | - | |
| instrument | string | required | 0.0% | - | Deribit instrument name, e.g. ETH-25DEC26-1800-C |
| strike | double | required | 0.0% | 0.0% | |
| expiry | timestamp[us, tz=UTC] | required | 0.0% | - | explicit column - never parse it out of the instrument name. Expired contracts persist in the chain briefly after expiry (up to ~10 min observed), so filter expiry > timestamp |
| type | string | required | 0.0% | - | |
| mark_iv | double | nullable | 0.0% | 0.0% | decimal, not percent: 0.65 means 65% |
| bid_iv | double | nullable | 25.47% | 0.0% | null where no IV solves - mostly deep ITM rows WITH a live bid, below intrinsic; not an illiquidity marker |
| ask_iv | double | nullable | 0.06% | 0.0% | null where no IV solves for the ask side |
| mark_price | double | nullable | 0.0% | 4.01% | 0 where the exchange published no mark |
| bid_price | double | nullable | 0.0% | 5.41% | 0 means no bid at that instant - not null. Filter > 0 |
| ask_price | double | nullable | 0.0% | 0.06% | 0 means no ask at that instant - not null. Filter > 0 |
| underlying_price | double | nullable | 0.0% | 0.0% | |
| open_interest | double | nullable | 0.0% | 21.76% | |
| volume_24h | double | nullable | 0.0% | 59.76% | |
| delta | double | nullable | 0.01% | 1.14% | exchange-published, not re-derived by us |
| gamma | double | nullable | 0.01% | 5.33% | |
| vega | double | nullable | 0.01% | 1.63% | |
| theta | double | nullable | 0.01% | 1.27% | |
| rho | double | nullable | 0.01% | 1.78% | |
| source | string | required | 0.0% | - | capture provenance. Live capture is live_ws, with occasional live_rest snapshots when the socket stalls (~0.01% of rows); reconstructed rows are modeled_surface |
| settlement_currency | string | nullable | 0.0% | - | currency Greeks and mark_price are expressed in |
| quote_currency | string | nullable | 0.0% | - | currency strikes are quoted in |
Null rates measured from 2026-10-24.parquet (1,046,338 rows).
Conventions worth knowing
Implied volatilities are decimals, not percentages - 0.65
means 65%. Greeks are decimals too: delta = 0.5. Both are
the exchange's own published values, captured at the snapshot instant
and never re-derived by us.
Deribit publishes 0, not null, when nothing is quoted at
that instant. We pass it through unchanged rather than normalising it,
because collapsing the two would destroy a real distinction and silently
change data customers have already reconciled.
So a price column can read as fully populated while a material share of
its rows carry no market. Filter
bid_price > 0 and ask_price > 0 before
computing a mid - a naive (bid+ask)/2 is wrong on
roughly 9% of rows, and rows that look like crossed quotes are almost
always an unquoted side rather than bad data. The zero-rate column above
is measured, not estimated.
On a small share of live-captured rows the exchange's
mark_price is marginally below the option's intrinsic value
computed from the underlying_price reported in the same
message. Measured across 32,441,980 rows on the 28 days where it occurs:
11,776 rows, 0.036%, of which
11,774 are puts. It concentrates in very illiquid deep in-the-money,
long-dated strikes, where a mark priced moments earlier is paired with a
forward that has since moved - the mark is consistent with a forward
about 1.7% higher.
mark_iv is the sound field on those rows: re-pricing at the
row's own stored IV clears intrinsic on
11,776 of 11,776. We do not
rewrite the exchange's mark, for the same reason we pass zeros through -
it is an observation, and correcting it would put our archive at odds
with Deribit's own record, which is what customers reconcile against.
If you need a no-arbitrage mark on deep
ITM strikes, derive it from mark_iv rather than
reading mark_price.
This is distinct from the 2021-2024 historical archive, where
mark_price was our derivation from vendor quotes
and was wrong. That was repaired on 2026-10-08.
A null means the field was absent from the exchange message altogether,
or the row pre-dates the column's introduction. On the IV columns it also
marks strikes the exchange quoted no volatility for - which is why
bid_iv carries a much higher null rate than
mark_iv. We never interpolate a value nobody quoted.
All timestamps are UTC, microsecond precision. Files partition on the
UTC day. expiry is an explicit column - you never have to
parse it out of the instrument name.
Every row carries a source tag recording how it was
captured, so live capture and the openly-modeled bridge are always
distinguishable in your own data. See methodology.
Loading it
import pandas as pd
df = pd.read_parquet("ETH-2026-07-14.parquet")
df[df.type == "call"].groupby("expiry").mark_iv.mean()
import duckdb
duckdb.sql("""
SELECT expiry, avg(mark_iv)
FROM 'ETH-*.parquet'
WHERE type = 'call'
GROUP BY expiry
""")