[V5] openbb-deribit: Refactor provider for V5 (#7674)

* refactor openbb-deribit for v5

* restructure the optimizer around the published combos data and add integration test suites
This commit is contained in:
Danglewood
2026-09-23 13:02:49 -07:00
committed by GitHub
parent d1f71e670c
commit d6fd646402
124 changed files with 32530 additions and 5074 deletions

View File

@@ -0,0 +1,165 @@
name: 📦 publish openbb-deribit
on:
pull_request:
types: [closed]
branches: [develop, v5]
workflow_dispatch:
permissions:
contents: read
jobs:
version-check:
name: Verify version bump
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/openbb-deribit-v')) || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.parse.outputs.version }}
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
- name: Parse [project].version from pyproject.toml
id: parse
run: |
VERSION=$(grep -m1 -E '^version[[:space:]]*=' pyproject.toml | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
if [ -z "$VERSION" ]; then
echo "::error::Could not parse [project].version from pyproject.toml."
exit 1
fi
echo "Local version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Fail if version already exists on PyPI
env:
VERSION: ${{ steps.parse.outputs.version }}
run: |
STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/openbb-deribit/${VERSION}/json")
case "$STATUS" in
200)
echo "::error::openbb-deribit ${VERSION} is already published on PyPI."
echo "Bump openbb_platform/providers/deribit/pyproject.toml [project].version and re-merge a fresh release branch."
exit 1
;;
404)
echo "openbb-deribit ${VERSION} is not yet on PyPI — proceeding."
;;
*)
echo "::error::Unexpected status $STATUS from PyPI JSON API. Aborting to avoid a silent skip."
exit 1
;;
esac
- name: Verify release branch name matches pyproject version
if: github.event_name == 'pull_request'
env:
VERSION: ${{ steps.parse.outputs.version }}
BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
EXPECTED="release/openbb-deribit-v${VERSION}"
if [ "$BRANCH" != "$EXPECTED" ]; then
echo "::error::Release branch '${BRANCH}' does not match pyproject version '${VERSION}'."
echo "Expected branch name: '${EXPECTED}'."
echo "Either rename the release branch or update [project].version in pyproject.toml so they agree."
exit 1
fi
echo "Branch '${BRANCH}' matches pyproject version '${VERSION}'."
build:
name: Build sdist + wheel
needs: version-check
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- run: python -m pip install --upgrade pip uv
- name: Build sdist + wheel
run: uv build --no-sources
- uses: actions/upload-artifact@v4
with:
name: openbb-deribit-dist
path: openbb_platform/providers/deribit/dist/
if-no-files-found: error
retention-days: 7
test:
name: Test (${{ matrix.os }} / Python ${{ matrix.python_version }})
needs: build
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, python_version: "3.10" }
- { os: ubuntu-latest, python_version: "3.11" }
- { os: ubuntu-latest, python_version: "3.12" }
- { os: ubuntu-latest, python_version: "3.13" }
- { os: ubuntu-latest, python_version: "3.14" }
- { os: macos-latest, python_version: "3.10" }
- { os: macos-latest, python_version: "3.14" }
- { os: windows-latest, python_version: "3.10" }
- { os: windows-latest, python_version: "3.14" }
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python_version }}
allow-prereleases: true
- uses: actions/download-artifact@v4
with:
name: openbb-deribit-dist
path: openbb_platform/providers/deribit/dist
- run: python -m pip install --upgrade pip uv
- name: Install the dev group then the openbb-deribit wheel
run: |
uv pip install --system --group dev
uv pip install --system dist/openbb_deribit-*.whl
- name: Test the installed wheel
run: |
rm -rf openbb_deribit
pytest tests --cov=openbb_deribit --cov-report=term-missing --cov-fail-under=100
publish:
name: Publish to PyPI
needs: test
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: openbb-deribit-dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist

View File

@@ -0,0 +1,118 @@
name: 🧪 test openbb-deribit
on:
pull_request:
branches: [develop, main, v5]
paths:
- "openbb_platform/providers/deribit/**"
- ".github/workflows/test-unit-openbb-deribit.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: test-openbb-deribit-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build sdist + wheel
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- run: python -m pip install --upgrade pip uv
- name: Build sdist + wheel
run: uv build --no-sources
- uses: actions/upload-artifact@v4
with:
name: openbb-deribit-dist
path: openbb_platform/providers/deribit/dist/
if-no-files-found: error
retention-days: 7
lint:
name: Lint (ruff + ty)
needs: build
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- run: python -m pip install --upgrade pip uv
- name: Install openbb-deribit in editable mode with the dev group
run: uv pip install --system -e . --group dev
- name: ruff check
run: ruff check --no-fix openbb_deribit tests integration
- name: ruff format --check
run: ruff format --check openbb_deribit tests integration
- name: ty check
run: ty check openbb_deribit
test:
name: Test (${{ matrix.os }} / Python ${{ matrix.python_version }})
needs: build
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, python_version: "3.10" }
- { os: ubuntu-latest, python_version: "3.11" }
- { os: ubuntu-latest, python_version: "3.12" }
- { os: ubuntu-latest, python_version: "3.13" }
- { os: ubuntu-latest, python_version: "3.14" }
- { os: macos-latest, python_version: "3.10" }
- { os: macos-latest, python_version: "3.14" }
- { os: windows-latest, python_version: "3.10" }
- { os: windows-latest, python_version: "3.14" }
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: openbb_platform/providers/deribit
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python_version }}
allow-prereleases: true
- uses: actions/download-artifact@v4
with:
name: openbb-deribit-dist
path: openbb_platform/providers/deribit/dist
- run: python -m pip install --upgrade pip uv
- name: Install the dev group then the openbb-deribit wheel
run: |
uv pip install --system --group dev
uv pip install --system dist/openbb_deribit-*.whl
- name: Test the installed wheel
run: |
rm -rf openbb_deribit
pytest tests --cov=openbb_deribit --cov-report=term-missing --cov-fail-under=100

View File

@@ -1,45 +1,305 @@
# OpenBB Deribit Provider Extension
This package adds the `openbb-deribit` provider and router extension to the Open Data Platform by
OpenBB.
It implements the complete public market data surface of [Deribit](https://docs.deribit.com/) — the
crypto derivatives exchange listing options, futures, perpetuals, spot pairs, and multi-leg combos —
as OpenBB Platform endpoints. No API key is required, and nothing here trades.
## Installation
Install from PyPI with:
```sh
pip install openbb-deribit
```
Then build the Python static assets by running:
```sh
openbb-build
```
## Quick Start
The fastest way to get started is by connecting to the OpenBB Workspace as a custom backend.
### Start Server
```sh
openbb-api
```
This starts the FastAPI server over localhost on port 6900.
### Add to Workspace
See the documentation [here](https://docs.openbb.co/python/quickstart/workspace) for more details.
The extension ships an `apps.json` describing a ready-made dashboard with Markets, Futures, Options,
Analysis, Volatility, and Reference tabs.
## Conditional Registration
Five of the Deribit models implement OpenBB standard models — `FuturesCurve`, `FuturesHistorical`,
`FuturesInfo`, `FuturesInstruments`, and `OptionsChains` — which are normally served by the
`openbb-derivatives` extension. When it is installed, Deribit registers as a provider under its
existing commands and no Deribit-specific route is added for them.
When it is absent, the same data is served from this extension's own router under
`/deribit/futures/...` and `/deribit/options/...`, so installing `openbb-deribit` on its own still
exposes every dataset. The bundled `apps.json` resolves its widget IDs the same way.
Everything else is Deribit-specific and always lives under `/deribit/...`.
## Coverage
- obb.derivatives.options.chains
- Support for symbols:
- BTC
- ETH
- XRP
- SOL
- BNB
- PAXG
- obb.derivatives.futures.curve
- Support for symbols:
- BTC
- ETH
- PAXG
- obb.derivatives.futures.historical
- Support for symbols (Perpetual Futures):
- ADAUSDC
- ALGOUSDC
- AVAXUSDC
- BCHUSDC
- BNBUSDC
- BTC
- BTCUSDC
- BTCUSDT
- DOGEUSDC
- DOTUSDC
- ETH
- ETHUSDC
- ETHUSDT
- LINKUSDC
- LTCUSDC
- NEARUSDC
- PAXGUSDC
- SOLUSDC
- TRXUSDC
- UNIUSDC
- XRPUSDC
- Additional symbols include all Deribit instrument names of current open contracts for BTC, ETH, and PAXG futures.
- obb.derivatives.futures.instruments
- Reference data for all current futures instruments.
- obb.derivatives.futures.info
- Key stats by symbol (multiple items allowed).
### Endpoints
```python
from openbb import obb
obb.deribit
# /deribit
# index
# delivery_prices
# historical
# price
# market
# block_rfq_trades
# book_summary
# order_book
# settlements
# ticker
# trade_volumes
# trades
# rates
# apr_history
# funding_chart
# funding_history
# funding_value <- utility endpoint, returns a single rate
# reference
# announcements
# combo_choices <- utility endpoint serving choices to Workspace widgets
# combos
# contract_size <- utility endpoint
# currencies
# currency_choices <- utility endpoint
# expirations
# index_choices <- utility endpoint
# instrument_choices <- utility endpoint
# instruments
# server_time <- utility endpoint
# status <- utility endpoint
# volatility
# index
# realized
# futures
# curve
# curve_choices <- utility endpoint
# historical
# info
# instruments
# perpetual_choices <- utility endpoint
# options
# chains
# optimizer
# payoff
# smile
# spreads
# stats
# straddle
# strangle
# surface
# term_structure
# underlying_choices <- utility endpoint
```
Every public data method the exchange documents is reachable. The methods that exist only to open a
websocket session — `auth`, `hello`, `subscribe`, `set_heartbeat`, `test` — are not exposed, because
they carry no data.
### Datasets
- **Options chains** — the full chain of an underlying, with all five greeks, mark/bid/ask implied
volatility, and open interest. Read over a websocket subscription, which is the only way the
exchange publishes greeks. BTC and ETH contracts are quoted in the underlying, so their premiums
are carried to USD at the index level each quote was published against.
- **Order books** — the resting depth of any instrument, one row per level, by name or by numeric
instrument identifier.
- **Quotes** — the live ticker of any instrument, and the whole-currency book summary carrying the
top of book and trailing session statistics of every instrument at once.
- **Trades** — the prints that crossed, by instrument or across a currency, optionally over a span.
Option prints carry the volatility they implied.
- **Block RFQ** — the block requests that traded, one row per leg, with the index levels each
structure was marked against.
- **Futures** — the term structure of any underlying with a dated curve, priced off each contract's
mark, optionally against how the curve stood some hours ago. Candles are available for any listed
instrument, not only futures.
- **Funding and yield** — the hourly funding a perpetual has paid, the funding accrued over the
current window, the single rate for a span, and the daily yield of each yield-bearing currency.
- **Volatility** — the DVOL volatility index as candles, and the volatility an asset has actually
realized. Both accept several at once, and both offer only what the reading means something for:
DVOL is computed for BTC and ETH alone, and a dollar-pegged token has no volatility against the
dollar to realize, so USDC and USDT are not offered on either. EURR is, because a euro token
measured against the dollar reads the euro rate — around 5 percent against their 0.5.
This is separate from a currency contracts are *settled* in. USDC margins a real book on Deribit,
so it stays on the instrument, book summary, settlement and trade views, and the dashboard binds
the two meanings to two different controls rather than one.
### Options Are Listed By Underlying, Not By Settlement Currency
Only BTC and ETH are both a settlement currency and an underlying, which is the only reason the
exchange's by-currency listings read sensibly for them. USDC settles options written on seven
different underlyings, so asking for `kind=option` by USDC returns 3,656 contracts across AVAX,
BTC, ETH, HYPE, SOL, TRON and XRP — a settlement rail, not a chain. USDT and EURR settle no options
at all.
So a listing of option contracts scoped to a currency that only settles them is refused, and says
to ask by the underlying instead. Every one of those underlyings is offered by name, and naming an
instrument directly still works. The same views keep USDC for what it genuinely carries — 174
futures, 47 spot pairs — and settlements, which are a settlement-currency fact, keep every
currency the exchange collateralises in.
- **Indices** — the current level of any of the 344 published indices, its history over a span, and
the level each index has delivered at.
- **Settlements** — the settlements, deliveries, and bankruptcies the exchange has processed.
- **Reference** — the full contract specification of every listed instrument, every currency the
exchange lists, the expirations listed on each currency, every multi-leg combo, and the exchange's
own announcements.
## Options Analysis
The chain is also the input to an analysis suite built on it. Every figure below is computed from
the quotes the exchange publishes — there is no second data source, and no assumption about a rate
or a dividend the exchange does not quote.
- **Strategy optimizer** — builds every Deribit combo structure the chosen expiration can form,
bought and sold: call and put spreads (CS, PS), straddles and strangles (STRD, STRG), risk
reversals and synthetics (RR, REV), 1x2 ratio spreads (CSR12, PSR12), butterflies and iron
butterflies (CBUT, PBUT, IBUT), condors and iron condors (CCOND, PCOND, ICOND), calendars and
diagonals against the expiration about a month later (CCAL, PCAL, CDIAG, PDIAG), and single
calls and puts. A position Deribit lists as a combo book is priced from that book's bid or offer,
and every listed combo on the expiration is ranked even outside the strike window. Each position
is sized so its worst loss across a threefold move either way is the budget, which is what lets
credit structures rank alongside debit ones. The best position of each structure is returned,
ranked by what it returns if the underlying reaches a price by a date.
- **Payoff** — the profit and loss of one ranked strategy, drawn only over the prices where the
position actually responds. A payoff is a straight line everywhere outside its strikes, so the
range is taken from the position rather than from a fixed span either side of spot, and the
target and breakevens are added when they sit within reach of it.
- **Straddle, strangle, vertical spreads** — the standard structures priced at every listed
expiration, each with its cost, its cost as a share of spot, its breakevens, and its bounds.
- **Volatility smile** — implied volatility or skew against strike for calls and puts, up to five
expirations, optionally only out of the money. Drawn the same way as the Cboe smile.
- **Volatility term structure** — implied volatility or price at the nearest out-of-the-money
strike, a chosen strike, or a moneyness, across every expiration. Drawn the same way as Cboe's.
- **Volatility surface** — implied volatility, a greek, or delta and gamma exposure, raised over
days to expiration and strike, for either side of the chain. Drawn the same way as Cboe's.
- **Statistics** — call and put open interest or volume by expiration, or by strike for one
expiration, as values, shares of the total, or put/call ratios. Drawn the same way as Cboe's.
### Priced and Sized in the Quote Currency
Budgets and profits are expressed in what the underlying is quoted in — USD for the coin-margined
roots, USDC for the linear ones — so a budget means the same thing whichever contract it buys, and
the numbers are readable without converting anything.
BTC and ETH options are still settled in the coin, and that is not hidden. The premium is coin the
account no longer holds, so what it cost depends on what the coin is worth at the price being
valued: at the price the position was opened at the loss is exactly the premium, above it more,
below it less. This is why an inverse position's loss is not the flat line a linear payoff has, and
the chart says which contract settles in what.
The settlement style, contract size and tick size are read from the exchange's own instrument
specification rather than assumed. The tick matters for sizing: far out of the money the published
mark can run below the smallest price the instrument trades in — a TRON contract marked at 3.1e-07
against a tick of 5e-05 — and sizing a budget against a price nobody can pay invents a position
hundreds of times larger than the money would really buy. A contract that is priced at all is
entered at no less than one tick; a contract with no price is left out.
### Greeks Are Modelled, Not Subscribed
The `options.chains` table reads the greeks the exchange itself publishes, which means subscribing
to every contract over a websocket — about twelve seconds for BTC. The analysis views do not need
the exchange's greeks, only its quotes and volatilities, and those come from three REST calls in
well under a second. They read that path instead and model the greeks from the quoted volatility.
Measured against the greeks Deribit publishes, across all 972 BTC contracts: delta agrees to a
median of 0.0001, gamma to 0.000002, vega to 0.03. Theta follows the exchange's own two
conventions rather than the bare derivative — scaled down inside the last day, where the
derivative diverges but a contract cannot decay faster than it expires, and floored at the
contract's own value, because nothing decays by more than it is worth. Both rules bind on real
contracts; together they bring the worst disagreement across the chain from 193 per day to 0.11.
The whole nine-widget Analysis tab loads in under three seconds cold and about one warm, against
fourteen seconds before. The last read of a chain is held briefly and shared, so nine views of one
underlying read it once.
### Choices Are Read Live
The exchange lists 52 currencies, 344 indices, and roughly 5,800 instruments, and the set changes
week to week. Nothing in this extension freezes those into a literal: the underlyings with listed
options, the underlyings with a futures curve, the perpetuals, the indices, and the combo
identifiers are all read from the exchange when asked for. The utility endpoints marked above serve
them to Workspace widgets as `[{label, value}]`.
## Example
```python
from openbb import obb
# The full BTC option chain, with greeks.
chains = obb.deribit.options.chains(symbol="BTC", provider="deribit")
# The BTC futures term structure, against where it stood a day ago.
curve = obb.deribit.futures.curve(symbol="BTC", hours_ago=24, provider="deribit")
# The resting depth of the BTC perpetual.
book = obb.deribit.market.order_book(
symbol="BTC-PERPETUAL", depth=20, provider="deribit"
)
# The top of book of every BTC instrument at once.
summary = obb.deribit.market.book_summary(
currency="BTC", kind="option", provider="deribit"
)
# The hourly funding the BTC perpetual has paid.
funding = obb.deribit.rates.funding_history(symbol="BTC-PERPETUAL", provider="deribit")
# The DVOL volatility index, as daily candles.
dvol = obb.deribit.volatility.index(currency="BTC", interval="1d", provider="deribit")
# Every instrument the exchange lists, with its full specification.
instruments = obb.deribit.reference.instruments(currency="any", provider="deribit")
# The best position of every combo structure for a view of 100,000 BTC by year end,
# each risking 5,000 USD.
ranked = obb.deribit.options.optimizer(
symbol="BTC", target_price=100000, target_date="2026-12-25", budget=5000
)
# The payoff of the best of them.
diagram = obb.deribit.options.payoff(
symbol="BTC", target_price=100000, target_date="2026-12-25"
)
# At-the-money volatility against time to expiration.
term = obb.deribit.options.term_structure(symbol="BTC")
```
When `openbb-derivatives` is installed, the five shared models are reached through the standard
namespace instead — `obb.derivatives.options.chains(provider="deribit")`,
`obb.derivatives.futures.curve(provider="deribit")`, and so on.
## Notes
Responses are cached on disk with a lifetime chosen per endpoint: a day for the currency and index
directories, an hour for the instrument listings, minutes for the historical series, and not at all
for quotes, books, and the server clock. The cache file is swept once per process and capped at
128 MiB.
Perpetuals can be named either in full — `SOL_USDC-PERPETUAL` — or by their shortened root,
`SOLUSDC`.

View File

@@ -0,0 +1 @@
"""Live Deribit integration tests."""

View File

@@ -0,0 +1,107 @@
"""The Deribit commands and parameters both consumption surfaces are exercised with."""
SHARED = {
("futures", "curve"),
("futures", "historical"),
("futures", "info"),
("futures", "instruments"),
("options", "chains"),
}
MODELS = [
("futures", "curve", {"symbol": "BTC"}),
(
"futures",
"historical",
{
"symbol": "BTC-PERPETUAL",
"start_date": "2026-01-01",
"end_date": "2026-02-01",
},
),
("futures", "info", {"symbol": "BTC-PERPETUAL"}),
("futures", "instruments", {}),
("index", "delivery_prices", {"index_name": "btc_usd", "limit": 5}),
("index", "historical", {"index_name": "btc_usd", "span": "1h"}),
("index", "price", {"index_name": "btc_usd,eth_usd"}),
("market", "block_rfq_trades", {"limit": 10}),
("market", "book_summary", {"currency": "BTC", "kind": "future"}),
("market", "order_book", {"symbol": "BTC-PERPETUAL", "depth": 5}),
("market", "settlements", {"currency": "BTC", "limit": 5}),
("market", "ticker", {"symbol": "BTC-PERPETUAL"}),
("market", "trade_volumes", {}),
("market", "trades", {"symbol": "BTC-PERPETUAL", "limit": 5}),
("options", "chains", {"symbol": "BTC"}),
("rates", "apr_history", {"currency": "usde", "limit": 5}),
("rates", "funding_chart", {"symbol": "BTC-PERPETUAL"}),
("rates", "funding_history", {"symbol": "BTC-PERPETUAL"}),
("reference", "announcements", {"limit": 3}),
("reference", "combos", {"currency": "BTC"}),
("reference", "currencies", {}),
("reference", "expirations", {"currency": "BTC", "kind": "any"}),
("reference", "instruments", {"currency": "BTC", "kind": "future"}),
("volatility", "index", {"currency": "BTC", "interval": "1d"}),
("volatility", "realized", {"currency": "BTC"}),
]
FEEDS = [
("futures", "curve_choices", {}),
("futures", "perpetual_choices", {}),
("options", "underlying_choices", {}),
("options", "expiration_choices", {"symbol": "BTC"}),
("options", "strike_choices", {"symbol": "BTC"}),
("options", "strategy_choices", {"symbol": "BTC"}),
("rates", "funding_value", {"symbol": "BTC-PERPETUAL"}),
("reference", "combo_choices", {"currency": "BTC"}),
("reference", "contract_size", {"symbol": "BTC-PERPETUAL"}),
("reference", "currency_choices", {}),
("reference", "index_choices", {}),
("reference", "index_choices", {"supported": True}),
("reference", "instrument_choices", {"kind": "future"}),
("reference", "server_time", {}),
("reference", "status", {}),
]
TABLES = [
("optimizer", {"symbol": "BTC", "target_price": 100000, "limit": 5}),
("straddle", {"symbol": "BTC"}),
("strangle", {"symbol": "BTC", "moneyness": 5}),
("spreads", {"symbol": "BTC", "moneyness": 5}),
("straddle", {"symbol": "ETH"}),
]
CHARTS = [
("smile", {"symbol": "BTC"}),
("smile", {"symbol": "BTC", "otm": True, "skew": True}),
("smile", {"symbol": "ETH"}),
("stats", {"symbol": "BTC"}),
("stats", {"symbol": "BTC", "metric": "volume", "unit": "pcr"}),
("stats", {"symbol": "BTC", "by": "strike", "unit": "percent"}),
("stats", {"symbol": "SOL_USDC"}),
("surface", {"symbol": "BTC"}),
("surface", {"symbol": "BTC", "metric": "gex", "option_type": "calls"}),
("surface", {"symbol": "BTC", "dte_min": 7, "dte_max": 90, "oi": True}),
("term_structure", {"symbol": "BTC"}),
("term_structure", {"symbol": "BTC", "metric": "price", "moneyness": 5}),
("payoff", {"symbol": "BTC"}),
("payoff", {"symbol": "BTC", "target_price": 100000, "budget": 1000}),
]
def namespace(router: str, name: str) -> str:
"""Return where a command is served: its own namespace or the shared one."""
from openbb_deribit import DERIVATIVES_INSTALLED
return (
"derivatives"
if DERIVATIVES_INSTALLED and (router, name) in SHARED
else "deribit"
)
def case_id(case: tuple) -> str:
"""Name a parametrized case by its command and parameters."""
*path, params = case
written = ",".join(f"{key}={value}" for key, value in params.items())
return ".".join(path) + (f"[{written}]" if written else "")

View File

@@ -0,0 +1,72 @@
"""Serve the OpenBB REST API for the Deribit integration tests."""
import pytest
def _free_port() -> int:
"""Return a local port nothing is listening on."""
import socket
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
return probe.getsockname()[1]
@pytest.fixture(scope="session")
def api():
"""Run the REST API on a free local port.
Yields
------
dict
The base URL of the API and the authorization headers to call it with.
Raises
------
RuntimeError
If the server does not start within a minute.
"""
import base64
import threading
import time
import uvicorn
from openbb_core.api.rest_api import app
from openbb_core.env import Env
from openbb_deribit.utils.constants import API_PREFIX
port = _free_port()
server = uvicorn.Server(
uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")
)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
deadline = time.monotonic() + 60
while not server.started:
if time.monotonic() > deadline:
raise RuntimeError("The OpenBB REST API did not start within 60 seconds.")
time.sleep(0.25)
credentials = base64.b64encode(
f"{Env().API_USERNAME}:{Env().API_PASSWORD}".encode("ascii")
).decode("ascii")
yield {
"base": f"http://127.0.0.1:{port}{API_PREFIX}",
"headers": {"Authorization": f"Basic {credentials}"},
}
server.should_exit = True
thread.join(timeout=15)
@pytest.fixture(scope="session")
def obb():
"""Return the Python interface, built with this extension installed."""
from openbb import obb as interface
return interface

View File

@@ -0,0 +1,143 @@
"""Deribit through the REST API, against the live exchange."""
import pytest
from .cases import CHARTS, FEEDS, MODELS, TABLES, case_id, namespace
pytestmark = pytest.mark.integration
def _get(api: dict, path: str, params: dict):
"""Call one route and return its decoded body.
Raises
------
AssertionError
If the route does not answer 200.
"""
import requests
written = {
key: str(value).lower() if isinstance(value, bool) else value
for key, value in params.items()
}
response = requests.get(
f"{api['base']}{path}", params=written, headers=api["headers"], timeout=120
)
assert response.status_code == 200, f"{path} {params}: {response.text}"
return response.json()
class TestModels:
"""Every model-backed route returns rows."""
@pytest.mark.parametrize("case", MODELS, ids=case_id)
def test_returns_rows(self, api, case):
"""The route answers with an OBBject body holding at least one row."""
router, name, params = case
body = _get(
api,
f"/{namespace(router, name)}/{router}/{name}",
{"provider": "deribit", **params},
)
assert body["provider"] == "deribit"
assert body["results"]
def test_an_unoffered_depth_is_refused(self, api):
"""A book depth Deribit does not return is refused before the exchange."""
import requests
response = requests.get(
f"{api['base']}/deribit/market/order_book",
params={"provider": "deribit", "symbol": "BTC-PERPETUAL", "depth": "7"},
headers=api["headers"],
timeout=60,
)
assert response.status_code in (400, 422)
assert "not 7" in response.text
class TestFeeds:
"""Every choice feed and scalar route answers."""
@pytest.mark.parametrize("case", FEEDS, ids=case_id)
def test_answers(self, api, case):
"""The route returns something to offer."""
router, name, params = case
body = _get(api, f"/deribit/{router}/{name}", params)
assert body is not None
assert body not in ([], {})
class TestAnalysisTables:
"""The options analysis routes return ranked or priced rows."""
@pytest.mark.parametrize(
"case", TABLES, ids=lambda case: case_id(("options", *case))
)
def test_returns_rows(self, api, case):
"""The route answers with rows."""
name, params = case
body = _get(api, f"/deribit/options/{name}", params)
assert body["results"]
def test_the_optimizer_codes_every_row(self, api):
"""Each ranked position names the Deribit combo it is."""
body = _get(
api, "/deribit/options/optimizer", {"symbol": "BTC", "target_price": 100000}
)
assert all(row["code"] for row in body["results"])
class TestCharts:
"""Every chart route draws, and hands back its rows on request."""
@pytest.mark.parametrize(
"case", CHARTS, ids=lambda case: case_id(("options", *case))
)
def test_draws_a_figure(self, api, case):
"""The route answers with a Plotly figure and its display config."""
name, params = case
figure = _get(api, f"/deribit/options/{name}", params)
assert figure["data"]
assert figure["layout"]["title"]["text"]
assert figure["config"]["responsive"] is True
@pytest.mark.parametrize(
"name", sorted({name for name, _params in CHARTS}), ids=str
)
def test_raw_returns_rows(self, api, name):
"""With raw set, the route answers with the rows behind the chart."""
rows = _get(api, f"/deribit/options/{name}", {"symbol": "BTC", "raw": True})
assert isinstance(rows, list)
assert rows
def test_the_payoff_draws_a_ranked_position(self, api):
"""The legs the optimizer prints draw back as that position."""
view = {"symbol": "BTC", "target_price": 100000}
row = _get(api, "/deribit/options/optimizer", view)["results"][-1]
figure = _get(api, "/deribit/options/payoff", {**view, "legs": row["legs"]})
assert row["strategy"] in figure["layout"]["title"]["text"]
def test_an_unlisted_underlying_is_a_404(self, api):
"""An underlying Deribit lists no options on answers 404."""
import requests
response = requests.get(
f"{api['base']}/deribit/options/smile",
params={"symbol": "NOPE"},
headers=api["headers"],
timeout=60,
)
assert response.status_code == 404

View File

@@ -0,0 +1,148 @@
"""Deribit through the Python interface, against the live exchange."""
import pytest
from openbb_core.app.model.obbject import OBBject
from .cases import CHARTS, FEEDS, MODELS, TABLES, case_id, namespace
pytestmark = pytest.mark.integration
def _command(obb, router: str, name: str):
"""Return the Python command for a route, wherever it is served."""
return getattr(getattr(getattr(obb, namespace(router, name)), router), name)
def _field(row, name: str):
"""Read one field of a result row, whether a model or a mapping."""
return row[name] if isinstance(row, dict) else getattr(row, name)
class TestModels:
"""Every model-backed command returns rows."""
@pytest.mark.parametrize("case", MODELS, ids=case_id)
def test_returns_rows(self, obb, case):
"""The command answers with an OBBject holding at least one row."""
router, name, params = case
result = _command(obb, router, name)(provider="deribit", **params)
assert isinstance(result, OBBject)
assert result.provider == "deribit"
assert result.results
def test_a_result_converts_to_a_frame(self, obb):
"""The rows convert to a DataFrame for Python users."""
frame = _command(obb, "market", "ticker")(
symbol="BTC-PERPETUAL", provider="deribit"
).to_df()
assert not frame.empty
def test_an_unoffered_depth_is_refused(self, obb):
"""A book depth Deribit does not return is refused before the exchange."""
from openbb_core.app.model.abstract.error import OpenBBError
with pytest.raises(OpenBBError, match="not 7"):
obb.deribit.market.order_book(
symbol="BTC-PERPETUAL", depth=7, provider="deribit"
)
class TestFeeds:
"""Every choice feed and scalar answers."""
@pytest.mark.parametrize("case", FEEDS, ids=case_id)
def test_answers(self, obb, case):
"""The feed returns something to offer."""
router, name, params = case
answer = _command(obb, router, name)(**params)
assert answer is not None
assert answer not in ([], {})
def test_status_reports_locked_currencies(self, obb):
"""The platform status names what is locked."""
assert "locked" in obb.deribit.reference.status()
class TestAnalysisTables:
"""The options analysis tables return ranked or priced rows."""
@pytest.mark.parametrize(
"case", TABLES, ids=lambda case: case_id(("options", *case))
)
def test_returns_rows(self, obb, case):
"""The table answers with rows."""
name, params = case
result = getattr(obb.deribit.options, name)(**params)
assert isinstance(result, OBBject)
assert result.results
def test_the_optimizer_codes_every_row(self, obb):
"""Each ranked position names the Deribit combo it is."""
result = obb.deribit.options.optimizer(symbol="BTC", target_price=100000)
codes = [_field(row, "code") for row in result.results]
assert all(codes)
assert len(set(codes)) > 1
class TestCharts:
"""Every chart draws, and every chart hands back its rows on request."""
@pytest.mark.parametrize(
"case", CHARTS, ids=lambda case: case_id(("options", *case))
)
def test_draws_a_figure(self, obb, case):
"""The chart answers with a Plotly figure."""
name, params = case
figure = getattr(obb.deribit.options, name)(**params)
assert isinstance(figure, dict)
assert figure["data"]
assert figure["layout"]["title"]["text"]
@pytest.mark.parametrize(
"name", sorted({name for name, _params in CHARTS}), ids=str
)
def test_raw_returns_rows(self, obb, name):
"""With raw set, the chart answers with the rows behind it."""
rows = getattr(obb.deribit.options, name)(symbol="BTC", raw=True)
assert isinstance(rows, list)
assert rows
def test_the_smile_draws_chosen_expirations(self, obb):
"""Expirations picked from the feed are drawn as calls and puts each."""
offered = obb.deribit.options.expiration_choices(symbol="BTC")
chosen = [item["value"] for item in offered[2:4]]
figure = obb.deribit.options.smile(symbol="BTC", expirations=",".join(chosen))
assert len(figure["data"]) == 2 * len(chosen)
def test_stats_for_one_expiration(self, obb):
"""An expiration picked from the feed is drawn by strike."""
offered = obb.deribit.options.expiration_choices(symbol="BTC")
figure = obb.deribit.options.stats(symbol="BTC", date=offered[1]["value"])
assert "By Strike" in figure["layout"]["title"]["text"]
def test_term_structure_at_a_chosen_strike(self, obb):
"""A strike picked from the feed titles the term structure."""
offered = obb.deribit.options.strike_choices(symbol="BTC")
strike = offered[len(offered) // 2]["value"]
figure = obb.deribit.options.term_structure(symbol="BTC", strike=strike)
assert f"${strike}" in figure["layout"]["title"]["text"]
def test_the_payoff_draws_a_ranked_position(self, obb):
"""The legs the optimizer prints draw back as that position."""
ranked = obb.deribit.options.optimizer(symbol="BTC", target_price=100000)
row = ranked.results[-1]
figure = obb.deribit.options.payoff(
symbol="BTC", target_price=100000, legs=_field(row, "legs")
)
assert _field(row, "strategy") in figure["layout"]["title"]["text"]

View File

@@ -1,24 +1,94 @@
"""OpenBB Deribit Provider Module."""
from importlib.util import find_spec
from openbb_core.provider.abstract.provider import Provider
from openbb_deribit.models.announcements import DeribitAnnouncementsFetcher
from openbb_deribit.models.apr_history import DeribitAprHistoryFetcher
from openbb_deribit.models.block_rfq_trades import DeribitBlockRfqTradesFetcher
from openbb_deribit.models.book_summary import DeribitBookSummaryFetcher
from openbb_deribit.models.combos import DeribitCombosFetcher
from openbb_deribit.models.currencies import DeribitCurrenciesFetcher
from openbb_deribit.models.delivery_prices import DeribitDeliveryPricesFetcher
from openbb_deribit.models.expirations import DeribitExpirationsFetcher
from openbb_deribit.models.funding_chart import DeribitFundingChartFetcher
from openbb_deribit.models.funding_rate_history import (
DeribitFundingRateHistoryFetcher,
)
from openbb_deribit.models.futures_curve import DeribitFuturesCurveFetcher
from openbb_deribit.models.futures_historical import DeribitFuturesHistoricalFetcher
from openbb_deribit.models.futures_info import DeribitFuturesInfoFetcher
from openbb_deribit.models.futures_instruments import DeribitFuturesInstrumentsFetcher
from openbb_deribit.models.historical_volatility import (
DeribitHistoricalVolatilityFetcher,
)
from openbb_deribit.models.index_historical import DeribitIndexHistoricalFetcher
from openbb_deribit.models.index_price import DeribitIndexPriceFetcher
from openbb_deribit.models.instruments import DeribitInstrumentsFetcher
from openbb_deribit.models.options_chains import DeribitOptionsChainsFetcher
from openbb_deribit.models.order_book import DeribitOrderBookFetcher
from openbb_deribit.models.settlements import DeribitSettlementsFetcher
from openbb_deribit.models.ticker import DeribitTickerFetcher
from openbb_deribit.models.trade_volumes import DeribitTradeVolumesFetcher
from openbb_deribit.models.trades import DeribitTradesFetcher
from openbb_deribit.models.volatility_index import DeribitVolatilityIndexFetcher
DERIVATIVES_INSTALLED = find_spec("openbb_derivatives") is not None
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
def _key(standard: str, alias: str, installed: bool) -> str:
"""Return the standard key when the extension owning it is installed, else the alias."""
return standard if installed else alias
_fetcher_dict: dict = {
"DeribitAnnouncements": DeribitAnnouncementsFetcher,
"DeribitAprHistory": DeribitAprHistoryFetcher,
"DeribitBlockRfqTrades": DeribitBlockRfqTradesFetcher,
"DeribitBookSummary": DeribitBookSummaryFetcher,
"DeribitCombos": DeribitCombosFetcher,
"DeribitCurrencies": DeribitCurrenciesFetcher,
"DeribitDeliveryPrices": DeribitDeliveryPricesFetcher,
"DeribitExpirations": DeribitExpirationsFetcher,
"DeribitFundingChart": DeribitFundingChartFetcher,
"DeribitFundingRateHistory": DeribitFundingRateHistoryFetcher,
"DeribitHistoricalVolatility": DeribitHistoricalVolatilityFetcher,
"DeribitIndexHistorical": DeribitIndexHistoricalFetcher,
"DeribitIndexPrice": DeribitIndexPriceFetcher,
"DeribitInstruments": DeribitInstrumentsFetcher,
"DeribitOrderBook": DeribitOrderBookFetcher,
"DeribitSettlements": DeribitSettlementsFetcher,
"DeribitTicker": DeribitTickerFetcher,
"DeribitTradeVolumes": DeribitTradeVolumesFetcher,
"DeribitTrades": DeribitTradesFetcher,
"DeribitVolatilityIndex": DeribitVolatilityIndexFetcher,
_key("FuturesCurve", "DeribitFuturesCurve", DERIVATIVES_INSTALLED): (
DeribitFuturesCurveFetcher
),
_key("FuturesHistorical", "DeribitFuturesHistorical", DERIVATIVES_INSTALLED): (
DeribitFuturesHistoricalFetcher
),
_key("FuturesInfo", "DeribitFuturesInfo", DERIVATIVES_INSTALLED): (
DeribitFuturesInfoFetcher
),
_key("FuturesInstruments", "DeribitFuturesInstruments", DERIVATIVES_INSTALLED): (
DeribitFuturesInstrumentsFetcher
),
_key("OptionsChains", "DeribitOptionsChains", DERIVATIVES_INSTALLED): (
DeribitOptionsChainsFetcher
),
}
deribit_provider = Provider(
name="deribit",
website="https://deribit.com/",
description="""Unofficial Python client for public data published by Deribit.""",
description="""Deribit is a crypto derivatives exchange listing futures, perpetuals,
options, and spot pairs. This extension covers its public market data API, which
needs no credentials and carries no trading.""",
credentials=None,
fetcher_dict={
"FuturesCurve": DeribitFuturesCurveFetcher,
"FuturesHistorical": DeribitFuturesHistoricalFetcher,
"FuturesInfo": DeribitFuturesInfoFetcher,
"FuturesInstruments": DeribitFuturesInstrumentsFetcher,
"OptionsChains": DeribitOptionsChainsFetcher,
},
fetcher_dict=_fetcher_dict,
repr_name="Deribit Public Data",
instructions="This provider does not require any credentials and is not meant for trading.",
instructions="This provider needs no credentials and is not meant for trading.",
)

View File

@@ -0,0 +1,586 @@
[
{
"name": "Deribit",
"img": "https://www.deribit.com//share-image.png",
"img_dark": "",
"img_light": "",
"description": "Order books, options chains, funding, and volatility indexes from Deribit's public market data API.",
"allowCustomization": true,
"groups": [
{
"name": "Instrument",
"type": "endpointParam",
"paramName": "symbol",
"defaultValue": "BTC-PERPETUAL",
"widgetIds": [
"deribit_market_ticker_deribit_obb",
"deribit_market_order_book_deribit_obb",
"deribit_market_trades_deribit_obb",
"deribit_market_book_summary_deribit_obb",
"deribit_market_settlements_deribit_obb",
"derivatives_futures_historical_deribit_obb",
"derivatives_futures_info_deribit_obb"
]
},
{
"name": "Index",
"type": "endpointParam",
"paramName": "index_name",
"defaultValue": "btc_usd",
"widgetIds": [
"deribit_index_price_deribit_obb",
"deribit_index_historical_deribit_obb",
"deribit_index_delivery_prices_deribit_obb"
]
},
{
"name": "Settlement Currency",
"type": "param",
"paramName": "currency",
"defaultValue": "BTC",
"widgetIds": [
"deribit_reference_expirations_deribit_obb",
"deribit_reference_combos_deribit_obb"
]
},
{
"name": "Volatility Asset",
"type": "param",
"paramName": "currency",
"defaultValue": "BTC",
"widgetIds": [
"deribit_volatility_realized_deribit_obb",
"deribit_volatility_index_deribit_obb"
]
},
{
"name": "Underlying",
"type": "endpointParam",
"paramName": "symbol",
"defaultValue": "BTC",
"widgetIds": [
"derivatives_options_chains_deribit_obb",
"deribit_options_optimizer_custom_obb",
"deribit_options_payoff_custom_obb",
"deribit_options_smile_custom_obb",
"deribit_options_term_structure_custom_obb",
"deribit_options_surface_custom_obb",
"deribit_options_stats_custom_obb",
"deribit_options_straddle_custom_obb",
"deribit_options_strangle_custom_obb",
"deribit_options_spreads_custom_obb"
]
},
{
"name": "Strategy",
"type": "endpointParam",
"paramName": "legs",
"widgetIds": [
"deribit_options_optimizer_custom_obb",
"deribit_options_payoff_custom_obb"
]
}
],
"tabs": {
"markets": {
"id": "markets",
"name": "Markets",
"layout": [
{
"i": "deribit_market_ticker_deribit_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 8,
"state": {
"params": {
"symbol": "BTC-PERPETUAL"
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_market_order_book_deribit_obb",
"x": 0,
"y": 8,
"w": 20,
"h": 16,
"state": {
"params": {
"symbol": "BTC-PERPETUAL",
"depth": "20"
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_market_trades_deribit_obb",
"x": 20,
"y": 8,
"w": 20,
"h": 16,
"state": {
"params": {
"symbol": "BTC-PERPETUAL",
"limit": 100
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_market_book_summary_deribit_obb",
"x": 0,
"y": 24,
"w": 40,
"h": 15,
"state": {
"params": {
"currency": "BTC",
"kind": "future"
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_market_trade_volumes_deribit_obb",
"x": 0,
"y": 39,
"w": 40,
"h": 12,
"state": {
"params": {
"extended": true
}
}
}
]
},
"futures": {
"id": "futures",
"name": "Futures",
"layout": [
{
"i": "derivatives_futures_curve_deribit_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 15,
"state": {
"params": {
"symbol": "BTC"
}
}
},
{
"i": "derivatives_futures_historical_deribit_obb",
"x": 0,
"y": 15,
"w": 40,
"h": 15,
"state": {
"params": {
"symbol": "BTC-PERPETUAL",
"interval": "1d"
}
},
"groups": [
"Instrument"
]
},
{
"i": "derivatives_futures_info_deribit_obb",
"x": 0,
"y": 30,
"w": 40,
"h": 8,
"state": {
"params": {
"symbol": "BTC-PERPETUAL"
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_rates_funding_history_deribit_obb",
"x": 0,
"y": 38,
"w": 40,
"h": 14,
"state": {
"params": {
"symbol": "BTC-PERPETUAL"
}
}
},
{
"i": "derivatives_futures_instruments_deribit_obb",
"x": 0,
"y": 52,
"w": 40,
"h": 15
}
]
},
"options": {
"id": "options",
"name": "Options",
"layout": [
{
"i": "derivatives_options_chains_deribit_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 20,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_reference_expirations_deribit_obb",
"x": 0,
"y": 20,
"w": 20,
"h": 14,
"state": {
"params": {
"currency": "BTC",
"kind": "any"
}
},
"groups": [
"Settlement Currency"
]
},
{
"i": "deribit_reference_combos_deribit_obb",
"x": 20,
"y": 20,
"w": 20,
"h": 14,
"state": {
"params": {
"currency": "BTC"
}
},
"groups": [
"Settlement Currency"
]
},
{
"i": "deribit_market_block_rfq_trades_deribit_obb",
"x": 0,
"y": 34,
"w": 40,
"h": 15,
"state": {
"params": {
"limit": 50
}
}
}
]
},
"analysis": {
"id": "analysis",
"name": "Analysis",
"layout": [
{
"i": "deribit_options_optimizer_custom_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 18,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying",
"Strategy"
]
},
{
"i": "deribit_options_payoff_custom_obb",
"x": 0,
"y": 18,
"w": 40,
"h": 18,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying",
"Strategy"
]
},
{
"i": "deribit_options_smile_custom_obb",
"x": 0,
"y": 36,
"w": 40,
"h": 14,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_term_structure_custom_obb",
"x": 0,
"y": 50,
"w": 40,
"h": 15,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_surface_custom_obb",
"x": 0,
"y": 65,
"w": 40,
"h": 20,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_stats_custom_obb",
"x": 0,
"y": 72,
"w": 40,
"h": 16,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_straddle_custom_obb",
"x": 0,
"y": 88,
"w": 20,
"h": 16,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_strangle_custom_obb",
"x": 20,
"y": 88,
"w": 20,
"h": 16,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
},
{
"i": "deribit_options_spreads_custom_obb",
"x": 0,
"y": 104,
"w": 40,
"h": 16,
"state": {
"params": {
"symbol": "BTC"
}
},
"groups": [
"Underlying"
]
}
]
},
"volatility": {
"id": "volatility",
"name": "Volatility",
"layout": [
{
"i": "deribit_volatility_index_deribit_obb",
"x": 0,
"y": 0,
"w": 20,
"h": 15,
"state": {
"params": {
"currency": "BTC",
"interval": "1d"
}
},
"groups": [
"Volatility Asset"
]
},
{
"i": "deribit_volatility_realized_deribit_obb",
"x": 20,
"y": 0,
"w": 20,
"h": 15,
"state": {
"params": {
"currency": "BTC"
}
},
"groups": [
"Volatility Asset"
]
}
]
},
"reference": {
"id": "reference",
"name": "Reference",
"layout": [
{
"i": "deribit_index_price_deribit_obb",
"x": 0,
"y": 0,
"w": 20,
"h": 8,
"state": {
"params": {
"index_name": "btc_usd"
}
},
"groups": [
"Index"
]
},
{
"i": "deribit_index_historical_deribit_obb",
"x": 20,
"y": 0,
"w": 20,
"h": 8,
"state": {
"params": {
"index_name": "btc_usd",
"span": "1d"
}
},
"groups": [
"Index"
]
},
{
"i": "deribit_index_delivery_prices_deribit_obb",
"x": 0,
"y": 8,
"w": 40,
"h": 14,
"state": {
"params": {
"index_name": "btc_usd"
}
},
"groups": [
"Index"
]
},
{
"i": "deribit_market_settlements_deribit_obb",
"x": 0,
"y": 22,
"w": 40,
"h": 14,
"state": {
"params": {
"symbol": "BTC-PERPETUAL"
}
},
"groups": [
"Instrument"
]
},
{
"i": "deribit_rates_apr_history_deribit_obb",
"x": 0,
"y": 36,
"w": 40,
"h": 14,
"state": {
"params": {
"currency": "usde"
}
}
},
{
"i": "deribit_reference_currencies_deribit_obb",
"x": 0,
"y": 50,
"w": 40,
"h": 12
},
{
"i": "deribit_reference_instruments_deribit_obb",
"x": 0,
"y": 62,
"w": 40,
"h": 15,
"state": {
"params": {
"currency": "any",
"kind": "future"
}
}
},
{
"i": "deribit_reference_announcements_deribit_obb",
"x": 0,
"y": 77,
"w": 40,
"h": 12
}
]
}
}
}
]

View File

@@ -0,0 +1,83 @@
"""Deribit Router."""
import json
from pathlib import Path
from openbb_core.app.router import Router
from openbb_deribit import DERIVATIVES_INSTALLED
from openbb_deribit.routers.futures import router as futures_router
from openbb_deribit.routers.index import router as index_router
from openbb_deribit.routers.market import router as market_router
from openbb_deribit.routers.options import router as options_router
from openbb_deribit.routers.rates import router as rates_router
from openbb_deribit.routers.reference import router as reference_router
from openbb_deribit.routers.volatility import router as volatility_router
router = Router(prefix="", description="Deribit provider router.")
for _subrouter in (
reference_router,
market_router,
index_router,
rates_router,
volatility_router,
futures_router,
options_router,
):
router.include_router(_subrouter)
_APPS_JSON = Path(__file__).parent / "assets" / "apps.json"
_STANDALONE_WIDGET_IDS: dict[str, tuple[str, bool]] = {
"derivatives_futures_curve_deribit_obb": (
"deribit_futures_curve_deribit_obb",
DERIVATIVES_INSTALLED,
),
"derivatives_futures_historical_deribit_obb": (
"deribit_futures_historical_deribit_obb",
DERIVATIVES_INSTALLED,
),
"derivatives_futures_info_deribit_obb": (
"deribit_futures_info_deribit_obb",
DERIVATIVES_INSTALLED,
),
"derivatives_futures_instruments_deribit_obb": (
"deribit_futures_instruments_deribit_obb",
DERIVATIVES_INSTALLED,
),
"derivatives_options_chains_deribit_obb": (
"deribit_options_chains_deribit_obb",
DERIVATIVES_INSTALLED,
),
}
def _resolve_widget_id(widget_id: str) -> str:
"""Return the widget ID that resolves against the active install."""
standalone, installed = _STANDALONE_WIDGET_IDS.get(widget_id, (widget_id, True))
return widget_id if installed else standalone
@router.api_router.get("/apps.json", include_in_schema=False)
async def deribit_apps() -> list:
"""Serve the bundled Deribit dashboard template (``assets/apps.json``).
A parameter group names the widgets it binds, so its identifiers are
resolved against the active install the same way the layout's are.
"""
apps = json.loads(_APPS_JSON.read_text(encoding="utf-8"))
for app in apps:
for tab in app.get("tabs", {}).values():
for widget in tab.get("layout", []):
widget["i"] = _resolve_widget_id(widget["i"])
for group in app.get("groups", []):
group["widgetIds"] = [
_resolve_widget_id(widget_id)
for widget_id in group.get("widgetIds", [])
]
return apps

View File

@@ -0,0 +1,145 @@
"""Deribit Announcements Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
class DeribitAnnouncementsQueryParams(QueryParams):
"""Deribit Announcements Query.
Source: https://docs.deribit.com/api-reference/account-management/public-get_announcements
"""
start_date: Any | None = Field(
default=None,
description="Return announcements published on or after this date.",
)
limit: int = Field(
default=50, description="The number of announcements to return.", ge=1
)
class DeribitAnnouncementsData(Data):
"""Deribit Announcements Data."""
__alias_dict__ = {"date": "publication_timestamp"}
date: datetime = Field(
description="When the announcement was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
title: str = Field(
description="The title of the announcement.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
body: str = Field(
description="The body of the announcement, as HTML.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "cellDataType": "text"}
},
)
important: bool | None = Field(
default=None,
description="Whether the exchange flagged the announcement.",
json_schema_extra={
"x-widget_config": {
"chartDataType": "excluded",
"cellDataType": "boolean",
}
},
)
confirmation: bool | None = Field(
default=None,
description="Whether the announcement needs acknowledging.",
json_schema_extra={
"x-widget_config": {
"chartDataType": "excluded",
"cellDataType": "boolean",
"hide": True,
}
},
)
id: int | None = Field(
default=None,
description="The identifier of the announcement.",
json_schema_extra={
"x-widget_config": {
"headerName": "ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitAnnouncementsFetcher(
Fetcher[DeribitAnnouncementsQueryParams, list[DeribitAnnouncementsData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitAnnouncementsQueryParams:
"""Transform the query."""
return DeribitAnnouncementsQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitAnnouncementsQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange published no announcements over the span.
"""
from openbb_deribit.utils.client import request
from openbb_deribit.utils.helpers import to_timestamp
data = await request(
"get_announcements",
{
"start_timestamp": (
to_timestamp(query.start_date) if query.start_date else None
),
"count": query.limit,
},
)
if not data:
raise EmptyDataError("Deribit published no announcements over the span.")
return data
@staticmethod
def transform_data(
query: DeribitAnnouncementsQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitAnnouncementsData]:
"""Transform the data to the model."""
return [
DeribitAnnouncementsData.model_validate(record)
for record in sorted(
data, key=lambda d: d.get("publication_timestamp") or 0, reverse=True
)
]

View File

@@ -0,0 +1,113 @@
"""Deribit APR History Model."""
from datetime import date as dateType
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import AprCurrencies
class DeribitAprHistoryQueryParams(QueryParams):
"""Deribit APR History Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_apr_history
"""
currency: AprCurrencies = Field(
default="usde", description="The yield-bearing currency."
)
limit: int = Field(default=365, description="The number of days to return.", ge=1)
class DeribitAprHistoryData(Data):
"""Deribit APR History Data."""
__alias_dict__ = {"date": "day"}
date: dateType = Field(
description="The day the rate applied to.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
currency: str = Field(
description="The currency earning the rate.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
apr: float = Field(
description="The annual percentage rate the currency earned.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "APR",
"chartDataType": "series",
},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the day number as a date."""
from openbb_deribit.utils.helpers import from_day_number
return from_day_number(v)
class DeribitAprHistoryFetcher(
Fetcher[DeribitAprHistoryQueryParams, list[DeribitAprHistoryData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitAprHistoryQueryParams:
"""Transform the query."""
return DeribitAprHistoryQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitAprHistoryQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the currency earned nothing over the span.
"""
from openbb_deribit.utils.client import request
result = await request(
"get_apr_history",
{"currency": query.currency.lower(), "limit": query.limit},
)
data = [
{"currency": query.currency.upper(), **record}
for record in (result or {}).get("data") or []
]
if not data:
raise EmptyDataError(
f"Deribit published no yield history for {query.currency.upper()}."
)
return data
@staticmethod
def transform_data(
query: DeribitAprHistoryQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitAprHistoryData]:
"""Transform the data to the model."""
return [
DeribitAprHistoryData.model_validate(record)
for record in sorted(data, key=lambda d: d.get("day") or 0)
]

View File

@@ -0,0 +1,212 @@
"""Deribit Block RFQ Trades Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
BLOCK_RFQ_MAX_COUNT,
BLOCK_RFQ_MIN_COUNT,
AnyListingCurrencies,
)
class DeribitBlockRfqTradesQueryParams(QueryParams):
"""Deribit Block RFQ Trades Query.
Source: https://docs.deribit.com/api-reference/block-rfq/public-get_block_rfq_trades
"""
currency: AnyListingCurrencies = Field(
default="any", description="The settlement currency of the quoted legs."
)
limit: int = Field(
default=BLOCK_RFQ_MAX_COUNT,
description=f"The number of requests to return, between"
f" {BLOCK_RFQ_MIN_COUNT} and {BLOCK_RFQ_MAX_COUNT}.",
ge=BLOCK_RFQ_MIN_COUNT,
le=BLOCK_RFQ_MAX_COUNT,
)
class DeribitBlockRfqTradesData(Data):
"""Deribit Block RFQ Trades Data."""
timestamp: datetime = Field(
description="When the request traded.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
rfq_id: int | None = Field(
default=None,
description="The identifier of the request.",
json_schema_extra={
"x-widget_config": {
"headerName": "RFQ ID",
"cellDataType": "text",
"chartDataType": "excluded",
}
},
)
combo_id: str | None = Field(
default=None,
description="The combo the request was quoted as.",
json_schema_extra={
"x-widget_config": {
"headerName": "Combo ID",
"chartDataType": "excluded",
}
},
)
direction: str | None = Field(
default=None,
description="Which side the taker was on.",
json_schema_extra={
"x-widget_config": {"headerName": "Side", "chartDataType": "excluded"}
},
)
amount: float | None = Field(
default=None,
description="The size of the request.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
mark_price: float | None = Field(
default=None,
description="The price the exchange marked the structure at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
leg: str | None = Field(
default=None,
description="The instrument the leg trades.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
leg_direction: str | None = Field(
default=None,
description="Which side the leg was traded on.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
leg_price: float | None = Field(
default=None,
description="The price the leg traded at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
leg_ratio: float | None = Field(
default=None,
description="How many of the leg one structure holds.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
index_prices: dict | None = Field(
default=None,
description="The index prices the structure was marked against.",
json_schema_extra={
"x-widget_config": {
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitBlockRfqTradesFetcher(
Fetcher[DeribitBlockRfqTradesQueryParams, list[DeribitBlockRfqTradesData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitBlockRfqTradesQueryParams:
"""Transform the query."""
return DeribitBlockRfqTradesQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitBlockRfqTradesQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If no block request traded.
"""
from openbb_deribit.utils.client import request
from openbb_deribit.utils.helpers import normalize_currency
result = await request(
"get_block_rfq_trades",
{
"currency": normalize_currency(query.currency),
"count": query.limit,
},
use_cache=False,
)
data = (result or {}).get("block_rfqs") or []
if not data:
raise EmptyDataError("Deribit reports no block requests traded.")
return data
@staticmethod
def transform_data(
query: DeribitBlockRfqTradesQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitBlockRfqTradesData]:
"""Transform the data to the model."""
records: list[dict] = []
for rfq in data:
context = {
"timestamp": rfq.get("timestamp"),
"rfq_id": rfq.get("id"),
"combo_id": rfq.get("combo_id"),
"direction": rfq.get("direction"),
"amount": rfq.get("amount"),
"mark_price": rfq.get("mark_price"),
"index_prices": rfq.get("index_prices"),
}
for leg in rfq.get("legs") or [{}]:
records.append(
{
**context,
"leg": leg.get("instrument_name"),
"leg_direction": leg.get("direction"),
"leg_price": leg.get("price"),
"leg_ratio": leg.get("ratio"),
}
)
return [
DeribitBlockRfqTradesData.model_validate(record)
for record in sorted(
records,
key=lambda d: (d["timestamp"] or 0, d["rfq_id"] or 0),
reverse=True,
)
]

View File

@@ -0,0 +1,334 @@
"""Deribit Book Summary Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator, model_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
SYMBOL_STYLE,
InstrumentKinds,
ListingCurrencies,
)
class DeribitBookSummaryQueryParams(QueryParams):
"""Deribit Book Summary Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_book_summary_by_currency
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str | None = Field(
default=None,
description="One or more instrument names. When given, the currency and"
+ " kind are ignored.",
)
currency: ListingCurrencies = Field(
default="BTC", description="The settlement currency of the instruments."
)
kind: InstrumentKinds | None = Field(
default=None, description="The kind of instrument. Default is all of them."
)
@model_validator(mode="after")
def _options_are_listed_by_underlying(self):
"""Refuse a listing of options scoped to a currency that only settles them."""
from openbb_deribit.utils.helpers import reject_options_by_settlement
if not self.symbol:
reject_options_by_settlement(self.currency, self.kind)
return self
class DeribitBookSummaryData(Data):
"""Deribit Book Summary Data."""
__alias_dict__ = {
"symbol": "instrument_name",
"last_price": "last",
"bid": "bid_price",
"ask": "ask_price",
"mid": "mid_price",
"change_percent": "price_change",
"implied_volatility": "mark_iv",
"timestamp": "creation_timestamp",
}
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
timestamp: datetime | None = Field(
default=None,
description="When the summary was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
base_currency: str | None = Field(
default=None,
description="The currency the instrument is based on.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
quote_currency: str | None = Field(
default=None,
description="The currency the instrument is quoted in.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
last_price: float | None = Field(
default=None,
description="The price the instrument last traded at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
bid: float | None = Field(
default=None,
description="The highest price bid.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
ask: float | None = Field(
default=None,
description="The lowest price offered.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
mid: float | None = Field(
default=None,
description="The midpoint of the best bid and offer.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
mark_price: float | None = Field(
default=None,
description="The price the exchange marks positions at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
high: float | None = Field(
default=None,
description="The highest price of the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
low: float | None = Field(
default=None,
description="The lowest price of the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
change_percent: float | None = Field(
default=None,
description="The price change over the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume: float | None = Field(
default=None,
description="The volume of the last 24 hours, in base currency.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
volume_notional: float | None = Field(
default=None,
description="The volume of the last 24 hours, in quote currency.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume_usd: float | None = Field(
default=None,
description="The volume of the last 24 hours, in USD.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {
"headerName": "Volume USD",
"chartDataType": "excluded",
},
},
)
open_interest: float | None = Field(
default=None,
description="The contracts left outstanding.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
current_funding: float | None = Field(
default=None,
description="The funding rate a perpetual is paying right now.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
funding_8h: float | None = Field(
default=None,
description="The funding rate a perpetual paid over eight hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Funding 8H",
"chartDataType": "excluded",
},
},
)
estimated_delivery_price: float | None = Field(
default=None,
description="What the instrument would deliver at right now.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
interest_rate: float | None = Field(
default=None,
description="The rate the exchange prices the option's greeks at.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
implied_volatility: float | None = Field(
default=None,
description="The volatility implied by the mark price.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Mark IV",
"chartDataType": "excluded",
},
},
)
underlying_index: str | None = Field(
default=None,
description="The future or index the option is priced against.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
underlying_price: float | None = Field(
default=None,
description="The price of the underlying the option is priced against.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v) if v else None
@field_validator(
"current_funding",
"funding_8h",
"interest_rate",
mode="before",
check_fields=False,
)
@classmethod
def as_percent(cls, v):
"""Return a rate the exchange sends as a fraction in percent units."""
return v * 100 if v is not None else None
class DeribitBookSummaryFetcher(
Fetcher[DeribitBookSummaryQueryParams, list[DeribitBookSummaryData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitBookSummaryQueryParams:
"""Transform the query."""
return DeribitBookSummaryQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitBookSummaryQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange published no summary for the query.
"""
from openbb_deribit.utils.client import gather, request
from openbb_deribit.utils.helpers import get_perpetual_symbols
if query.symbol:
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
results = await gather(
[
(
"get_book_summary_by_instrument",
{"instrument_name": perpetuals.get(symbol, symbol)},
)
for symbol in symbols
]
)
data = [
record
for result in results
if isinstance(result, list)
for record in result
]
else:
data = await request(
"get_book_summary_by_currency",
{"currency": query.currency.upper(), "kind": query.kind},
)
if not data:
raise EmptyDataError("Deribit published no summary for the query.")
return data
@staticmethod
def transform_data(
query: DeribitBookSummaryQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitBookSummaryData]:
"""Transform the data to the model."""
return [
DeribitBookSummaryData.model_validate(record)
for record in sorted(data, key=lambda d: str(d.get("instrument_name")))
]

View File

@@ -0,0 +1,175 @@
"""Deribit Combos Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
COMBO_CHOICES_ENDPOINT,
SYMBOL_STYLE,
AnyListingCurrencies,
)
class DeribitCombosQueryParams(QueryParams):
"""Deribit Combos Query.
Source: https://docs.deribit.com/api-reference/combo-books/public-get_combos
"""
__json_schema_extra__ = {
"combo_id": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": COMBO_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
currency: AnyListingCurrencies = Field(
default="BTC", description="The currency the combos settle in."
)
combo_id: str | None = Field(
default=None,
description="One or more combo identifiers. When given, the currency is"
+ " ignored.",
)
class DeribitCombosData(Data):
"""Deribit Combos Data."""
combo_id: str = Field(
description="The identifier of the combo.",
json_schema_extra={
"x-widget_config": {
"headerName": "Combo ID",
"chartDataType": "category",
"pinned": "left",
}
},
)
instrument_id: int | None = Field(
default=None,
description="The numeric identifier of the combo.",
json_schema_extra={
"x-widget_config": {
"headerName": "Instrument ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
state: str | None = Field(
default=None,
description="Whether the combo is still tradeable.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
leg: str = Field(
description="The instrument the leg trades.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
amount: float | None = Field(
default=None,
description="The size of the leg. A negative amount is a sold leg.",
json_schema_extra={"x-widget_config": {"chartDataType": "series"}},
)
creation_timestamp: datetime | None = Field(
default=None,
description="When the combo was listed.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
state_timestamp: datetime | None = Field(
default=None,
description="When the combo last changed state.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
@field_validator(
"creation_timestamp", "state_timestamp", mode="before", check_fields=False
)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v) if v else None
class DeribitCombosFetcher(Fetcher[DeribitCombosQueryParams, list[DeribitCombosData]]):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitCombosQueryParams:
"""Transform the query."""
return DeribitCombosQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitCombosQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange lists no matching combo.
"""
from openbb_deribit.utils.client import gather, request
from openbb_deribit.utils.helpers import normalize_currency
if query.combo_id:
ids = [c.strip() for c in query.combo_id.split(",") if c.strip()]
results = await gather(
[("get_combo_details", {"combo_id": c}) for c in ids]
)
data = [result for result in results if isinstance(result, dict) and result]
else:
data = await request(
"get_combos", {"currency": normalize_currency(query.currency)}
)
if not data:
raise EmptyDataError("Deribit lists no combo matching the query.")
return data
@staticmethod
def transform_data(
query: DeribitCombosQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitCombosData]:
"""Transform the data to the model."""
records: list[dict] = []
for combo in data:
for leg in combo.get("legs") or []:
records.append(
{
"combo_id": combo.get("id"),
"instrument_id": combo.get("instrument_id"),
"state": combo.get("state"),
"leg": leg.get("instrument_name"),
"amount": leg.get("amount"),
"creation_timestamp": combo.get("creation_timestamp"),
"state_timestamp": combo.get("state_timestamp"),
}
)
return [
DeribitCombosData.model_validate(record)
for record in sorted(records, key=lambda d: (d["combo_id"], d["leg"]))
]

View File

@@ -0,0 +1,155 @@
"""Deribit Currencies Model."""
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field
class DeribitCurrenciesQueryParams(QueryParams):
"""Deribit Currencies Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_currencies
"""
class DeribitCurrenciesData(Data):
"""Deribit Currencies Data."""
currency: str = Field(
description="The abbreviation of the currency.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
currency_long: str | None = Field(
default=None,
description="The full name of the currency.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "headerName": "Name"}
},
)
coin_type: str | None = Field(
default=None,
description="The type of the currency.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
currency_uuid: str | None = Field(
default=None,
description="The unique identifier of the currency.",
json_schema_extra={
"x-widget_config": {
"chartDataType": "excluded",
"headerName": "Currency UUID",
"hide": True,
}
},
)
network_currency: str | None = Field(
default=None,
description="The currency the network fee is charged in.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
apr: float | None = Field(
default=None,
description="The annual percentage rate earned by holding the currency.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "series", "headerName": "APR"},
},
)
decimals: int | None = Field(
default=None,
description="The number of decimal places the currency is held to.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
onchain_operations_precision: int | None = Field(
default=None,
description="The number of decimal places an on-chain operation is held to.",
json_schema_extra={
"x-widget_config": {
"chartDataType": "excluded",
"headerName": "On-chain Precision",
}
},
)
min_confirmations: int | None = Field(
default=None,
description="The confirmations a deposit needs before it is credited.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
min_withdrawal_fee: float | None = Field(
default=None,
description="The smallest fee a withdrawal is charged.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
withdrawal_fee: float | None = Field(
default=None,
description="The fee a withdrawal is charged.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
network_fee: float | None = Field(
default=None,
description="The fee the network itself charges.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
in_cross_collateral_pool: bool | None = Field(
default=None,
description="Whether the currency counts towards cross collateral.",
json_schema_extra={
"x-widget_config": {
"chartDataType": "excluded",
"cellDataType": "boolean",
}
},
)
class DeribitCurrenciesFetcher(
Fetcher[DeribitCurrenciesQueryParams, list[DeribitCurrenciesData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitCurrenciesQueryParams:
"""Transform the query."""
return DeribitCurrenciesQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitCurrenciesQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange listed no currencies.
"""
from openbb_deribit.utils.helpers import get_currencies
data = await get_currencies()
if not data:
raise EmptyDataError("Deribit listed no currencies.")
return data
@staticmethod
def transform_data(
query: DeribitCurrenciesQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitCurrenciesData]:
"""Transform the data to the model."""
return [
DeribitCurrenciesData.model_validate(record)
for record in sorted(data, key=lambda d: str(d.get("currency")))
]

View File

@@ -0,0 +1,134 @@
"""Deribit Delivery Prices Model."""
from datetime import date as dateType
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field
from openbb_deribit.utils.constants import (
INDEX_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
class DeribitDeliveryPricesQueryParams(QueryParams):
"""Deribit Delivery Prices Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_delivery_prices
"""
__json_schema_extra__ = {
"index_name": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INDEX_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
index_name: str = Field(
default="btc_usd",
description="One or more index names, such as 'btc_usd'.",
)
limit: int = Field(
default=100, description="The number of settlements to return.", ge=1
)
offset: int = Field(
default=0,
description="How many of the most recent settlements to skip.",
ge=0,
)
class DeribitDeliveryPricesData(Data):
"""Deribit Delivery Prices Data."""
date: dateType = Field(
description="The session the index delivered on.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
index_name: str = Field(
description="The name of the index.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
delivery_price: float = Field(
description="The level the index delivered at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
class DeribitDeliveryPricesFetcher(
Fetcher[DeribitDeliveryPricesQueryParams, list[DeribitDeliveryPricesData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitDeliveryPricesQueryParams:
"""Transform the query."""
return DeribitDeliveryPricesQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitDeliveryPricesQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the indexes has delivered.
"""
from openbb_deribit.utils.client import gather
names = [n.strip().lower() for n in query.index_name.split(",") if n.strip()]
results = await gather(
[
(
"get_delivery_prices",
{
"index_name": name,
"count": query.limit,
"offset": query.offset,
},
)
for name in names
]
)
data = [
{"index_name": name, **record}
for name, result in zip(names, results)
if isinstance(result, dict)
for record in result.get("data") or []
]
if not data:
raise EmptyDataError(
f"Deribit published no delivery prices for {', '.join(names)}."
)
return data
@staticmethod
def transform_data(
query: DeribitDeliveryPricesQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitDeliveryPricesData]:
"""Transform the data to the model."""
return [
DeribitDeliveryPricesData.model_validate(record)
for record in sorted(data, key=lambda d: (d["date"], d["index_name"]))
]

View File

@@ -0,0 +1,142 @@
"""Deribit Expirations Model."""
from datetime import date as dateType
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field
from openbb_deribit.utils.constants import ExpirationCurrencies, ExpirationKinds
class DeribitExpirationsQueryParams(QueryParams):
"""Deribit Expirations Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_expirations
"""
currency: ExpirationCurrencies = Field(
default="grouped",
description="The currency to list expirations for. 'any' pools every"
+ " currency together, and 'grouped' keeps them apart.",
)
kind: ExpirationKinds = Field(default="any", description="The kind of instrument.")
currency_pair: str | None = Field(
default=None,
description="Narrow the listing to one currency pair, such as 'btc_usd'.",
)
class DeribitExpirationsData(Data):
"""Deribit Expirations Data."""
currency: str = Field(
description="The currency the contracts settle in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "pinned": "left"}
},
)
kind: str = Field(
description="The kind of instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
code: str = Field(
description="The expiration as it appears in instrument names.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
expiration: dateType | None = Field(
default=None,
description="The expiration date. Perpetual contracts carry none.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
class DeribitExpirationsFetcher(
Fetcher[DeribitExpirationsQueryParams, list[DeribitExpirationsData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitExpirationsQueryParams:
"""Transform the query."""
return DeribitExpirationsQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitExpirationsQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> dict:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange lists no expirations for the query.
"""
from openbb_deribit.utils.helpers import get_expirations
data = await get_expirations(query.currency, query.kind, query.currency_pair)
if not data:
raise EmptyDataError("Deribit lists no expirations matching the query.")
return data
@staticmethod
def transform_data(
query: DeribitExpirationsQueryParams,
data: dict,
**kwargs: Any,
) -> list[DeribitExpirationsData]:
"""Transform the data to the model.
Raises
------
EmptyDataError
If every currency in the response listed nothing.
"""
from pandas import to_datetime
by_currency = (
{query.currency: data}
if all(isinstance(value, list) for value in data.values())
else data
)
records: list[dict] = []
for currency, kinds in by_currency.items():
for kind, codes in kinds.items():
for code in codes:
records.append(
{
"currency": currency.upper(),
"kind": kind,
"code": code,
"expiration": (
None
if code == "PERPETUAL"
else to_datetime(code).date()
),
}
)
if not records:
raise EmptyDataError("Deribit lists no expirations matching the query.")
return [
DeribitExpirationsData.model_validate(record)
for record in sorted(
records,
key=lambda d: (
d["currency"],
d["kind"],
d["expiration"] or dateType.max,
),
)
]

View File

@@ -0,0 +1,177 @@
"""Deribit Funding Chart Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
PERPETUAL_CHOICES_ENDPOINT,
SYMBOL_STYLE,
FundingLengths,
)
class DeribitFundingChartQueryParams(QueryParams):
"""Deribit Funding Chart Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_funding_chart_data
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": PERPETUAL_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str = Field(
default="BTC-PERPETUAL",
description="One or more perpetual instrument names. A perpetual can also"
+ " be given by its shortened root, such as 'SOLUSDC'.",
)
length: FundingLengths = Field(
default="8h", description="How far back the series runs."
)
class DeribitFundingChartData(Data):
"""Deribit Funding Chart Data."""
__alias_dict__ = {"date": "timestamp"}
date: datetime = Field(
description="When the rate was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
interest_8h: float | None = Field(
default=None,
description="The funding rate over the trailing eight hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Interest 8H",
"chartDataType": "series",
},
},
)
index_price: float | None = Field(
default=None,
description="The level of the index when the rate was published.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
current_interest: float | None = Field(
default=None,
description="The funding rate accrued so far in the current interval.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
@field_validator(
"interest_8h",
"current_interest",
mode="before",
check_fields=False,
)
@classmethod
def as_percent(cls, v):
"""Return a rate the exchange sends as a fraction in percent units."""
return v * 100 if v is not None else None
class DeribitFundingChartFetcher(
Fetcher[DeribitFundingChartQueryParams, list[DeribitFundingChartData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitFundingChartQueryParams:
"""Transform the query."""
return DeribitFundingChartQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitFundingChartQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the instruments published funding over the span.
"""
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.helpers import get_perpetual_symbols
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
resolved = [perpetuals.get(symbol, symbol) for symbol in symbols]
results = await gather(
[
(
"get_funding_chart_data",
{"instrument_name": symbol, "length": query.length},
)
for symbol in resolved
]
)
data = [
{
"symbol": symbol,
"current_interest": result.get("current_interest"),
**record,
}
for symbol, result in zip(resolved, results)
if isinstance(result, dict)
for record in result.get("data") or []
]
if not data:
raise EmptyDataError(
f"Deribit published no funding over the span for {', '.join(resolved)}."
)
return data
@staticmethod
def transform_data(
query: DeribitFundingChartQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFundingChartData]:
"""Transform the data to the model."""
return [
DeribitFundingChartData.model_validate(record)
for record in sorted(
data, key=lambda d: (d.get("timestamp") or 0, d["symbol"])
)
]

View File

@@ -0,0 +1,208 @@
"""Deribit Funding Rate History Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
PERPETUAL_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
class DeribitFundingRateHistoryQueryParams(QueryParams):
"""Deribit Funding Rate History Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_funding_rate_history
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": PERPETUAL_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str = Field(
default="BTC-PERPETUAL",
description="One or more perpetual instrument names. A perpetual can also"
+ " be given by its shortened root, such as 'SOLUSDC'.",
)
start_date: Any | None = Field(
default=None, description="Return rates from this date onwards."
)
end_date: Any | None = Field(
default=None, description="Return rates up to this date."
)
class DeribitFundingRateHistoryData(Data):
"""Deribit Funding Rate History Data."""
__alias_dict__ = {"date": "timestamp"}
date: datetime = Field(
description="The hour the rate applied to.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
interest_1h: float | None = Field(
default=None,
description="The funding rate over the hour.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Interest 1H",
"chartDataType": "excluded",
},
},
)
interest_8h: float | None = Field(
default=None,
description="The funding rate over the trailing eight hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Interest 8H",
"chartDataType": "series",
},
},
)
index_price: float | None = Field(
default=None,
description="The level of the index at the end of the hour.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
prev_index_price: float | None = Field(
default=None,
description="The level of the index at the start of the hour.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {
"headerName": "Previous Index Price",
"chartDataType": "excluded",
},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
@field_validator(
"interest_1h",
"interest_8h",
mode="before",
check_fields=False,
)
@classmethod
def as_percent(cls, v):
"""Return a rate the exchange sends as a fraction in percent units."""
return v * 100 if v is not None else None
class DeribitFundingRateHistoryFetcher(
Fetcher[DeribitFundingRateHistoryQueryParams, list[DeribitFundingRateHistoryData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(
params: dict[str, Any],
) -> DeribitFundingRateHistoryQueryParams:
"""Transform the query."""
return DeribitFundingRateHistoryQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitFundingRateHistoryQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the instruments paid funding over the span.
"""
from datetime import (
datetime as dt,
timedelta,
timezone,
)
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.helpers import get_perpetual_symbols, to_timestamp
now = dt.now(timezone.utc)
start = (
to_timestamp(query.start_date)
if query.start_date
else to_timestamp(now - timedelta(days=7))
)
end = to_timestamp(query.end_date) if query.end_date else to_timestamp(now)
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
resolved = [perpetuals.get(symbol, symbol) for symbol in symbols]
results = await gather(
[
(
"get_funding_rate_history",
{
"instrument_name": symbol,
"start_timestamp": start,
"end_timestamp": end,
},
)
for symbol in resolved
]
)
data = [
{"symbol": symbol, **record}
for symbol, result in zip(resolved, results)
if isinstance(result, list)
for record in result
]
if not data:
raise EmptyDataError(
f"Deribit published no funding over the span for {', '.join(resolved)}."
)
return data
@staticmethod
def transform_data(
query: DeribitFundingRateHistoryQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFundingRateHistoryData]:
"""Transform the data to the model."""
return [
DeribitFundingRateHistoryData.model_validate(record)
for record in sorted(
data, key=lambda d: (d.get("timestamp") or 0, d["symbol"])
)
]

View File

@@ -1,105 +1,142 @@
"""Deribit Futures Curve Model."""
# pylint: disable=unused-argument
from datetime import date as dateType
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.futures_curve import (
FuturesCurveData,
FuturesCurveQueryParams,
)
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.utils.helpers import (
DERIBIT_FUTURES_CURVE_SYMBOLS,
FuturesCurveSymbols,
)
from pydantic import Field, field_validator, model_validator
from openbb_deribit.utils.constants import (
CURVE_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
class DeribitFuturesCurveQueryParams(FuturesCurveQueryParams):
"""
Deribit Futures Curve Query.
"""Deribit Futures Curve Query.
Source: https://docs.deribit.com/?shell#public-ticker
Source: https://docs.deribit.com/api-reference/market-data/public-ticker
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": False,
"choices": DERIBIT_FUTURES_CURVE_SYMBOLS,
},
"hours_ago": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": CURVE_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
},
"hours_ago": {"multiple_items_allowed": True},
}
symbol: FuturesCurveSymbols = Field(
symbol: str = Field(
default="BTC",
description=QUERY_DESCRIPTIONS.get("symbol", "")
+ " Default is 'btc' Supported symbols are: ['btc', 'eth', 'paxg']",
+ " The underlying root, as it appears in the instrument name.",
)
hours_ago: int | list[int] | str | None = Field(
hours_ago: str | None = Field(
default=None,
description="Compare the current curve with the specified number of hours ago. Default is None.",
description="Compare the current curve against how it stood this many"
+ " hours ago.",
)
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def validate_symbol(cls, v):
"""Validate the symbol."""
symbol = v.upper()
if symbol not in DERIBIT_FUTURES_CURVE_SYMBOLS:
raise ValueError(
f"Invalid Deribit symbol, {symbol}. Supported symbols are: {', '.join(DERIBIT_FUTURES_CURVE_SYMBOLS)}"
)
return symbol
@field_validator("hours_ago", mode="before", check_fields=False)
@classmethod
def _validate_hours_ago(cls, v):
"""Validate hours ago."""
if isinstance(v, str):
return v
if isinstance(v, int):
return v
if isinstance(v, list):
return ",".join([str(i) for i in v])
return None
def validate_hours_ago(cls, v):
"""Read one or more hour counts as a comma-separated string."""
if v is None:
return None
return ",".join(str(hour) for hour in v) if isinstance(v, list) else str(v)
@model_validator(mode="before")
@classmethod
def _validate_model(cls, values):
"""Validate the model."""
if values.get("date"):
def validate_model(cls, values):
"""Reject a date, which the exchange does not serve a curve for.
Raises
------
ValueError
If a date was given.
"""
if isinstance(values, dict) and values.get("date"):
raise ValueError(
"Date field is not supported for Deribit provider. Use 'hours_ago' instead."
"Deribit serves no curve as of a date. Use 'hours_ago' instead."
)
return values
class DeribitFuturesCurveData(FuturesCurveData):
"""Deribit Futures Curve Data."""
date: dateType | None = Field(
default=None,
description=DATA_DESCRIPTIONS.get("date", ""),
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
expiration: str = Field(
description="Futures expiration month.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
price: float | None = Field(
default=None,
description="The price of the futures contract, taken from the mark.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
symbol: str | None = Field(
default=None,
description="The name of the contract.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
last_price: float | None = Field(
default=None,
description="The price the contract last traded at, which on a thin"
+ " contract can be a long way from the mark the curve is built on.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
hours_ago: int | None = Field(
default=None,
description="The number of hours ago represented by the price."
+ " Only available when hours_ago is set in the query.",
description="How many hours back the price was read, when the query"
+ " asked for one.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
class DeribitFuturesCurveFetcher(
Fetcher[DeribitFuturesCurveQueryParams, list[DeribitFuturesCurveData]]
):
"""Deribit Futures Curve Fetcher."""
"""Transform the query, extract and transform the data from the Deribit endpoint.
The current curve is priced off each contract's mark, because a thin contract's
last trade can be a long way from where the exchange marks it. A curve read
some hours back is priced off traded closes instead, the only history the
exchange publishes, so it carries only the contracts that had traded by then.
"""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitFuturesCurveQueryParams:
"""Transform query params."""
"""Transform the query."""
return DeribitFuturesCurveQueryParams(**params)
@staticmethod
@@ -107,79 +144,83 @@ class DeribitFuturesCurveFetcher(
query: DeribitFuturesCurveQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list:
"""Extract the raw data."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the contracts on the curve published a quote.
"""
from openbb_deribit.utils.helpers import (
get_futures_curve_symbols,
get_ticker_data,
get_futures_curve_by_hours_ago,
get_futures_curve_symbols,
get_tickers,
)
try:
symbols = await get_futures_curve_symbols(query.symbol)
tasks = [get_ticker_data(s) for s in symbols]
data = await asyncio.gather(*tasks, return_exceptions=True)
symbols = await get_futures_curve_symbols(query.symbol)
data = await get_tickers(symbols)
if query.hours_ago is not None:
num_hours = query.hours_ago
if query.hours_ago:
for hour in [int(h) for h in query.hours_ago.split(",") if h.strip()]:
data.extend(await get_futures_curve_by_hours_ago(query.symbol, hour))
hours_ago = (
[int(d) for d in num_hours.split(",")]
if isinstance(num_hours, str)
else [int(num_hours)] if isinstance(num_hours, int) else num_hours
)
if not data:
raise EmptyDataError(
f"Deribit published no quote for the {query.symbol} curve."
)
for hours in hours_ago:
hours_data = await get_futures_curve_by_hours_ago(
query.symbol, hours
)
if hours_data:
data.extend(hours_data)
return data
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(
f"Failed to get futures curve -> {e.__class__.__name__ if hasattr(e, '__class__') else e}: {e.args}"
) from e
return data
@staticmethod
def transform_data(
query: DeribitFuturesCurveQueryParams, data: list, **kwargs: Any
query: DeribitFuturesCurveQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFuturesCurveData]:
"""Transform the data."""
# pylint: disable=import-outside-toplevel
from datetime import datetime # noqa
"""Transform the data to the model.
Raises
------
EmptyDataError
If every contract on the curve came back unpriced.
"""
from datetime import datetime, timezone
from pandas import to_datetime
if not data:
raise EmptyDataError("No data found")
records: list[dict] = []
futures_curve: list[DeribitFuturesCurveData] = []
for record in data:
symbol = str(record.get("instrument_name") or "")
code = symbol.split("-")[1] if "-" in symbol else ""
price = record.get("mark_price") or record.get("last_price")
for d in data:
if not d:
if not symbol or price is None:
continue
ins_name = d.get("instrument_name", "")
exp = ins_name.split("-")[1]
hours_ago = d.get("hours_ago", 0)
exp = (
datetime.today().strftime("%Y-%m-%d")
if exp == "PERPETUAL"
else to_datetime(exp).strftime("%Y-%m-%d")
records.append(
{
"symbol": symbol,
"expiration": (
datetime.now(timezone.utc).strftime("%Y-%m-%d")
if code == "PERPETUAL"
else to_datetime(code).strftime("%Y-%m-%d")
),
"price": price,
"last_price": record.get("last_price"),
"hours_ago": record.get("hours_ago") if query.hours_ago else None,
}
)
price = d.get("last_price", d.get("mark_price"))
if not records:
raise EmptyDataError(
f"Deribit published no priced contract on the {query.symbol} curve."
)
result = {"expiration": exp, "price": price}
if query.hours_ago:
result["hours_ago"] = hours_ago
if price:
futures_curve.append(DeribitFuturesCurveData.model_validate(result))
if not futures_curve:
raise EmptyDataError("No data found.")
return sorted(futures_curve, key=lambda x: x.expiration)
return [
DeribitFuturesCurveData.model_validate(record)
for record in sorted(
records, key=lambda d: (d["hours_ago"] or 0, d["expiration"])
)
]

View File

@@ -1,91 +1,87 @@
"""Deribit Futures Historical Model."""
# pylint: disable=unused-argument
from datetime import datetime, timedelta
from datetime import (
date as dateType,
datetime,
timedelta,
)
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.futures_historical import (
FuturesHistoricalData,
FuturesHistoricalQueryParams,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.utils.helpers import DERIBIT_INTERVALS, DeribitIntervals
from pydantic import Field, field_validator, model_validator
from pydantic import Field, model_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
INTERVALS,
SYMBOL_STYLE,
Intervals,
)
INTERVAL_LOOKBACK = {
"1m": 30,
"3m": 60,
"5m": 60,
"10m": 90,
"15m": 90,
"30m": 90,
}
DEFAULT_LOOKBACK = 364
class DeribitFuturesHistoricalQueryParams(FuturesHistoricalQueryParams):
"""
Deribit Futures historical Price Query.
"""Deribit Futures Historical Query.
Source: https://docs.deribit.com/?shell#public-get_tradingview_chart_data
Source: https://docs.deribit.com/api-reference/market-data/public-get_tradingview_chart_data
"""
__json_schema_extra__ = {
"symbol": {"multiple_items_allowed": True},
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
},
"interval": {
"multiple_items_allowed": False,
"choices": DERIBIT_INTERVALS,
"choices": list(INTERVALS),
},
"expiration": {"x-widget_config": {"exclude": True}},
}
interval: DeribitIntervals = Field(
symbol: str = Field(
description=QUERY_DESCRIPTIONS.get("symbol", "")
+ " Any listed instrument works, not only futures. A perpetual can also"
+ " be given by its shortened root, such as 'SOLUSDC'."
)
interval: Intervals = Field(
default="1d", description=QUERY_DESCRIPTIONS.get("interval", "")
)
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def _validate_symbol(cls, v):
"""Validate the symbol."""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import run_async
from openbb_deribit.utils.helpers import (
get_futures_symbols,
get_perpetual_symbols,
)
if not v:
raise ValueError("Symbol is required.")
futures_symbols = run_async(get_futures_symbols)
perpetual_symbols = run_async(get_perpetual_symbols)
all_symbols = list(perpetual_symbols) + futures_symbols
symbols = v.upper().split(",")
new_symbols: list = []
for symbol in symbols:
if symbol not in all_symbols:
raise ValueError(
f"Invalid Deribit symbol: {symbol}. Supported symbols are: {', '.join(all_symbols)}"
)
if symbol in perpetual_symbols:
new_symbols.append(perpetual_symbols[symbol])
else:
new_symbols.append(symbol)
return ",".join(new_symbols)
@model_validator(mode="before")
@classmethod
def _validate_model(cls, values):
"""Validate the model."""
interval = values.get("interval")
def validate_model(cls, values):
"""Fill in a span the interval can be served over."""
if not isinstance(values, dict):
return values
now = datetime.today()
if not values.get("start_date"):
if interval == "1m":
start = now - timedelta(days=30)
elif interval in ["3m", "5m"]:
start = now - timedelta(days=60)
elif interval in ["15m", "30m"]:
start = now - timedelta(days=90)
else:
start = now - timedelta(days=364)
values["start_date"] = start.strftime("%Y-%m-%d")
lookback = INTERVAL_LOOKBACK.get(
str(values.get("interval") or "1d"), DEFAULT_LOOKBACK
)
values["start_date"] = (now - timedelta(days=lookback)).strftime("%Y-%m-%d")
if not values.get("end_date"):
values["end_date"] = (now + timedelta(days=1)).strftime("%Y-%m-%d")
@@ -96,16 +92,72 @@ class DeribitFuturesHistoricalQueryParams(FuturesHistoricalQueryParams):
class DeribitFuturesHistoricalData(FuturesHistoricalData):
"""Deribit Futures Historical Data."""
volume_notional: float = Field(description="Trading volume in quote currency.")
date: datetime | dateType = Field(
description=DATA_DESCRIPTIONS.get("date", ""),
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
open: float | None = Field(
default=None,
description=DATA_DESCRIPTIONS.get("open", ""),
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
high: float | None = Field(
default=None,
description=DATA_DESCRIPTIONS.get("high", ""),
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
low: float | None = Field(
default=None,
description=DATA_DESCRIPTIONS.get("low", ""),
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
close: float = Field(
description=DATA_DESCRIPTIONS.get("close", ""),
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
volume: float | None = Field(
default=None,
description=DATA_DESCRIPTIONS.get("volume", ""),
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
symbol: str | None = Field(
default=None,
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
volume_notional: float | None = Field(
default=None,
description="The volume of the candle, in quote currency.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
class DeribitFuturesHistoricalFetcher(
Fetcher[DeribitFuturesHistoricalQueryParams, list[DeribitFuturesHistoricalData]]
):
"""Deribit Futures Historical Fetcher."""
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitFuturesHistoricalQueryParams:
def transform_query(
params: dict[str, Any],
) -> DeribitFuturesHistoricalQueryParams:
"""Transform the query."""
return DeribitFuturesHistoricalQueryParams(**params)
@@ -114,42 +166,51 @@ class DeribitFuturesHistoricalFetcher(
query: DeribitFuturesHistoricalQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list:
"""Extract the raw data."""
# pylint: disable=import-outside-toplevel
from openbb_deribit.utils.helpers import get_ohlc_data
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
symbols = query.symbol.split(",")
results: list = []
Raises
------
EmptyDataError
If none of the instruments published candles over the span.
"""
from openbb_deribit.utils.helpers import get_ohlc_data, get_perpetual_symbols
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
results: list[dict] = []
for symbol in symbols:
try:
data = await get_ohlc_data(
symbol=symbol,
interval=query.interval,
start_date=query.start_date,
end_date=query.end_date,
results.extend(
await get_ohlc_data(
perpetuals.get(symbol, symbol),
query.start_date,
query.end_date,
query.interval,
)
if data:
results.extend(data)
except OpenBBError as e:
raise e from e
)
if not results:
raise EmptyDataError("No data found.")
raise EmptyDataError(
f"Deribit published no candles over the span for {', '.join(symbols)}."
)
return sorted(results, key=lambda x: x["date"])
return results
@staticmethod
def transform_data(
query: DeribitFuturesHistoricalQueryParams, data: list, **kwargs: Any
query: DeribitFuturesHistoricalQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFuturesHistoricalData]:
"""Transform the data."""
symbols = query.symbol.split(",")
if len(symbols) == 1:
results: list[DeribitFuturesHistoricalData] = []
for d in data:
_ = d.pop("symbol", None)
results.append(DeribitFuturesHistoricalData.model_validate(d))
return [DeribitFuturesHistoricalData.model_validate(d) for d in data]
return [DeribitFuturesHistoricalData.model_validate(d) for d in data]
"""Transform the data to the model."""
single = len([s for s in query.symbol.split(",") if s.strip()]) == 1
return [
DeribitFuturesHistoricalData.model_validate(
{key: value for key, value in record.items() if key != "symbol"}
if single
else record
)
for record in sorted(data, key=lambda d: (d["date"], d["symbol"]))
]

View File

@@ -1,187 +1,59 @@
"""Deribit Futures Info Models."""
"""Deribit Futures Info Model."""
# pylint: disable=unused-argument
from datetime import datetime
from typing import Any, Literal
from typing import Any
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.futures_info import (
FuturesInfoData,
FuturesInfoQueryParams,
)
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import ConfigDict, Field
from openbb_deribit.models.ticker import DeribitTickerData
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
from pydantic import ConfigDict, Field, field_validator
class DeribitFuturesInfoQueryParams(FuturesInfoQueryParams):
"""Deribit Futures Instruments Query."""
"""Deribit Futures Info Query.
Source: https://docs.deribit.com/api-reference/market-data/public-ticker
"""
__json_schema_extra__ = {
"symbol": {"multiple_items_allowed": True},
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str = Field(
description=QUERY_DESCRIPTIONS.get("symbol", "")
+ " Perpetual contracts can be referenced by their currency pair - i.e, SOLUSDC"
+ " - or by their official Deribit symbol - i.e, SOL_USDC-PERPETUAL"
+ " For a list of currently available instruments, use `derivatives.futures.instruments()`"
+ " A perpetual can be given by its currency pair - SOLUSDC - or by its"
+ " full instrument name - SOL_USDC-PERPETUAL."
)
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def _validate_symbol(cls, v):
"""Validate the symbol."""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import run_async
from openbb_deribit.utils.helpers import (
get_futures_symbols,
get_perpetual_symbols,
)
symbols = v.split(",")
new_symbols: list = []
perpetual_symbols = run_async(get_perpetual_symbols)
futures_symbols = run_async(get_futures_symbols)
all_symbols = futures_symbols + list(perpetual_symbols)
for s in symbols:
if s not in all_symbols:
raise ValueError(
f"Invalid symbol: {s}. Valid symbols are: {all_symbols}"
)
if s in perpetual_symbols:
new_symbols.append(perpetual_symbols[s])
else:
new_symbols.append(s)
return ",".join(new_symbols)
class DeribitFuturesInfoData(FuturesInfoData):
class DeribitFuturesInfoData(DeribitTickerData, FuturesInfoData):
"""Deribit Futures Info Data."""
__alias_dict__ = {
"symbol": "instrument_name",
"change_percent": "price_change",
}
model_config = ConfigDict(extra="ignore")
symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
state: Literal["open", "closed"] = Field(
description="The state of the order book. Possible values are open and closed."
)
open_interest: float = Field(
description="The total amount of outstanding contracts in the corresponding amount units."
)
index_price: float = Field(
description="Current index (reference) price",
json_schema_extra={"x-unit_measurement": "currency"},
)
best_ask_amount: float | None = Field(
default=None,
description="It represents the requested order size of all best asks",
)
best_ask_price: float | None = Field(
default=None,
description="The current best ask price, null if there aren't any asks",
json_schema_extra={"x-unit_measurement": "currency"},
)
best_bid_price: float | None = Field(
default=None,
description="The current best bid price, null if there aren't any bids",
json_schema_extra={"x-unit_measurement": "currency"},
)
best_bid_amount: float | None = Field(
default=None,
description="It represents the requested order size of all best bids",
)
last_price: float | None = Field(
default=None,
description="The price for the last trade",
json_schema_extra={"x-unit_measurement": "currency"},
)
high: float | None = Field(
default=None,
description="Highest price during 24h",
json_schema_extra={"x-unit_measurement": "currency"},
)
low: float | None = Field(
default=None,
description="Lowest price during 24h",
json_schema_extra={"x-unit_measurement": "currency"},
)
change_percent: float | None = Field(
default=None,
description="24-hour price change expressed as a percentage, null if there weren't any trades",
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
)
volume: float | None = Field(
default=None,
description="Volume during last 24h in base currency",
json_schema_extra={"x-unit_measurement": "currency"},
)
volume_usd: float | None = Field(
default=None,
description="Volume in USD",
json_schema_extra={"x-unit_measurement": "currency"},
)
mark_price: float = Field(
description="The mark price for the instrument",
json_schema_extra={"x-unit_measurement": "currency"},
)
settlement_price: float | None = Field(
default=None,
description="The settlement price for the instrument. Only when state = open",
json_schema_extra={"x-unit_measurement": "currency"},
)
delivery_price: float | None = Field(
default=None,
description="The settlement price for the instrument. Only when state = closed.",
json_schema_extra={"x-unit_measurement": "currency"},
)
estimated_delivery_price: float = Field(
description="Estimated delivery price for the market.",
json_schema_extra={"x-unit_measurement": "currency"},
)
current_funding: float | None = Field(
default=None,
description="Current funding (perpetual only)",
)
funding_8h: float | None = Field(
default=None,
description="Funding 8h (perpetual only)",
)
interest_value: float | None = Field(
default=None,
description="Value used to calculate realized_funding in positions (perpetual only)",
)
max_price: float = Field(
description="The maximum price for the future."
+ " Any buy orders submitted higher than this price, will be clamped to this maximum.",
json_schema_extra={"x-unit_measurement": "currency"},
)
min_price: float = Field(
description="The minimum price for the future."
+ " Any sell orders submitted lower than this price will be clamped to this minimum.",
json_schema_extra={"x-unit_measurement": "currency"},
)
timestamp: datetime = Field(description="The timestamp of the data.")
@field_validator("change_percent", mode="before", check_fields=False)
@classmethod
def _validate_change_percent(cls, v):
"""Validate the change percent."""
return v / 100 if v else v
class DeribitFuturesInfoFetcher(
Fetcher[DeribitFuturesInfoQueryParams, list[DeribitFuturesInfoData]]
):
"""Deribit Futures Info Fetcher."""
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitFuturesInfoQueryParams:
@@ -193,32 +65,40 @@ class DeribitFuturesInfoFetcher(
query: DeribitFuturesInfoQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list:
"""Extract data from the response."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
from openbb_core.provider.utils.errors import EmptyDataError, OpenBBError
from openbb_deribit.utils.helpers import get_ticker_data
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
result: list = []
symbols = query.symbol.split(",")
try:
tasks = [get_ticker_data(symbol) for symbol in symbols]
for task in asyncio.as_completed(tasks, timeout=10):
result.append(await task)
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(f"Error fetching data: {e}") from e
Raises
------
EmptyDataError
If none of the instruments published a quote.
"""
from openbb_deribit.utils.helpers import get_perpetual_symbols, get_tickers
if not result:
raise EmptyDataError("No data found for the given symbol(s).")
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
resolved = [perpetuals.get(symbol, symbol) for symbol in symbols]
data = await get_tickers(resolved)
return sorted(result, key=lambda x: symbols.index(x["instrument_name"]))
if not data:
raise EmptyDataError(
f"Deribit published no quote for {', '.join(resolved)}."
)
return sorted(
data,
key=lambda d: (
resolved.index(d["instrument_name"])
if d["instrument_name"] in resolved
else len(resolved)
),
)
@staticmethod
def transform_data(
query: DeribitFuturesInfoQueryParams,
data: list,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFuturesInfoData]:
"""Transform the data."""
return [DeribitFuturesInfoData(**d) for d in data]
"""Transform the data to the model."""
return [DeribitFuturesInfoData.model_validate(record) for record in data]

View File

@@ -1,104 +1,301 @@
"""Deribit Futures Instruments Model."""
# pylint: disable=unused-argument
from datetime import datetime
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.futures_instruments import (
FuturesInstrumentsData,
FuturesInstrumentsQueryParams,
)
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import ConfigDict, Field, field_validator
from openbb_deribit.models.instruments import PERPETUAL_EXPIRATION
class DeribitFuturesInstrumentsQueryParams(FuturesInstrumentsQueryParams):
"""Deribit Futures Instruments Query."""
"""Deribit Futures Instruments Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_instruments
"""
class DeribitFuturesInstrumentData(FuturesInstrumentsData):
"""Deribit Futures Instrument Data."""
__alias_dict__ = {
"symbol": "instrument_name",
}
__alias_dict__ = {"symbol": "instrument_name"}
model_config = ConfigDict(extra="ignore")
instrument_id: int = Field(description="Deribit Instrument ID")
symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
base_currency: str = Field(description="The underlying currency being traded.")
counter_currency: str = Field(description="Counter currency for the instrument.")
quote_currency: str = Field(
description="The currency in which the instrument prices are quoted."
symbol: str = Field(
description=DATA_DESCRIPTIONS.get("symbol", ""),
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
instrument_id: int = Field(
description="The numeric identifier of the instrument.",
json_schema_extra={
"x-widget_config": {
"headerName": "Instrument ID",
"cellDataType": "text",
"chartDataType": "excluded",
},
},
)
kind: str | None = Field(
default=None,
description="The kind of instrument.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
instrument_type: str | None = Field(
default=None,
description="Whether the instrument is linear or reversed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
future_type: str | None = Field(
default=None,
description="Whether the future is linear or reversed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
state: str | None = Field(
default=None,
description="Whether the instrument's book is open.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
base_currency: str | None = Field(
default=None,
description="The currency the instrument is based on.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
quote_currency: str | None = Field(
default=None,
description="The currency the instrument is quoted in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
counter_currency: str | None = Field(
default=None,
description="The counter currency of the instrument.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
settlement_currency: str | None = Field(
default=None, description="Settlement currency for the instrument."
default=None,
description="The currency the instrument settles in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
future_type: str = Field(description="Type of the instrument. linear or reversed")
settlement_period: str | None = Field(
default=None, description="The settlement period."
default=None,
description="How often the instrument settles.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
price_index: str = Field(
description="Name of price index that is used for this instrument"
underlying_type: str | None = Field(
default=None,
description="The asset class of the underlying.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
contract_size: float = Field(description="Contract size for instrument.")
is_active: bool = Field(
description="Indicates if the instrument can currently be traded."
product_group: str | None = Field(
default=None,
description="The margin group the instrument belongs to.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
creation_timestamp: datetime = Field(
description="The time when the instrument was first created (milliseconds since the UNIX epoch)."
price_index: str | None = Field(
default=None,
description="The index the instrument is priced against.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
lot_size: int | None = Field(
default=None,
description="The number of contracts in one lot.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
min_trade_amount: float | None = Field(
default=None,
description="The smallest amount that can be traded.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
tick_size: float | None = Field(
default=None,
description="The smallest price increment.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_leverage: int | None = Field(
default=None,
description="The highest leverage the instrument allows.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_non_default_leverage: float | None = Field(
default=None,
description="The highest leverage available outside the default tier.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_liquidation_commission: float | None = Field(
default=None,
description="The largest commission a liquidation is charged.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
maker_commission: float | None = Field(
default=None,
description="The commission charged to a resting order.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
taker_commission: float | None = Field(
default=None,
description="The commission charged to an aggressing order.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_commission: float | None = Field(
default=None,
description="The commission charged to a block trade.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_min_trade_amount: float | None = Field(
default=None,
description="The smallest amount a block trade can be.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_tick_size: float | None = Field(
default=None,
description="The smallest price increment of a block trade.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
is_active: bool | None = Field(
default=None,
description="Whether the instrument can be traded right now.",
json_schema_extra={
"x-widget_config": {"cellDataType": "boolean", "chartDataType": "excluded"},
},
)
contract_size: float | None = Field(
default=None,
description="The size of one contract.",
json_schema_extra={
"x-widget_config": {"chartDataType": "series"},
},
)
index_id: int | None = Field(
default=None,
description="The numeric identifier of the price index.",
json_schema_extra={
"x-widget_config": {
"headerName": "Index ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
},
},
)
tick_size_steps: list[dict] | None = Field(
default=None,
description="The price levels above which a different tick size applies.",
json_schema_extra={
"x-widget_config": {
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
},
},
)
base_currency_uuid: str | None = Field(
default=None,
description="The unique identifier of the base currency.",
json_schema_extra={
"x-widget_config": {
"headerName": "Base Currency UUID",
"chartDataType": "excluded",
"hide": True,
},
},
)
quote_currency_uuid: str | None = Field(
default=None,
description="The unique identifier of the quote currency.",
json_schema_extra={
"x-widget_config": {
"headerName": "Quote Currency UUID",
"chartDataType": "excluded",
"hide": True,
},
},
)
creation_timestamp: datetime | None = Field(
default=None,
description="When the instrument was listed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
expiration_timestamp: datetime | None = Field(
default=None,
description="The time when the instrument will expire (milliseconds since the UNIX epoch).",
)
tick_size: float = Field(
description="Specifies minimal price change and, as follows, the number of decimal places for instrument prices."
)
min_trade_amount: float = Field(
description="Minimum amount for trading, in USD units."
)
max_leverage: int = Field(description="Maximal leverage for instrument.")
max_liquidation_commission: float = Field(
description="Maximal liquidation trade commission for instrument."
)
block_trade_commission: float = Field(
description="Block Trade commission for instrument."
)
block_trade_min_trade_amount: float = Field(
description="Minimum amount for block trading."
)
block_trade_tick_size: float = Field(
description="Specifies minimal price change for block trading."
)
maker_commission: float | None = Field(
default=None, description="Maker commission for instrument."
)
taker_commission: float | None = Field(
default=None, description="Taker commission for instrument."
description="When the instrument expires. Perpetual contracts carry none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
@field_validator("expiration_timestamp", mode="before", check_fields=False)
@classmethod
def _validate_expiration(cls, v):
"""Validate the expiration timestamp."""
if int(v) == 32503708800000:
return None
return v
def validate_expiration(cls, v):
"""Read the expiration, dropping the sentinel a perpetual carries."""
return None if not v or int(v) == PERPETUAL_EXPIRATION else v
class DeribitFuturesInstrumentsFetcher(
Fetcher[DeribitFuturesInstrumentsQueryParams, list[DeribitFuturesInstrumentData]]
):
"""Deribit Futures Instruments Fetcher."""
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitFuturesInstrumentsQueryParams:
def transform_query(
params: dict[str, Any],
) -> DeribitFuturesInstrumentsQueryParams:
"""Transform the query."""
return DeribitFuturesInstrumentsQueryParams(**params)
@@ -107,27 +304,37 @@ class DeribitFuturesInstrumentsFetcher(
query: DeribitFuturesInstrumentsQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list:
"""Extract data from Deribit API."""
# pylint: disable=import-outside-toplevel
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange lists no futures.
"""
from openbb_deribit.utils.helpers import get_instruments
try:
data = await get_instruments("all", "future")
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(f"Error fetching data: {e}") from e
data = await get_instruments("any", "future")
if not data:
raise OpenBBError(
"There was an error with the request and it was returned empty."
)
raise EmptyDataError("Deribit lists no futures.")
return data
@staticmethod
def transform_data(
query: DeribitFuturesInstrumentsQueryParams,
data: list,
data: list[dict],
**kwargs: Any,
) -> list[DeribitFuturesInstrumentData]:
"""Transform the data."""
return [DeribitFuturesInstrumentData.model_validate(d) for d in data]
"""Transform the data to the model."""
return [
DeribitFuturesInstrumentData.model_validate(record)
for record in sorted(
data,
key=lambda d: (
d.get("expiration_timestamp") or 0,
str(d.get("instrument_name")),
),
)
]

View File

@@ -0,0 +1,159 @@
"""Deribit Historical Volatility Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import REALIZED_VOLATILITY_CURRENCIES
class DeribitHistoricalVolatilityQueryParams(QueryParams):
"""Deribit Historical Volatility Query.
A currency pegged to the unit it is measured against has no volatility to
realize: USDC and USDT read a few tenths of a percent, which is the peg
moving rather than the asset, so neither is offered. EURR is a euro token
measured against the dollar, so what it reads is the euro rate, and it is.
Source: https://docs.deribit.com/api-reference/market-data/public-get_historical_volatility
"""
__json_schema_extra__ = {
"currency": {
"multiple_items_allowed": True,
"x-widget_config": {
"options": [
{"label": name, "value": name}
for name in REALIZED_VOLATILITY_CURRENCIES
]
},
}
}
currency: str = Field(
default="BTC", description="The currency whose realized volatility to read."
)
@field_validator("currency", mode="before")
@classmethod
def _listed_currencies(cls, value):
"""Accept one currency or several, and refuse any the exchange omits.
Raises
------
ValueError
If a currency the exchange does not publish this for is asked for.
"""
chosen = [
item.strip().upper() for item in str(value).split(",") if item.strip()
]
unlisted = [
item for item in chosen if item not in REALIZED_VOLATILITY_CURRENCIES
]
if unlisted:
raise ValueError(
f"Deribit publishes no realized volatility for {unlisted}."
f" It publishes {list(REALIZED_VOLATILITY_CURRENCIES)}."
)
return ",".join(chosen)
class DeribitHistoricalVolatilityData(Data):
"""Deribit Historical Volatility Data."""
date: datetime = Field(
description="When the reading was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
currency: str = Field(
description="The currency the reading belongs to.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
volatility: float = Field(
description="The realized volatility over the trailing window.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "series"},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitHistoricalVolatilityFetcher(
Fetcher[
DeribitHistoricalVolatilityQueryParams,
list[DeribitHistoricalVolatilityData],
]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(
params: dict[str, Any],
) -> DeribitHistoricalVolatilityQueryParams:
"""Transform the query."""
return DeribitHistoricalVolatilityQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitHistoricalVolatilityQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the currency published no readings.
"""
from openbb_deribit.utils.client import gather
currencies = [c.strip().upper() for c in query.currency.split(",") if c.strip()]
results = await gather(
[
("get_historical_volatility", {"currency": currency})
for currency in currencies
]
)
data = [
{"date": point[0], "currency": currency, "volatility": point[1]}
for currency, result in zip(currencies, results)
if isinstance(result, list)
for point in result
]
if not data:
raise EmptyDataError(
f"Deribit published no realized volatility for {', '.join(currencies)}."
)
return data
@staticmethod
def transform_data(
query: DeribitHistoricalVolatilityQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitHistoricalVolatilityData]:
"""Transform the data to the model."""
return [
DeribitHistoricalVolatilityData.model_validate(record)
for record in sorted(data, key=lambda d: (d["date"], d["currency"]))
]

View File

@@ -0,0 +1,129 @@
"""Deribit Index Historical Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
INDEX_CHOICES_ENDPOINT,
SYMBOL_STYLE,
IndexChartRanges,
)
class DeribitIndexHistoricalQueryParams(QueryParams):
"""Deribit Index Historical Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_index_chart_data
"""
__json_schema_extra__ = {
"index_name": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INDEX_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
index_name: str = Field(
default="btc_usd",
description="One or more index names, such as 'btc_usd'.",
)
span: IndexChartRanges = Field(
default="1d", description="How far back the series runs."
)
class DeribitIndexHistoricalData(Data):
"""Deribit Index Historical Data."""
date: datetime = Field(
description="When the level was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
index_name: str = Field(
description="The name of the index.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
price: float = Field(
description="The level of the index.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitIndexHistoricalFetcher(
Fetcher[DeribitIndexHistoricalQueryParams, list[DeribitIndexHistoricalData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitIndexHistoricalQueryParams:
"""Transform the query."""
return DeribitIndexHistoricalQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitIndexHistoricalQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the indexes published a series.
"""
from openbb_deribit.utils.client import gather
names = [n.strip().lower() for n in query.index_name.split(",") if n.strip()]
results = await gather(
[
("get_index_chart_data", {"index_name": name, "range": query.span})
for name in names
]
)
data = [
{"date": point[0], "index_name": name, "price": point[1]}
for name, result in zip(names, results)
if isinstance(result, list)
for point in result
]
if not data:
raise EmptyDataError(f"Deribit published no series for {', '.join(names)}.")
return data
@staticmethod
def transform_data(
query: DeribitIndexHistoricalQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitIndexHistoricalData]:
"""Transform the data to the model."""
return [
DeribitIndexHistoricalData.model_validate(record)
for record in sorted(data, key=lambda d: (d["date"], d["index_name"]))
]

View File

@@ -0,0 +1,118 @@
"""Deribit Index Price Model."""
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field
from openbb_deribit.utils.constants import (
INDEX_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
class DeribitIndexPriceQueryParams(QueryParams):
"""Deribit Index Price Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_index_price
"""
__json_schema_extra__ = {
"index_name": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INDEX_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
index_name: str = Field(
default="btc_usd",
description="One or more index names, such as 'btc_usd'.",
)
class DeribitIndexPriceData(Data):
"""Deribit Index Price Data."""
index_name: str = Field(
description="The name of the index.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
index_price: float | None = Field(
default=None,
description="The current level of the index.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
estimated_delivery_price: float | None = Field(
default=None,
description="What instruments on the index would deliver at right now.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
class DeribitIndexPriceFetcher(
Fetcher[DeribitIndexPriceQueryParams, list[DeribitIndexPriceData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitIndexPriceQueryParams:
"""Transform the query."""
return DeribitIndexPriceQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitIndexPriceQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the indexes published a level.
"""
from openbb_deribit.utils.client import gather
names = [n.strip().lower() for n in query.index_name.split(",") if n.strip()]
results = await gather(
[("get_index_price", {"index_name": name}) for name in names],
use_cache=False,
)
data = [
{"index_name": name, **result}
for name, result in zip(names, results)
if isinstance(result, dict) and result
]
if not data:
raise EmptyDataError(f"Deribit published no level for {', '.join(names)}.")
return data
@staticmethod
def transform_data(
query: DeribitIndexPriceQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitIndexPriceData]:
"""Transform the data to the model."""
return [DeribitIndexPriceData.model_validate(record) for record in data]

View File

@@ -0,0 +1,430 @@
"""Deribit Instruments Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator, model_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
SYMBOL_STYLE,
AnyListingCurrencies,
InstrumentKinds,
)
PERPETUAL_EXPIRATION = 32503708800000
class DeribitInstrumentsQueryParams(QueryParams):
"""Deribit Instruments Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_instruments
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str | None = Field(
default=None,
description="One or more instrument names. When given, the currency and"
+ " kind are ignored.",
)
currency: AnyListingCurrencies = Field(
default="any", description="The settlement currency of the instruments."
)
kind: InstrumentKinds | None = Field(
default=None,
description="The kind of instrument. Default is all of them.",
)
expired: bool = Field(
default=False,
description="When True, returns instruments that have already expired.",
)
@model_validator(mode="after")
def _options_are_listed_by_underlying(self):
"""Refuse a listing of options scoped to a currency that only settles them."""
from openbb_deribit.utils.helpers import reject_options_by_settlement
if not self.symbol:
reject_options_by_settlement(self.currency, self.kind)
return self
class DeribitInstrumentsData(Data):
"""Deribit Instruments Data."""
__alias_dict__ = {"symbol": "instrument_name"}
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
instrument_id: int | None = Field(
default=None,
description="The numeric identifier of the instrument.",
json_schema_extra={
"x-widget_config": {
"headerName": "Instrument ID",
"cellDataType": "text",
"chartDataType": "excluded",
},
},
)
kind: str | None = Field(
default=None,
description="The kind of instrument.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
instrument_type: str | None = Field(
default=None,
description="Whether the instrument is linear or reversed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
future_type: str | None = Field(
default=None,
description="Whether the future is linear or reversed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
state: str | None = Field(
default=None,
description="Whether the instrument's book is open.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
base_currency: str | None = Field(
default=None,
description="The currency the instrument is based on.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
quote_currency: str | None = Field(
default=None,
description="The currency the instrument is quoted in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
counter_currency: str | None = Field(
default=None,
description="The counter currency of the instrument.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
settlement_currency: str | None = Field(
default=None,
description="The currency the instrument settles in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
settlement_period: str | None = Field(
default=None,
description="How often the instrument settles.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
underlying_type: str | None = Field(
default=None,
description="The asset class of the underlying.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
product_group: str | None = Field(
default=None,
description="The margin group the instrument belongs to.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
price_index: str | None = Field(
default=None,
description="The index the instrument is priced against.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
lot_size: int | None = Field(
default=None,
description="The number of contracts in one lot.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
min_trade_amount: float | None = Field(
default=None,
description="The smallest amount that can be traded.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
tick_size: float | None = Field(
default=None,
description="The smallest price increment.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_leverage: int | None = Field(
default=None,
description="The highest leverage the instrument allows.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_non_default_leverage: float | None = Field(
default=None,
description="The highest leverage available outside the default tier.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_liquidation_commission: float | None = Field(
default=None,
description="The largest commission a liquidation is charged.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
maker_commission: float | None = Field(
default=None,
description="The commission charged to a resting order.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
taker_commission: float | None = Field(
default=None,
description="The commission charged to an aggressing order.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_commission: float | None = Field(
default=None,
description="The commission charged to a block trade.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_min_trade_amount: float | None = Field(
default=None,
description="The smallest amount a block trade can be.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
block_trade_tick_size: float | None = Field(
default=None,
description="The smallest price increment of a block trade.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
option_type: str | None = Field(
default=None,
description="Whether the option is a call or a put.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
strike: float | None = Field(
default=None,
description="The strike price of the option.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
is_active: bool | None = Field(
default=None,
description="Whether the instrument can be traded right now.",
json_schema_extra={
"x-widget_config": {"cellDataType": "boolean", "chartDataType": "excluded"},
},
)
contract_size: float | None = Field(
default=None,
description="The size of one contract.",
json_schema_extra={
"x-widget_config": {"chartDataType": "series"},
},
)
is_csr: bool | None = Field(
default=None,
description="Whether the instrument is a cross settled routed pair.",
json_schema_extra={
"x-widget_config": {
"headerName": "Is CSR",
"cellDataType": "boolean",
"chartDataType": "excluded",
"hide": True,
},
},
)
is_cbe_routed: bool | None = Field(
default=None,
description="Whether the instrument routes to Coinbase Exchange liquidity.",
json_schema_extra={
"x-widget_config": {
"headerName": "Is CBE Routed",
"cellDataType": "boolean",
"chartDataType": "excluded",
},
},
)
index_id: int | None = Field(
default=None,
description="The numeric identifier of the price index.",
json_schema_extra={
"x-widget_config": {
"headerName": "Index ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
},
},
)
tick_size_steps: list[dict] | None = Field(
default=None,
description="The price levels above which a different tick size applies.",
json_schema_extra={
"x-widget_config": {
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
},
},
)
base_currency_uuid: str | None = Field(
default=None,
description="The unique identifier of the base currency.",
json_schema_extra={
"x-widget_config": {
"headerName": "Base Currency UUID",
"chartDataType": "excluded",
"hide": True,
},
},
)
quote_currency_uuid: str | None = Field(
default=None,
description="The unique identifier of the quote currency.",
json_schema_extra={
"x-widget_config": {
"headerName": "Quote Currency UUID",
"chartDataType": "excluded",
"hide": True,
},
},
)
creation_timestamp: datetime | None = Field(
default=None,
description="When the instrument was listed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
expiration_timestamp: datetime | None = Field(
default=None,
description="When the instrument expires. Perpetual and spot instruments carry none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
@field_validator(
"creation_timestamp", "expiration_timestamp", mode="before", check_fields=False
)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime, dropping the perpetual sentinel."""
from openbb_deribit.utils.helpers import from_timestamp
return None if not v or int(v) == PERPETUAL_EXPIRATION else from_timestamp(v)
class DeribitInstrumentsFetcher(
Fetcher[DeribitInstrumentsQueryParams, list[DeribitInstrumentsData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitInstrumentsQueryParams:
"""Transform the query."""
return DeribitInstrumentsQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitInstrumentsQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange lists no matching instrument.
"""
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.helpers import get_instruments
if query.symbol:
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
results = await gather(
[("get_instrument", {"instrument_name": s}) for s in symbols]
)
data = [result for result in results if isinstance(result, dict) and result]
else:
data = await get_instruments(query.currency, query.kind, query.expired)
if not data:
raise EmptyDataError("Deribit lists no instrument matching the query.")
return data
@staticmethod
def transform_data(
query: DeribitInstrumentsQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitInstrumentsData]:
"""Transform the data to the model."""
return [
DeribitInstrumentsData.model_validate(record)
for record in sorted(
data,
key=lambda d: (
str(d.get("kind")),
d.get("expiration_timestamp") or 0,
str(d.get("instrument_name")),
),
)
]

View File

@@ -1,44 +1,58 @@
"""Deribit Options Chains Model."""
# pylint: disable=unused-argument
from datetime import datetime
from datetime import (
date as dateType,
datetime,
)
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.options_chains import (
OptionsChainsData,
OptionsChainsQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.utils.helpers import DERIBIT_OPTIONS_SYMBOLS
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
SYMBOL_STYLE,
UNDERLYING_CHOICES_ENDPOINT,
)
INVERSE_QUOTED = ("BTC", "ETH")
PRICE_FIELDS = (
"last_price",
"settlement_price",
"mark_price",
"min_price",
"max_price",
"best_ask_price",
"best_bid_price",
"high",
"low",
)
class DeribitOptionsChainsQueryParams(OptionsChainsQueryParams):
"""Deribit Options Chains Query Parameters Model."""
"""Deribit Options Chains Query.
Source: https://docs.deribit.com/api-reference/market-data/public-ticker
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": False,
"choices": DERIBIT_OPTIONS_SYMBOLS,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": UNDERLYING_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def _validate_symbol(cls, v):
"""Validate the symbol."""
if v.upper() not in DERIBIT_OPTIONS_SYMBOLS:
raise ValueError(
f"Invalid Deribit symbol. Supported symbols are: {', '.join(DERIBIT_OPTIONS_SYMBOLS)}",
)
return v
class DeribitOptionsChainsData(OptionsChainsData):
"""Deribit Options Chains Data Model."""
"""Deribit Options Chains Data."""
__alias_dict__ = {
"contract_symbol": "instrument_name",
@@ -57,60 +71,459 @@ class DeribitOptionsChainsData(OptionsChainsData):
__doc__ = OptionsChainsData.__doc__
bid_iv: list[float | None] = Field(
default_factory=list,
description="The implied volatility of the bid price.",
json_schema_extra={"x-unit_measurement": "decimal"},
contract_symbol: list[str] = Field(
description="The symbol of the contract.",
json_schema_extra={
"x-widget_config": {
"headerName": "Contract",
"chartDataType": "excluded",
"pinned": "left",
},
},
)
ask_iv: list[float | None] = Field(
default_factory=list,
description="The implied volatility of the ask price.",
json_schema_extra={"x-unit_measurement": "decimal"},
expiration: list[dateType] = Field(
description="The expiration date of the contract.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
interest_rate: list[float | None] = Field(
default_factory=list,
description="The interest rate used by Deribit to calculate greeks.",
dte: list[int | None] = Field(
description="The number of days until expiration.",
json_schema_extra={
"x-widget_config": {"headerName": "DTE", "chartDataType": "excluded"},
},
)
underlying_spot_price: list[float] = Field(
description="The spot price of the underlying asset."
" The underlying asset is the specific future or index that the option is based on.",
json_schema_extra={"x-unit_measurement": "currency"},
strike: list[float] = Field(
description="The strike price of the contract.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "category"},
},
)
settlement_price: list[float | None] = Field(
default_factory=list,
description="The settlement price of the contract.",
json_schema_extra={"x-unit_measurement": "currency"},
option_type: list[str] = Field(
description="Whether the contract is a call or a put.",
json_schema_extra={
"x-widget_config": {"headerName": "Type", "chartDataType": "excluded"},
},
)
min_price: list[float | None] = Field(
default_factory=list,
description="The minimum price allowed.",
json_schema_extra={"x-unit_measurement": "currency"},
mark: list[float | None] = Field(
description="The mark price of the contract.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
max_price: list[float | None] = Field(
default_factory=list,
description="The maximum price allowed.",
json_schema_extra={"x-unit_measurement": "currency"},
bid: list[float | None] = Field(
description="The highest price bid.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
bid_size: list[int | float | None] = Field(
description="The size resting at the best bid.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
ask: list[float | None] = Field(
description="The lowest price offered.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
ask_size: list[int | float | None] = Field(
description="The size resting at the best offer.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
last_trade_price: list[float | None] = Field(
description="The price the contract last traded at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
open_interest: list[int | float | None] = Field(
description="The contracts left outstanding.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume: list[int | float | None] = Field(
description="The volume of the session.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume_notional: list[float | None] = Field(
description="The notional volume of the contract, in USD or USDC.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
high: list[float | None] = Field(
description="The highest price of the session.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
low: list[float | None] = Field(
description="The lowest price of the session.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
change_percent: list[float | None] = Field(
description="The price change over the session.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
implied_volatility: list[float | None] = Field(
description="The volatility implied by the mark price.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Mark IV", "chartDataType": "excluded"},
},
)
bid_iv: list[float | None] = Field(
description="The volatility implied by the bid price.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Bid IV", "chartDataType": "excluded"},
},
)
ask_iv: list[float | None] = Field(
description="The volatility implied by the ask price.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Ask IV", "chartDataType": "excluded"},
},
)
delta: list[float | None] = Field(
description="The sensitivity to the underlying's price.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
gamma: list[float | None] = Field(
description="The sensitivity of delta to the underlying's price.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
theta: list[float | None] = Field(
description="The sensitivity to the passage of time.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
vega: list[float | None] = Field(
description="The sensitivity to implied volatility.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
rho: list[float | None] = Field(
description="The sensitivity to the interest rate.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
interest_rate: list[float | None] = Field(
description="The rate the exchange prices the greeks at.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
settlement_price: list[float | None] = Field(
description="The price the contract last settled at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
min_price: list[float | None] = Field(
description="The lowest price an order will be accepted at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_price: list[float | None] = Field(
description="The highest price an order will be accepted at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
underlying_symbol: list[str | None] = Field(
description="The future or index the contract is priced against.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
underlying_price: list[float | None] = Field(
description="The price of the underlying the contract is priced against.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
underlying_spot_price: list[float] = Field(
description="The spot price of the underlying the option is based on.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
contract_size: list[int | float | None] = Field(
description="The size of one contract, in units of the underlying.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
is_inverse: list[bool | None] = Field(
default_factory=list,
description="The notional trading volume of the contract, as USD or USDC.",
json_schema_extra={"x-unit_measurement": "currency"},
description="Whether the contract settles in the underlying rather than"
+ " in the quote currency, which changes how its payoff is valued.",
json_schema_extra={
"x-widget_config": {
"headerName": "Is Inverse",
"cellDataType": "boolean",
"chartDataType": "excluded",
}
},
)
timestamp: list[datetime] = Field(
description="The datetime of the data, as America/New_York time.",
description="When the quote was published, as America/New_York time.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded"},
},
)
eod_date: list[dateType | None] = Field(
default_factory=list,
description="The end-of-day date. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {
"headerName": "EOD Date",
"chartDataType": "excluded",
"hide": True,
}
},
)
theoretical_price: list[float | None] = Field(
default_factory=list,
description="The theoretical price of the contract. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
last_trade_size: list[int | float | None] = Field(
default_factory=list,
description="The size of the last trade. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
last_trade_time: list[datetime | None] = Field(
default_factory=list,
description="When the last trade printed. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
tick: list[str | None] = Field(
default_factory=list,
description="The direction of the last price move. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
bid_time: list[datetime | None] = Field(
default_factory=list,
description="When the bid was published. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
bid_exchange: list[str | None] = Field(
default_factory=list,
description="The exchange the bid came from. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
ask_time: list[datetime | None] = Field(
default_factory=list,
description="When the offer was published. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
ask_exchange: list[str | None] = Field(
default_factory=list,
description="The exchange the offer came from. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
open: list[float | None] = Field(
default_factory=list,
description="The opening price of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
open_bid: list[float | None] = Field(
default_factory=list,
description="The opening bid of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
open_ask: list[float | None] = Field(
default_factory=list,
description="The opening offer of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
bid_high: list[float | None] = Field(
default_factory=list,
description="The highest bid of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
ask_high: list[float | None] = Field(
default_factory=list,
description="The highest offer of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
bid_low: list[float | None] = Field(
default_factory=list,
description="The lowest bid of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
ask_low: list[float | None] = Field(
default_factory=list,
description="The lowest offer of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close: list[float | None] = Field(
default_factory=list,
description="The closing price of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_size: list[int | float | None] = Field(
default_factory=list,
description="The size of the closing trade. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_time: list[datetime | None] = Field(
default_factory=list,
description="When the closing trade printed. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_bid: list[float | None] = Field(
default_factory=list,
description="The closing bid of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_bid_size: list[int | float | None] = Field(
default_factory=list,
description="The size at the closing bid. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_bid_time: list[datetime | None] = Field(
default_factory=list,
description="When the closing bid was published. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_ask: list[float | None] = Field(
default_factory=list,
description="The closing offer of the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_ask_size: list[int | float | None] = Field(
default_factory=list,
description="The size at the closing offer. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
close_ask_time: list[datetime | None] = Field(
default_factory=list,
description="When the closing offer was published. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
prev_close: list[float | None] = Field(
default_factory=list,
description="The closing price of the previous session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
change: list[float | None] = Field(
default_factory=list,
description="The price change over the session. Deribit publishes none.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
@field_validator(
"interest_rate",
mode="before",
check_fields=False,
)
@classmethod
def as_percent(cls, v):
"""Return a rate the exchange sends as a fraction in percent units."""
return v * 100 if v is not None else None
class DeribitOptionsChainsFetcher(
Fetcher[DeribitOptionsChainsQueryParams, DeribitOptionsChainsData]
):
"""Deribit Options Chains Fetcher."""
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitOptionsChainsQueryParams:
"""Transform the query parameters."""
"""Transform the query."""
return DeribitOptionsChainsQueryParams(**params)
@staticmethod
@@ -119,126 +532,87 @@ class DeribitOptionsChainsFetcher(
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Extract the data."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
import json
import websockets
from openbb_deribit.utils.helpers import get_options_symbols
from pandas import to_datetime
from websockets.asyncio.client import connect
"""Return the raw data from the Deribit websocket.
Raises
------
OpenBBError
If every expiration failed.
EmptyDataError
If the subscriptions returned nothing at all.
"""
import asyncio
from warnings import warn
# We need to identify each option contract in order to fetch the chains data.
symbols_dict: dict[str, str] = {}
from openbb_core.app.model.abstract.error import OpenBBError
from pandas import to_datetime
try:
symbols_dict = await get_options_symbols(query.symbol) # type: ignore
except OpenBBError as e:
raise OpenBBError(e) from e
from openbb_deribit.utils.helpers import (
flatten_ticker,
get_instruments,
get_options_symbols,
)
from openbb_deribit.utils.websocket import subscribe_tickers
# For each expiration, we need to create a websocket connection to fetch the data.
# We subscribe to each contract symbol and break the connection when we have all the data for an expiry.
# If it takes too long, we break the connection and return an error message.
results: list = []
by_expiration = await get_options_symbols(query.symbol)
specs = {
str(d["instrument_name"]): d for d in await get_instruments("any", "option")
}
messages: set = set()
subscriptions = await asyncio.gather(
*(
subscribe_tickers(symbols, messages)
for symbols in by_expiration.values()
),
return_exceptions=True,
)
today = to_datetime("today").date()
results: list[dict] = []
async def call_api(expiration):
"""Call the Deribit API."""
symbols = symbols_dict[expiration]
received_symbols: set = set()
msg = {
"jsonrpc": "2.0",
"id": 3600,
"method": "public/subscribe",
"params": {"channels": ["ticker." + d + ".100ms" for d in symbols]},
}
async with connect("wss://www.deribit.com/ws/api/v2") as websocket:
await websocket.send(json.dumps(msg))
try:
await asyncio.wait_for(
receive_data(websocket, symbols, received_symbols), timeout=2.0
)
except asyncio.TimeoutError:
messages.add(f"Timeout reached for {expiration}, data incomplete.")
for subscription in subscriptions:
if not isinstance(subscription, dict):
continue
async def receive_data(websocket, symbols, received_symbols):
"""Receive the data from the websocket with a timeout."""
while True:
try:
response = await websocket.recv()
except websockets.ConnectionClosed:
break
data = json.loads(response)
for symbol, ticker in subscription.items():
parts = symbol.split("-")
strike = parts[2].replace("d", ".")
flat = flatten_ticker(ticker)
flat.pop("state", None)
published = flat.pop("timestamp", None)
if "params" not in data:
continue
if flat.get("underlying_index") == "index_price":
flat["underlying_index"] = parts[0].replace("_", "-")
if "error" in data and data.get("error"):
messages.add(f"Error while receiving data -> {data['error']}")
break
if flat.get("estimated_delivery_price") == flat.get("index_price"):
flat.pop("estimated_delivery_price", None)
res = data.get("params", {}).get("data", {})
symbol = res.get("instrument_name")
# While we are handling the data, we will parse the message.
if symbol not in received_symbols:
received_symbols.add(symbol)
stats = res.pop("stats", {})
greeks = res.pop("greeks", {})
timestamp = res.pop("timestamp", None)
underlying_symbol = res.get("underlying_index")
if underlying_symbol == "index_price":
res["underlying_index"] = symbol.split("-")[0].replace("_", "-")
res["timestamp"] = to_datetime(
timestamp, unit="ms", utc=True
).tz_convert("America/New_York")
if res.get("estimated_delivery_price") == res.get("index_price"):
_ = res.pop("estimated_delivery_price", None)
_ = res.pop("state", None)
result = {
"expiration": to_datetime(symbol.split("-")[1]).date(),
"strike": (
float(symbol.split("-")[2].replace("d", "."))
if "d" in symbol.split("-")[2]
else int(symbol.split("-")[2])
),
"option_type": (
"call"
if symbol.endswith("-C")
else "put" if symbol.endswith("-P") else None
),
**res,
**stats,
**greeks,
expiration = to_datetime(parts[1]).date()
spec = specs.get(symbol, {})
results.append(
{
"expiration": expiration,
"strike": float(strike),
"option_type": "call" if symbol.endswith("-C") else "put",
"dte": (expiration - today).days,
"contract_size": spec.get("contract_size"),
"is_inverse": spec.get("instrument_type") == "reversed",
"timestamp": to_datetime(
published, unit="ms", utc=True
).tz_convert("America/New_York"),
**flat,
}
result["dte"] = (
result["expiration"] - to_datetime("today").date()
).days
results.append(result)
if len(received_symbols) == len(symbols):
await websocket.close()
break
tasks = [
asyncio.create_task(call_api(expiration)) for expiration in symbols_dict
]
await asyncio.gather(*tasks, return_exceptions=True)
)
if messages and not results:
raise OpenBBError(", ".join(messages))
raise OpenBBError(", ".join(sorted(messages)))
if results and messages:
for message in messages:
warn(message)
for message in sorted(messages):
warn(message)
if not results and not messages:
raise EmptyDataError("All requests returned empty with no error messages.")
if not results:
raise EmptyDataError(
f"Deribit published no option quotes for {query.symbol}."
)
return results
@@ -248,34 +622,22 @@ class DeribitOptionsChainsFetcher(
data: list[dict],
**kwargs: Any,
) -> DeribitOptionsChainsData:
"""Transform the data."""
# pylint: disable=import-outside-toplevel
"""Transform the data to the model.
Contracts on BTC and ETH are quoted in the underlying, so their prices are
carried to USD at the index level the quote was published against.
"""
from numpy import nan
from pandas import DataFrame
df = DataFrame(data)
frame = DataFrame(data)
inverse = bool(frame["is_inverse"].any())
# For BTC and ETH options, we need to convert price units to USD.
for col in df.columns:
if col in [
"last_price",
"settlement_price",
"mark_price",
"min_price",
"max_price",
"best_ask_price",
"best_bid_price",
"high",
"low",
] and query.symbol.upper() in ["BTC", "ETH"]:
df[col] = (df[col].astype(float) * df["index_price"]).round(2)
elif col in ["price_change", "mark_iv", "bid_iv", "ask_iv"]:
df[col] = df[col].astype(float) / 100
for column in frame.columns:
if column in PRICE_FIELDS and inverse:
frame[column] = frame[column].astype(float) * frame["index_price"]
df = df.replace({nan: None})
df = df.sort_values(["expiration", "strike", "option_type"])
df = df.reset_index(drop=True)
df["contract_size"] = 1
results = df.to_dict(orient="list")
return DeribitOptionsChainsData.model_validate(results)
frame = frame.replace({nan: None})
frame = frame.sort_values(["expiration", "strike", "option_type"])
frame = frame.reset_index(drop=True)
return DeribitOptionsChainsData.model_validate(frame.to_dict(orient="list"))

View File

@@ -0,0 +1,261 @@
"""Deribit Order Book Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
ORDER_BOOK_DEPTHS,
SYMBOL_STYLE,
)
class DeribitOrderBookQueryParams(QueryParams):
"""Deribit Order Book Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_order_book
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
},
"depth": {
"x-widget_config": {
"options": [
{"label": str(depth), "value": depth} for depth in ORDER_BOOK_DEPTHS
]
}
},
}
symbol: str | None = Field(
default=None,
description="One or more instrument names. A perpetual can also be given"
+ " by its shortened root, such as 'SOLUSDC'.",
)
instrument_id: int | None = Field(
default=None,
description="The numeric identifier of one instrument, used in place of"
+ " its name.",
)
depth: int = Field(
default=10,
description="The number of levels to return on each side: 1, 5, 10, 20, 50,"
+ " 100, 1000, or 10000.",
)
@field_validator("depth", check_fields=False)
@classmethod
def validate_depth(cls, v):
"""Refuse a depth the exchange does not offer.
Raises
------
ValueError
If the depth is not one Deribit accepts.
"""
if v not in ORDER_BOOK_DEPTHS:
raise ValueError(
f"Deribit returns a book {', '.join(map(str, ORDER_BOOK_DEPTHS))}"
f" levels deep, not {v}."
)
return v
class DeribitOrderBookData(Data):
"""Deribit Order Book Data."""
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
timestamp: datetime | None = Field(
default=None,
description="When the book was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
side: str = Field(
description="Whether the level is a bid or an ask.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
level: int = Field(
description="How far the level sits from the top of the book.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
price: float = Field(
description="The price of the level.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "category", "pinned": "left"},
},
)
size: float = Field(
description="The size resting at the level.",
json_schema_extra={"x-widget_config": {"chartDataType": "series"}},
)
mark_price: float | None = Field(
default=None,
description="The price the exchange marks positions at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
index_price: float | None = Field(
default=None,
description="The price of the index the instrument is priced against.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
change_id: int | None = Field(
default=None,
description="The revision of the book the level came from.",
json_schema_extra={
"x-widget_config": {
"headerName": "Change ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v) if v else None
class DeribitOrderBookFetcher(
Fetcher[DeribitOrderBookQueryParams, list[DeribitOrderBookData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitOrderBookQueryParams:
"""Transform the query.
Raises
------
OpenBBError
If neither a symbol nor an instrument ID was given.
"""
from openbb_core.app.model.abstract.error import OpenBBError
query = DeribitOrderBookQueryParams(**params)
if not query.symbol and query.instrument_id is None:
raise OpenBBError("Give either a symbol or an instrument ID.")
return query
@staticmethod
async def aextract_data(
query: DeribitOrderBookQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the instruments published a book.
"""
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.helpers import get_perpetual_symbols
calls: list[tuple[str, dict[str, Any] | None]]
if query.symbol:
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
calls = [
(
"get_order_book",
{
"instrument_name": perpetuals.get(symbol, symbol),
"depth": query.depth,
},
)
for symbol in symbols
]
else:
calls = [
(
"get_order_book_by_instrument_id",
{
"instrument_id": query.instrument_id,
"depth": query.depth,
},
)
]
results = await gather(calls, use_cache=False)
data = [result for result in results if isinstance(result, dict) and result]
if not data:
raise EmptyDataError("Deribit published no book for the query.")
return data
@staticmethod
def transform_data(
query: DeribitOrderBookQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitOrderBookData]:
"""Transform the data to the model.
Raises
------
EmptyDataError
If every book came back with no resting orders.
"""
records: list[dict] = []
for book in data:
context = {
"symbol": book.get("instrument_name"),
"timestamp": book.get("timestamp"),
"mark_price": book.get("mark_price"),
"index_price": book.get("index_price"),
"change_id": book.get("change_id"),
}
for side in ("bids", "asks"):
for level, entry in enumerate(book.get(side) or [], start=1):
records.append(
{
**context,
"side": side[:-1],
"level": level,
"price": entry[0],
"size": entry[1],
}
)
if not records:
raise EmptyDataError("Deribit published no resting orders for the query.")
return [DeribitOrderBookData.model_validate(record) for record in records]

View File

@@ -0,0 +1,244 @@
"""Deribit Settlements Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
MAX_SETTLEMENT_COUNT,
SYMBOL_STYLE,
Currencies,
SettlementTypes,
)
class DeribitSettlementsQueryParams(QueryParams):
"""Deribit Settlements Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_last_settlements_by_currency
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str | None = Field(
default=None,
description="One or more instrument names. When given, the currency is"
+ " ignored.",
)
currency: Currencies = Field(
default="BTC", description="The settlement currency of the instruments."
)
settlement_type: SettlementTypes | None = Field(
default=None,
description="The kind of settlement. Default is all of them.",
)
start_date: Any | None = Field(
default=None, description="Search back no further than this date."
)
limit: int = Field(
default=100,
description="The number of settlements to return, at most"
f" {MAX_SETTLEMENT_COUNT}.",
ge=1,
le=MAX_SETTLEMENT_COUNT,
)
class DeribitSettlementsData(Data):
"""Deribit Settlements Data."""
__alias_dict__ = {"symbol": "instrument_name", "settlement_type": "type"}
timestamp: datetime = Field(
description="When the instrument settled.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
symbol: str | None = Field(
default=None,
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
settlement_type: str | None = Field(
default=None,
description="Whether the event was a settlement or a delivery.",
json_schema_extra={
"x-widget_config": {"headerName": "Type", "chartDataType": "excluded"}
},
)
position: float | None = Field(
default=None,
description="The size that settled.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
index_price: float | None = Field(
default=None,
description="The level of the index at settlement.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
mark_price: float | None = Field(
default=None,
description="The price the instrument settled at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
profit_loss: float | None = Field(
default=None,
description="The profit and loss the settlement realized.",
json_schema_extra={
"x-widget_config": {
"headerName": "Profit & Loss",
"chartDataType": "excluded",
"renderFn": "greenRed",
}
},
)
session_profit_loss: float | None = Field(
default=None,
description="The profit and loss of the whole session.",
json_schema_extra={
"x-widget_config": {
"headerName": "Session Profit & Loss",
"chartDataType": "excluded",
"renderFn": "greenRed",
}
},
)
funding: float | None = Field(
default=None,
description="The funding the session paid.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
session_bankruptcy: float | None = Field(
default=None,
description="The loss the session could not cover.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
session_tax: float | None = Field(
default=None,
description="The socialised loss the session was charged.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
session_tax_rate: float | None = Field(
default=None,
description="The rate the socialised loss was charged at.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
socialized: float | None = Field(
default=None,
description="The loss spread across other accounts.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitSettlementsFetcher(
Fetcher[DeribitSettlementsQueryParams, list[DeribitSettlementsData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitSettlementsQueryParams:
"""Transform the query."""
return DeribitSettlementsQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitSettlementsQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If nothing settled over the span.
"""
from openbb_deribit.utils.client import gather, request
from openbb_deribit.utils.helpers import get_perpetual_symbols, to_timestamp
shared = {
"type": query.settlement_type,
"count": query.limit,
"search_start_timestamp": (
to_timestamp(query.start_date) if query.start_date else None
),
}
if query.symbol:
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
results = await gather(
[
(
"get_last_settlements_by_instrument",
{
"instrument_name": perpetuals.get(symbol, symbol),
**shared,
},
)
for symbol in symbols
]
)
data = [
record
for result in results
if isinstance(result, dict)
for record in result.get("settlements") or []
]
else:
result = await request(
"get_last_settlements_by_currency",
{"currency": query.currency.upper(), **shared},
)
data = (result or {}).get("settlements") or []
if not data:
raise EmptyDataError("Deribit reports nothing settled over the span.")
return data
@staticmethod
def transform_data(
query: DeribitSettlementsQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitSettlementsData]:
"""Transform the data to the model."""
return [
DeribitSettlementsData.model_validate(record)
for record in sorted(
data, key=lambda d: d.get("timestamp") or 0, reverse=True
)
]

View File

@@ -0,0 +1,401 @@
"""Deribit Ticker Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
SYMBOL_STYLE,
)
class DeribitTickerQueryParams(QueryParams):
"""Deribit Ticker Query.
Source: https://docs.deribit.com/api-reference/market-data/public-ticker
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str = Field(
description="One or more instrument names. A perpetual can also be given"
+ " by its shortened root, such as 'SOLUSDC'."
)
class DeribitTickerData(Data):
"""Deribit Ticker Data."""
__alias_dict__ = {
"symbol": "instrument_name",
"change_percent": "price_change",
"implied_volatility": "mark_iv",
"bid": "best_bid_price",
"ask": "best_ask_price",
"bid_size": "best_bid_amount",
"ask_size": "best_ask_amount",
"last_price": "last_price",
}
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
timestamp: datetime | None = Field(
default=None,
description="When the quote was published.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
state: str | None = Field(
default=None,
description="Whether the instrument's book is open.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
combo_state: str | None = Field(
default=None,
description="Whether the combo is still tradeable.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
last_price: float | None = Field(
default=None,
description="The price the instrument last traded at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
bid: float | None = Field(
default=None,
description="The highest price bid.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
ask: float | None = Field(
default=None,
description="The lowest price offered.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
bid_size: float | None = Field(
default=None,
description="The size resting at the best bid.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
ask_size: float | None = Field(
default=None,
description="The size resting at the best offer.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
implied_bid: float | None = Field(
default=None,
description="The bid implied by the legs of a combo.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
implied_ask: float | None = Field(
default=None,
description="The offer implied by the legs of a combo.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
mark_price: float | None = Field(
default=None,
description="The price the exchange marks positions at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
index_price: float | None = Field(
default=None,
description="The price of the index the instrument is priced against.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
settlement_price: float | None = Field(
default=None,
description="The price the instrument last settled at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
delivery_price: float | None = Field(
default=None,
description="The price an expired instrument was delivered at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
estimated_delivery_price: float | None = Field(
default=None,
description="What the instrument would deliver at right now.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
min_price: float | None = Field(
default=None,
description="The lowest price an order will be accepted at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
max_price: float | None = Field(
default=None,
description="The highest price an order will be accepted at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
high: float | None = Field(
default=None,
description="The highest price of the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
low: float | None = Field(
default=None,
description="The lowest price of the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
change_percent: float | None = Field(
default=None,
description="The price change over the last 24 hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume: float | None = Field(
default=None,
description="The volume of the last 24 hours, in base currency.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
volume_notional: float | None = Field(
default=None,
description="The volume of the last 24 hours, in quote currency.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
volume_usd: float | None = Field(
default=None,
description="The volume of the last 24 hours, in USD.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {
"headerName": "Volume USD",
"chartDataType": "excluded",
},
},
)
open_interest: float | None = Field(
default=None,
description="The contracts left outstanding.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
current_funding: float | None = Field(
default=None,
description="The funding rate a perpetual is paying right now.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
funding_8h: float | None = Field(
default=None,
description="The funding rate a perpetual paid over eight hours.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {
"headerName": "Funding 8H",
"chartDataType": "excluded",
},
},
)
interest_value: float | None = Field(
default=None,
description="The value realized funding on a perpetual is derived from.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
interest_rate: float | None = Field(
default=None,
description="The rate the exchange prices the option's greeks at.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
underlying_index: str | None = Field(
default=None,
description="The future or index the option is priced against.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
underlying_price: float | None = Field(
default=None,
description="The price of the underlying the option is priced against.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
implied_volatility: float | None = Field(
default=None,
description="The volatility implied by the mark price.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Mark IV", "chartDataType": "excluded"},
},
)
bid_iv: float | None = Field(
default=None,
description="The volatility implied by the best bid.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Bid IV", "chartDataType": "excluded"},
},
)
ask_iv: float | None = Field(
default=None,
description="The volatility implied by the best offer.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "Ask IV", "chartDataType": "excluded"},
},
)
delta: float | None = Field(
default=None,
description="The sensitivity to the underlying's price.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
gamma: float | None = Field(
default=None,
description="The sensitivity of delta to the underlying's price.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
vega: float | None = Field(
default=None,
description="The sensitivity to implied volatility.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
theta: float | None = Field(
default=None,
description="The sensitivity to the passage of time.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
rho: float | None = Field(
default=None,
description="The sensitivity to the interest rate.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v) if v else None
@field_validator(
"current_funding",
"funding_8h",
"interest_rate",
mode="before",
check_fields=False,
)
@classmethod
def as_percent(cls, v):
"""Return a rate the exchange sends as a fraction in percent units."""
return v * 100 if v is not None else None
class DeribitTickerFetcher(Fetcher[DeribitTickerQueryParams, list[DeribitTickerData]]):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitTickerQueryParams:
"""Transform the query."""
return DeribitTickerQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitTickerQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If none of the instruments published a quote.
"""
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.helpers import get_perpetual_symbols
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
resolved = [perpetuals.get(symbol, symbol) for symbol in symbols]
results = await gather(
[("ticker", {"instrument_name": s}) for s in resolved], use_cache=False
)
data = [result for result in results if isinstance(result, dict) and result]
if not data:
raise EmptyDataError(
f"Deribit published no quote for {', '.join(resolved)}."
)
return data
@staticmethod
def transform_data(
query: DeribitTickerQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitTickerData]:
"""Transform the data to the model."""
from openbb_deribit.utils.helpers import flatten_ticker
return [
DeribitTickerData.model_validate(flatten_ticker(record)) for record in data
]

View File

@@ -0,0 +1,222 @@
"""Deribit Trade Volumes Model."""
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field
DAY_VOLUMES = ("futures_volume", "calls_volume", "puts_volume", "spot_volume")
def _traded(record: dict) -> float:
"""Return everything that traded in one currency over the last day."""
return sum(float(record.get(field) or 0) for field in DAY_VOLUMES)
class DeribitTradeVolumesQueryParams(QueryParams):
"""Deribit Trade Volumes Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_trade_volumes
"""
extended: bool = Field(
default=True,
description="When True, adds the seven and thirty day volumes to the"
+ " trailing twenty-four hours.",
)
class DeribitTradeVolumesData(Data):
"""Deribit Trade Volumes Data."""
currency: str = Field(
description="The currency the volume was traded in.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
currency_pair: str | None = Field(
default=None,
description="The index the currency is priced against.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
futures_volume: float | None = Field(
default=None,
description="The futures volume of the last 24 hours.",
json_schema_extra={
"x-widget_config": {
"headerName": "Futures Volume",
"chartDataType": "series",
}
},
)
calls_volume: float | None = Field(
default=None,
description="The call volume of the last 24 hours.",
json_schema_extra={
"x-widget_config": {
"headerName": "Calls Volume",
"chartDataType": "excluded",
}
},
)
puts_volume: float | None = Field(
default=None,
description="The put volume of the last 24 hours.",
json_schema_extra={
"x-widget_config": {
"headerName": "Puts Volume",
"chartDataType": "excluded",
}
},
)
spot_volume: float | None = Field(
default=None,
description="The spot volume of the last 24 hours.",
json_schema_extra={
"x-widget_config": {
"headerName": "Spot Volume",
"chartDataType": "excluded",
}
},
)
futures_volume_7d: float | None = Field(
default=None,
description="The futures volume of the last seven days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Futures Volume 7D",
"chartDataType": "excluded",
}
},
)
calls_volume_7d: float | None = Field(
default=None,
description="The call volume of the last seven days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Calls Volume 7D",
"chartDataType": "excluded",
}
},
)
puts_volume_7d: float | None = Field(
default=None,
description="The put volume of the last seven days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Puts Volume 7D",
"chartDataType": "excluded",
}
},
)
spot_volume_7d: float | None = Field(
default=None,
description="The spot volume of the last seven days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Spot Volume 7D",
"chartDataType": "excluded",
}
},
)
futures_volume_30d: float | None = Field(
default=None,
description="The futures volume of the last thirty days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Futures Volume 30D",
"chartDataType": "excluded",
}
},
)
calls_volume_30d: float | None = Field(
default=None,
description="The call volume of the last thirty days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Calls Volume 30D",
"chartDataType": "excluded",
}
},
)
puts_volume_30d: float | None = Field(
default=None,
description="The put volume of the last thirty days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Puts Volume 30D",
"chartDataType": "excluded",
}
},
)
spot_volume_30d: float | None = Field(
default=None,
description="The spot volume of the last thirty days.",
json_schema_extra={
"x-widget_config": {
"headerName": "Spot Volume 30D",
"chartDataType": "excluded",
}
},
)
class DeribitTradeVolumesFetcher(
Fetcher[DeribitTradeVolumesQueryParams, list[DeribitTradeVolumesData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitTradeVolumesQueryParams:
"""Transform the query."""
return DeribitTradeVolumesQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitTradeVolumesQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the exchange published no volumes.
"""
from openbb_deribit.utils.client import request
data = await request("get_trade_volumes", {"extended": query.extended})
if not data:
raise EmptyDataError("Deribit published no trade volumes.")
return data
@staticmethod
def transform_data(
query: DeribitTradeVolumesQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitTradeVolumesData]:
"""Transform the data to the model.
The busiest currencies come first. Most of the currencies the exchange
lists trade nothing on a given day, and ordering them by name buries the
handful that did under the ones that did not. Volumes are counted in
each currency's own units, so the ranking is by activity rather than by
value.
"""
return [
DeribitTradeVolumesData.model_validate(record)
for record in sorted(
data,
key=lambda d: (-_traded(d), str(d.get("currency"))),
)
]

View File

@@ -0,0 +1,354 @@
"""Deribit Trades Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator, model_validator
from openbb_deribit.utils.constants import (
INSTRUMENT_CHOICES_ENDPOINT,
MAX_TRADE_COUNT,
SYMBOL_STYLE,
ListingCurrencies,
Sorting,
TradeKinds,
)
class DeribitTradesQueryParams(QueryParams):
"""Deribit Trades Query.
Source: https://docs.deribit.com/api-reference/market-data/public-get_last_trades_by_instrument
"""
__json_schema_extra__ = {
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": INSTRUMENT_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
},
}
}
symbol: str | None = Field(
default=None,
description="One or more instrument names. When given, the currency is"
+ " ignored.",
)
currency: ListingCurrencies = Field(
default="BTC", description="The settlement currency of the instruments."
)
kind: TradeKinds = Field(
default="future",
description="The kind of instrument. Only used when reading by currency,"
+ " where the exchange rejects a request that leaves it out, and where"
+ " spot is not offered at all. Read spot trades by symbol instead.",
)
start_date: Any | None = Field(
default=None, description="Return trades from this date onwards."
)
end_date: Any | None = Field(
default=None, description="Return trades up to this date."
)
limit: int = Field(
default=100,
description=f"The number of trades to return, at most {MAX_TRADE_COUNT}.",
ge=1,
le=MAX_TRADE_COUNT,
)
sorting: Sorting = Field(
default="desc", description="The order the trades are returned in."
)
@model_validator(mode="after")
def _options_are_listed_by_underlying(self):
"""Refuse a listing of options scoped to a currency that only settles them."""
from openbb_deribit.utils.helpers import reject_options_by_settlement
if not self.symbol:
reject_options_by_settlement(self.currency, self.kind)
return self
class DeribitTradesData(Data):
"""Deribit Trades Data."""
__alias_dict__ = {"symbol": "instrument_name"}
timestamp: datetime = Field(
description="When the trade printed.",
json_schema_extra={
"x-widget_config": {"chartDataType": "category", "pinned": "left"}
},
)
symbol: str = Field(
description="The name of the instrument.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
direction: str | None = Field(
default=None,
description="Which side the aggressor was on.",
json_schema_extra={
"x-widget_config": {"headerName": "Side", "chartDataType": "excluded"}
},
)
price: float = Field(
description="The price the trade printed at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "series"},
},
)
amount: float | None = Field(
default=None,
description="The size of the trade, in base currency.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
contracts: float | None = Field(
default=None,
description="The size of the trade, in contracts.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
iv: float | None = Field(
default=None,
description="The volatility the option trade implies.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"headerName": "IV", "chartDataType": "excluded"},
},
)
mark_price: float | None = Field(
default=None,
description="The price the exchange marked the instrument at.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
index_price: float | None = Field(
default=None,
description="The price of the index at the time of the trade.",
json_schema_extra={
"x-unit_measurement": "currency",
"x-widget_config": {"chartDataType": "excluded"},
},
)
tick_direction: int | None = Field(
default=None,
description="How the price moved against the previous trade. 0 and 1 are"
+ " upticks, 2 and 3 are downticks.",
json_schema_extra={
"x-widget_config": {"chartDataType": "excluded", "hide": True}
},
)
trade_id: str | None = Field(
default=None,
description="The identifier of the trade.",
json_schema_extra={
"x-widget_config": {
"headerName": "Trade ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
trade_seq: int | None = Field(
default=None,
description="The sequence number of the trade.",
json_schema_extra={
"x-widget_config": {
"headerName": "Trade Seq",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
block_trade_id: str | None = Field(
default=None,
description="The block trade the print belongs to.",
json_schema_extra={
"x-widget_config": {
"headerName": "Block Trade ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
liquidation: str | None = Field(
default=None,
description="Which side of the trade was liquidated.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
starbase_match_id: int | None = Field(
default=None,
description="The matching engine's identifier for the fill.",
json_schema_extra={
"x-widget_config": {
"headerName": "Match ID",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
starbase_timestamp: int | None = Field(
default=None,
description="When the matching engine recorded the fill, in nanoseconds"
+ " since the epoch.",
json_schema_extra={
"x-widget_config": {
"headerName": "Match Timestamp",
"cellDataType": "text",
"chartDataType": "excluded",
"hide": True,
}
},
)
@field_validator("timestamp", mode="before", check_fields=False)
@classmethod
def validate_timestamp(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitTradesFetcher(Fetcher[DeribitTradesQueryParams, list[DeribitTradesData]]):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitTradesQueryParams:
"""Transform the query."""
return DeribitTradesQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitTradesQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
OpenBBError
If the symbol is one of the spot pairs the exchange routes to Coinbase,
which publish no trade history here.
EmptyDataError
If the exchange printed no trades over the span.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_deribit.utils.client import gather, request
from openbb_deribit.utils.constants import ROUTED_SPOT_ERROR
from openbb_deribit.utils.helpers import get_perpetual_symbols, to_timestamp
window = {
"start_timestamp": (
to_timestamp(query.start_date) if query.start_date else None
),
"end_timestamp": to_timestamp(query.end_date) if query.end_date else None,
}
timed = window["start_timestamp"] and window["end_timestamp"]
shared = {"count": query.limit, "sorting": query.sorting}
if query.symbol:
symbols = [s.strip().upper() for s in query.symbol.split(",") if s.strip()]
perpetuals = await get_perpetual_symbols()
method = (
"get_last_trades_by_instrument_and_time"
if timed
else "get_last_trades_by_instrument"
)
results = await gather(
[
(
method,
{
"instrument_name": perpetuals.get(symbol, symbol),
**window,
**shared,
},
)
for symbol in symbols
],
use_cache=False,
)
data = [
trade
for result in results
if isinstance(result, dict)
for trade in result.get("trades") or []
]
if not data and any(
ROUTED_SPOT_ERROR in str(result)
for result in results
if isinstance(result, Exception)
):
raise OpenBBError(
f"Deribit publishes no trade history for {', '.join(symbols)}."
" Spot pairs it routes to Coinbase carry none."
)
else:
method = (
"get_last_trades_by_currency_and_time"
if timed
else "get_last_trades_by_currency"
)
try:
result = await request(
method,
{
"currency": query.currency.upper(),
"kind": query.kind,
**window,
**shared,
},
use_cache=False,
)
except OpenBBError as error:
if ROUTED_SPOT_ERROR not in str(error):
raise
raise OpenBBError(
"Deribit publishes no trade history for the spot pairs it"
" routes to Coinbase. Read the other kinds by currency, or"
" a specific spot pair by symbol."
) from error
data = (result or {}).get("trades") or []
if not data:
raise EmptyDataError("Deribit printed no trades matching the query.")
return data
@staticmethod
def transform_data(
query: DeribitTradesQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitTradesData]:
"""Transform the data to the model."""
return [
DeribitTradesData.model_validate(record)
for record in sorted(
data,
key=lambda d: d.get("timestamp") or 0,
reverse=query.sorting != "asc",
)
]

View File

@@ -0,0 +1,216 @@
"""Deribit Volatility Index Model."""
from datetime import datetime
from typing import Any
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from pydantic import Field, field_validator
from openbb_deribit.utils.constants import (
VOLATILITY_INDEX_CURRENCIES,
VolatilityResolutions,
)
class DeribitVolatilityIndexQueryParams(QueryParams):
"""Deribit Volatility Index Query.
Deribit computes DVOL for BTC and ETH only. Asking for it on a pegged
currency returns an empty series, so none are offered.
Source: https://docs.deribit.com/api-reference/market-data/public-get_volatility_index_data
"""
__json_schema_extra__ = {
"currency": {
"multiple_items_allowed": True,
"x-widget_config": {
"options": [
{"label": name, "value": name}
for name in VOLATILITY_INDEX_CURRENCIES
]
},
}
}
currency: str = Field(
default="BTC", description="The currency whose volatility index to read."
)
@field_validator("currency", mode="before")
@classmethod
def _listed_currencies(cls, value):
"""Accept one currency or several, and refuse any the exchange omits.
Raises
------
ValueError
If a currency the exchange does not publish this for is asked for.
"""
chosen = [
item.strip().upper() for item in str(value).split(",") if item.strip()
]
unlisted = [item for item in chosen if item not in VOLATILITY_INDEX_CURRENCIES]
if unlisted:
raise ValueError(
f"Deribit publishes no volatility index for {unlisted}."
f" It publishes {list(VOLATILITY_INDEX_CURRENCIES)}."
)
return ",".join(chosen)
start_date: Any | None = Field(
default=None, description="Return candles from this date onwards."
)
end_date: Any | None = Field(
default=None, description="Return candles up to this date."
)
interval: VolatilityResolutions = Field(
default="1d", description=QUERY_DESCRIPTIONS.get("interval", "")
)
class DeribitVolatilityIndexData(Data):
"""Deribit Volatility Index Data."""
date: datetime = Field(
description="When the candle closed.",
json_schema_extra={"x-widget_config": {"chartDataType": "category"}},
)
currency: str = Field(
description="The currency the index belongs to.",
json_schema_extra={"x-widget_config": {"chartDataType": "excluded"}},
)
open: float = Field(
description="The level the candle opened at.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
high: float = Field(
description="The highest level of the candle.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
low: float = Field(
description="The lowest level of the candle.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "excluded"},
},
)
close: float = Field(
description="The level the candle closed at.",
json_schema_extra={
"x-unit_measurement": "percent",
"x-widget_config": {"chartDataType": "series"},
},
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def validate_date(cls, v):
"""Read the timestamp as a datetime."""
from openbb_deribit.utils.helpers import from_timestamp
return from_timestamp(v)
class DeribitVolatilityIndexFetcher(
Fetcher[DeribitVolatilityIndexQueryParams, list[DeribitVolatilityIndexData]]
):
"""Transform the query, extract and transform the data from the Deribit endpoint."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> DeribitVolatilityIndexQueryParams:
"""Transform the query."""
return DeribitVolatilityIndexQueryParams(**params)
@staticmethod
async def aextract_data(
query: DeribitVolatilityIndexQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
"""Return the raw data from the Deribit endpoint.
Raises
------
EmptyDataError
If the index published no candles over the span.
"""
from datetime import (
datetime as dt,
timedelta,
timezone,
)
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.constants import VOLATILITY_RESOLUTION_MAP
from openbb_deribit.utils.helpers import to_timestamp
now = dt.now(timezone.utc)
start = (
to_timestamp(query.start_date)
if query.start_date
else to_timestamp(now - timedelta(days=30))
)
end = to_timestamp(query.end_date) if query.end_date else to_timestamp(now)
currencies = [c.strip().upper() for c in query.currency.split(",") if c.strip()]
results = await gather(
[
(
"get_volatility_index_data",
{
"currency": currency,
"start_timestamp": start,
"end_timestamp": end,
"resolution": VOLATILITY_RESOLUTION_MAP[query.interval],
},
)
for currency in currencies
]
)
data = [
{
"date": candle[0],
"currency": currency,
"open": candle[1],
"high": candle[2],
"low": candle[3],
"close": candle[4],
}
for currency, result in zip(currencies, results)
if isinstance(result, dict)
for candle in result.get("data") or []
]
if not data:
raise EmptyDataError(
f"Deribit published no volatility index candles for"
f" {', '.join(currencies)} over the span."
)
return data
@staticmethod
def transform_data(
query: DeribitVolatilityIndexQueryParams,
data: list[dict],
**kwargs: Any,
) -> list[DeribitVolatilityIndexData]:
"""Transform the data to the model."""
return [
DeribitVolatilityIndexData.model_validate(record)
for record in sorted(data, key=lambda d: (d["date"], d["currency"]))
]

View File

@@ -0,0 +1 @@
"""Deribit sub-routers."""

View File

@@ -0,0 +1,181 @@
"""Deribit Futures sub-router."""
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
from openbb_deribit import DERIVATIVES_INSTALLED
router = Router(prefix="/futures", description="Deribit futures data.")
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get the underlyings with a listed futures curve.",
parameters={},
)
],
)
async def curve_choices() -> list[dict[str, str]]:
"""``[{label, value}]`` of every underlying with a listed futures curve."""
from openbb_deribit.utils.choices import futures_root_choices
return await futures_root_choices()
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[APIEx(description="Get the listed perpetuals.", parameters={})],
)
async def perpetual_choices() -> list[dict[str, str]]:
"""``[{label, value}]`` of every perpetual the exchange lists."""
from openbb_deribit.utils.choices import perpetual_choices as choices
return await choices()
if not DERIVATIVES_INSTALLED:
@router.command(
model="DeribitFuturesInstruments",
widget_config={
"name": "Deribit Futures Instruments",
"description": "The contract specification of every listed future.",
"category": "Crypto",
"subCategory": "Futures",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
},
examples=[
APIEx(
description="Every future Deribit lists.",
parameters={"provider": "deribit"},
)
],
)
async def instruments(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the contract specification of every listed Deribit future."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitFuturesCurve",
widget_config={
"name": "Deribit Futures Curve",
"description": "The term structure of an underlying's dated futures,"
+ " optionally against how it stood some hours ago.",
"category": "Crypto",
"subCategory": "Futures",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
"params": [{"paramName": "symbol", "value": "BTC"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The current BTC futures term structure.",
parameters={"symbol": "BTC", "provider": "deribit"},
),
APIEx(
description="The curve against where it stood a day ago.",
parameters={"symbol": "BTC", "hours_ago": 24, "provider": "deribit"},
),
],
)
async def curve(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the term structure of a Deribit underlying's dated futures."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitFuturesHistorical",
widget_config={
"name": "Deribit Historical Prices",
"description": "Candles for any listed instrument, not only futures.",
"category": "Crypto",
"subCategory": "Futures",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="Daily candles for the BTC perpetual.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
APIEx(
description="Hourly candles over a named span.",
parameters={
"symbol": "BTC-PERPETUAL",
"interval": "1h",
"start_date": "2026-09-01",
"end_date": "2026-09-15",
"provider": "deribit",
},
),
],
)
async def historical(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get candles for any listed Deribit instrument."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitFuturesInfo",
widget_config={
"name": "Deribit Futures Info",
"description": "The live quote and session statistics of a perpetual or"
+ " dated future.",
"category": "Crypto",
"subCategory": "Futures",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 8},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
},
examples=[
APIEx(
description="The live quote of the BTC perpetual.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
)
],
)
async def info(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the live quote and session statistics of a Deribit future."""
return await OBBject.from_query(OBBQuery(**locals()))

View File

@@ -0,0 +1,116 @@
"""Deribit Index sub-router."""
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
router = Router(prefix="/index", description="Deribit index data.")
@router.command(
model="DeribitIndexPrice",
widget_config={
"name": "Deribit Index Price",
"description": "The current level of an index, and what it would deliver at.",
"category": "Crypto",
"subCategory": "Index",
"source": ["Deribit"],
"gridData": {"w": 20, "h": 6},
"params": [{"paramName": "index_name", "value": "btc_usd"}],
},
examples=[
APIEx(
description="The current level of the BTC index.",
parameters={"index_name": "btc_usd", "provider": "deribit"},
),
APIEx(
description="Multiple indexes are comma-separated.",
parameters={"index_name": "btc_usd,eth_usd", "provider": "deribit"},
),
],
)
async def price(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the current level of a Deribit index."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitIndexHistorical",
widget_config={
"name": "Deribit Index History",
"description": "The published level of an index over a span.",
"category": "Crypto",
"subCategory": "Index",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"params": [{"paramName": "index_name", "value": "btc_usd"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The BTC index over the last day.",
parameters={"index_name": "btc_usd", "provider": "deribit"},
),
APIEx(
description="The whole published history.",
parameters={"index_name": "btc_usd", "span": "all", "provider": "deribit"},
),
],
)
async def historical(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the published level of a Deribit index over a span."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitDeliveryPrices",
widget_config={
"name": "Deribit Delivery Prices",
"description": "The level each index has delivered at.",
"category": "Crypto",
"subCategory": "Index",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"params": [{"paramName": "index_name", "value": "btc_usd"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The level the BTC index has delivered at.",
parameters={"index_name": "btc_usd", "provider": "deribit"},
)
],
)
async def delivery_prices(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the level a Deribit index has delivered at."""
return await OBBject.from_query(OBBQuery(**locals()))

View File

@@ -0,0 +1,256 @@
"""Deribit Market sub-router."""
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
router = Router(prefix="/market", description="Deribit market data.")
@router.command(
model="DeribitBookSummary",
widget_config={
"name": "Deribit Book Summary",
"description": "The top of book and trailing session statistics of every"
+ " instrument in a currency.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
"params": [{"paramName": "currency", "value": "BTC"}],
},
examples=[
APIEx(
description="Every BTC instrument's top of book.",
parameters={"currency": "BTC", "provider": "deribit"},
),
APIEx(
description="Only the futures.",
parameters={"currency": "BTC", "kind": "future", "provider": "deribit"},
),
APIEx(
description="One named instrument.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
],
)
async def book_summary(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the top of book and session statistics of Deribit instruments."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitOrderBook",
widget_config={
"name": "Deribit Order Book",
"description": "The resting depth of one instrument, one row per level.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 20, "h": 15},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
"data": {
"table": {
"chartView": {"enabled": False},
}
},
},
examples=[
APIEx(
description="The resting depth of the BTC perpetual.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
APIEx(
description="A book read by its numeric instrument identifier.",
parameters={"instrument_id": 210838, "provider": "deribit"},
),
],
)
async def order_book(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the resting depth of a Deribit instrument, one row per level."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitTicker",
widget_config={
"name": "Deribit Ticker",
"description": "The live quote, session statistics, and greeks of one"
+ " instrument.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 8},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
},
examples=[
APIEx(
description="The live quote of the BTC perpetual.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
APIEx(
description="Multiple instruments are comma-separated.",
parameters={"symbol": "BTC-PERPETUAL,ETH-PERPETUAL", "provider": "deribit"},
),
],
)
async def ticker(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the live quote, session statistics, and greeks of a Deribit instrument."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitTrades",
widget_config={
"name": "Deribit Trades",
"description": "The prints that crossed, by instrument or across a currency.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
"data": {
"table": {
"chartView": {"enabled": False},
}
},
},
examples=[
APIEx(
description="The most recent prints on the BTC perpetual.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
APIEx(
description="Every BTC option print over a span.",
parameters={
"currency": "BTC",
"kind": "option",
"start_date": "2026-09-01",
"end_date": "2026-09-02",
"provider": "deribit",
},
),
],
)
async def trades(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the Deribit prints that crossed, by instrument or across a currency."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitTradeVolumes",
widget_config={
"name": "Deribit Trade Volumes",
"description": "Traded volume by currency and product, over one, seven,"
+ " and thirty days.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 12},
},
examples=[
APIEx(
description="Traded volume by currency and product.",
parameters={"provider": "deribit"},
)
],
)
async def trade_volumes(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get Deribit's traded volume by currency and product."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitBlockRfqTrades",
widget_config={
"name": "Deribit Block RFQ Trades",
"description": "The block requests that traded, one row per leg.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
},
examples=[
APIEx(
description="The block requests that most recently traded.",
parameters={"provider": "deribit"},
)
],
)
async def block_rfq_trades(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the Deribit block requests that traded, one row per leg."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitSettlements",
widget_config={
"name": "Deribit Settlements",
"description": "The settlements, deliveries, and bankruptcies the exchange"
+ " has processed.",
"category": "Crypto",
"subCategory": "Market Data",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
"params": [{"paramName": "currency", "value": "BTC"}],
},
examples=[
APIEx(
description="The most recent BTC settlements.",
parameters={"currency": "BTC", "provider": "deribit"},
),
APIEx(
description="Only the deliveries of one instrument.",
parameters={
"symbol": "BTC-PERPETUAL",
"settlement_type": "settlement",
"provider": "deribit",
},
),
],
)
async def settlements(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the settlements, deliveries, and bankruptcies Deribit has processed."""
return await OBBject.from_query(OBBQuery(**locals()))

View File

@@ -0,0 +1,992 @@
"""Deribit Options sub-router."""
import logging
from typing import Annotated, Any, Literal
from fastapi import Query as FastAPIQuery
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
from openbb_deribit import DERIVATIVES_INSTALLED
from openbb_deribit.utils.constants import (
EXPIRATION_CHOICES_ENDPOINT,
STRATEGY_CHOICES_ENDPOINT,
STRIKE_CHOICES_ENDPOINT,
SYMBOL_STYLE,
UNDERLYING_CHOICES_ENDPOINT,
)
_logger = logging.getLogger(__name__)
router = Router(prefix="/options", description="Deribit options data and analysis.")
_UNDERLYING = {
"x-widget_config": {
"groupId": "deribit_underlying",
"type": "endpoint",
"optionsEndpoint": UNDERLYING_CHOICES_ENDPOINT,
"style": SYMBOL_STYLE,
}
}
def _underlying() -> Any:
"""Build the shared, group-linked underlying parameter."""
return FastAPIQuery(
description="The underlying root, as it appears in the instrument name.",
json_schema_extra=_UNDERLYING,
)
def _excluded_query(description: str = "") -> Any:
"""Build a query parameter that is accepted but hidden from the widget UI."""
return FastAPIQuery(
description=description,
json_schema_extra={"x-widget_config": {"exclude": True}},
)
def _expiry_query(description: str, multi: bool = False) -> Any:
"""Build an expiration query parameter backed by the expiration choices."""
config: dict = {
"type": "endpoint",
"optionsEndpoint": EXPIRATION_CHOICES_ENDPOINT,
"optionsParams": {"symbol": "$symbol"},
"style": SYMBOL_STYLE,
}
if multi:
config["multiSelect"] = True
return FastAPIQuery(
description=description, json_schema_extra={"x-widget_config": config}
)
def _strike_query(description: str) -> Any:
"""Build a strike query parameter backed by the strike choices."""
return FastAPIQuery(
description=description,
json_schema_extra={
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": STRIKE_CHOICES_ENDPOINT,
"optionsParams": {"symbol": "$symbol"},
"style": SYMBOL_STYLE,
}
},
)
def _choice_query(description: str, options: list[tuple[str, str]]) -> Any:
"""Build a query parameter rendered as a labelled dropdown."""
return FastAPIQuery(
description=description,
json_schema_extra={
"x-widget_config": {
"options": [
{"label": label, "value": value} for label, value in options
]
}
},
)
def _legs_query(description: str) -> Any:
"""Build the strategy parameter the optimizer's Legs column groups by."""
return FastAPIQuery(
description=description,
json_schema_extra={
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": STRATEGY_CHOICES_ENDPOINT,
"optionsParams": {
"symbol": "$symbol",
"target_price": "$target_price",
"target_date": "$target_date",
"budget": "$budget",
},
"style": SYMBOL_STYLE,
}
},
)
def _column(field: str, header: str, **extra) -> dict:
"""Describe one grid column."""
return {"field": field, "headerName": header, **extra}
_MONEY = {"cellDataType": "number"}
_COUNT = {"cellDataType": "number"}
_NORMALIZED = {"cellDataType": "number", "formatterFn": "normalizedPercent"}
_PERCENT = {"cellDataType": "number", "formatterFn": "percent"}
_STRATEGY_TAIL = [
_column("cost", "Cost", **_MONEY),
_column("cost_percent", "Cost %", **_NORMALIZED),
_column("max_loss", "Max Loss", **_MONEY),
_column("breakeven_lower", "Breakeven Lower", **_MONEY),
_column("breakeven_upper", "Breakeven Upper", **_MONEY),
]
CHAIN_COLUMNS = [
_column("contract_symbol", "Contract", pinned="left", cellDataType="text"),
_column("expiration", "Expiration", cellDataType="text"),
_column("dte", "DTE", **_COUNT),
_column("strike", "Strike", **_MONEY),
_column("option_type", "Type", cellDataType="text"),
_column("open_interest", "OI", **_COUNT),
_column("volume", "Volume", **_COUNT),
_column("volume_notional", "Volume Notional", **_MONEY),
_column("bid", "Bid", **_MONEY),
_column("bid_size", "Bid Size", **_COUNT),
_column("ask", "Ask", **_MONEY),
_column("ask_size", "Ask Size", **_COUNT),
_column("mark", "Mark", **_MONEY),
_column("last_trade_price", "Last", **_MONEY),
_column("high", "High", **_MONEY),
_column("low", "Low", **_MONEY),
_column("change_percent", "Change %", **_PERCENT, renderFn="greenRed"),
_column("implied_volatility", "Mark IV", **_PERCENT),
_column("bid_iv", "Bid IV", **_PERCENT),
_column("ask_iv", "Ask IV", **_PERCENT),
_column("delta", "Delta", cellDataType="number"),
_column("gamma", "Gamma", cellDataType="number"),
_column("theta", "Theta", cellDataType="number"),
_column("vega", "Vega", cellDataType="number"),
_column("rho", "Rho", cellDataType="number"),
_column("settlement_price", "Settlement Price", **_MONEY),
_column("min_price", "Min Price", **_MONEY),
_column("max_price", "Max Price", **_MONEY),
_column("interest_rate", "Interest Rate", **_PERCENT),
_column("contract_size", "Contract Size", **_COUNT),
_column("is_inverse", "Is Inverse", cellDataType="boolean"),
_column("underlying_symbol", "Underlying", cellDataType="text"),
_column("underlying_price", "Underlying Forward", **_MONEY),
_column("underlying_spot_price", "Underlying Spot", **_MONEY),
_column("timestamp", "Timestamp", cellDataType="text"),
]
OPTIMIZER_COLUMNS = [
_column("strategy", "Strategy", pinned="left", cellDataType="text"),
_column("code", "Combo Type", cellDataType="text"),
_column(
"legs",
"Legs",
cellDataType="text",
formatterFn="none",
renderFn="cellOnClick",
renderFnParams={
"actionType": "groupBy",
"groupBy": {"paramName": "legs"},
},
),
_column("combo", "Listed Combo", cellDataType="text"),
_column("contracts", "Contracts", **_COUNT),
_column("cost", "Net Premium", **_MONEY),
_column("expected_profit", "Expected Profit", **_MONEY, renderFn="greenRed"),
_column("expected_return", "Expected Return", **_NORMALIZED, renderFn="greenRed"),
_column("max_profit", "Max Profit", **_MONEY),
_column("max_loss", "Max Loss", **_MONEY),
_column("breakeven_lower", "Breakeven Lower", **_MONEY),
_column("breakeven_upper", "Breakeven Upper", **_MONEY),
]
STRADDLE_COLUMNS = [
_column("expiration", "Expiration", pinned="left", cellDataType="text"),
_column("dte", "DTE", **_COUNT),
_column("underlying_price", "Underlying Price", **_MONEY),
_column("strike", "Strike", **_MONEY),
_column("call_premium", "Call Premium", **_MONEY),
_column("put_premium", "Put Premium", **_MONEY),
] + _STRATEGY_TAIL
STRANGLE_COLUMNS = [
_column("expiration", "Expiration", pinned="left", cellDataType="text"),
_column("dte", "DTE", **_COUNT),
_column("underlying_price", "Underlying Price", **_MONEY),
_column("put_strike", "Put Strike", **_MONEY),
_column("call_strike", "Call Strike", **_MONEY),
_column("call_premium", "Call Premium", **_MONEY),
_column("put_premium", "Put Premium", **_MONEY),
] + _STRATEGY_TAIL
SPREAD_COLUMNS = [
_column("strategy", "Strategy", pinned="left", cellDataType="text"),
_column("expiration", "Expiration", cellDataType="text"),
_column("dte", "DTE", **_COUNT),
_column("underlying_price", "Underlying Price", **_MONEY),
_column("bought_strike", "Bought Strike", **_MONEY),
_column("sold_strike", "Sold Strike", **_MONEY),
_column("bought_premium", "Bought Premium", **_MONEY),
_column("sold_premium", "Sold Premium", **_MONEY),
_column("cost", "Cost", **_MONEY),
_column("cost_percent", "Cost %", **_NORMALIZED),
_column("max_profit", "Max Profit", **_MONEY),
_column("max_loss", "Max Loss", **_MONEY),
_column("breakeven", "Breakeven", **_MONEY),
]
def _table_widget(name: str, description: str, columns: list, height: int = 16) -> dict:
"""Build the Workspace table-widget config for an analysis view."""
return {
"name": f"Deribit {name}",
"description": description,
"category": "Crypto",
"subCategory": "Options",
"source": ["Deribit"],
"gridData": {"w": 40, "h": height},
"data": {"table": {"columnsDefs": columns}},
}
def _chart_widget(name: str, description: str, height: int = 18) -> dict:
"""Build the Workspace chart-widget config for a Plotly route."""
return {
"widget_config": {
"name": f"Deribit {name}",
"description": description,
"type": "chart",
"raw": True,
"category": "Crypto",
"subCategory": "Options",
"source": ["Deribit"],
"gridData": {"w": 40, "h": height},
"refetchInterval": False,
}
}
async def _loaded(symbol: str) -> tuple:
"""Return the chain, its spot price, and how it settles."""
from openbb_deribit.utils.options.chain import (
is_inverse,
load_chain,
settlement_currency,
underlying_price,
)
frame = await load_chain(symbol)
return (
frame,
underlying_price(frame),
is_inverse(frame),
settlement_currency(frame, symbol),
)
def _figure_json(output: Any, raw: bool) -> Any:
"""Return the drawn figure, or the rows behind it when raw is asked for.
Parameters
----------
output : Any
The OBBject a view returned.
raw : bool
Whether the widget's raw toggle is on.
Returns
-------
Any
The Plotly figure as plain JSON, or the list of rows.
"""
import json
from openbb_deribit.utils.options.theme import CHART_CONFIG
if raw or output.chart is None:
return [row.model_dump() for row in (output.results or [])]
figure_json = json.loads(output.chart.fig.to_json())
figure_json["config"] = CHART_CONFIG
return figure_json
async def _chart_json(builder, symbol: str, theme: str, raw: bool, **kwargs) -> Any:
"""Load the chain, build the view, and return rows or figure JSON.
Raises
------
HTTPException
If the exchange lists no options on the underlying.
"""
from fastapi import HTTPException
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_deribit.utils.options.data_handler import load_symbol
try:
data = await load_symbol(symbol)
except OpenBBError as exc:
_logger.exception("Loading the Deribit option chain for %s failed", symbol)
raise HTTPException(
status_code=404,
detail=f"Deribit lists no options on {symbol}.",
) from exc
return _figure_json(builder(data, theme=theme, **kwargs), raw)
async def _chain_or_404(symbol: str) -> tuple:
"""Load a chain, answering 404 when the exchange lists none.
Raises
------
HTTPException
If the exchange publishes no option quotes for the underlying.
"""
from fastapi import HTTPException
from openbb_core.app.model.abstract.error import OpenBBError
try:
return await _loaded(symbol)
except OpenBBError as exc:
_logger.exception("Loading the Deribit option chain for %s failed", symbol)
raise HTTPException(
status_code=404,
detail=f"Deribit lists no options on {symbol}.",
) from exc
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(description="Get the underlyings with listed options.", parameters={})
],
)
async def underlying_choices() -> list[dict[str, str]]:
"""``[{label, value}]`` of every underlying with listed options."""
from openbb_deribit.utils.choices import options_root_choices
return await options_root_choices()
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get the expirations listed on an underlying.",
parameters={"symbol": "BTC"},
)
],
)
async def expiration_choices(
symbol: Annotated[str, _underlying()] = "BTC",
) -> list[dict[str, str]]:
"""``[{label, value}]`` of every expiration the underlying lists."""
from openbb_deribit.utils.options.chain import expirations, load_chain
frame = await load_chain(symbol)
return [
{
"label": f"{day} ({int(frame[frame['expiration'] == day]['dte'].iloc[0])}d)",
"value": str(day),
}
for day in expirations(frame)
]
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get the strikes listed on an underlying.",
parameters={"symbol": "BTC"},
)
],
)
async def strike_choices(
symbol: Annotated[str, _underlying()] = "BTC",
) -> list[dict]:
"""``[{label, value}]`` of every strike the underlying lists."""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_deribit.utils.options.data_handler import get_strikes, load_symbol
try:
data = await load_symbol(symbol)
except OpenBBError:
return []
return get_strikes(data)
if not DERIVATIVES_INSTALLED:
@router.command(
model="DeribitOptionsChains",
widget_config={
"name": "Deribit Options Chains",
"description": "The full option chain of an underlying, with greeks and"
+ " implied volatility.",
"category": "Crypto",
"subCategory": "Options",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 20},
"params": [{"paramName": "symbol", "value": "BTC"}],
"data": {"table": {"columnsDefs": CHAIN_COLUMNS}},
},
examples=[
APIEx(
description="The full BTC option chain.",
parameters={"symbol": "BTC", "provider": "deribit"},
)
],
)
async def chains(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the full Deribit option chain of an underlying, with greeks."""
return await OBBject.from_query(OBBQuery(**locals()))
def _quote(symbol: str) -> str:
"""Return the currency a position in this underlying is priced in."""
return "USDC" if symbol.upper().endswith("_USDC") else "USD"
async def _combos(symbol: str) -> list:
"""Return the combo books listed on an underlying."""
from openbb_deribit.utils.options.chain import load_combos
return await load_combos(symbol)
def _ranked_row(item: dict) -> dict:
"""Describe one ranked strategy as a grid row."""
crossings = item.get("breakevens") or []
return {
"strategy": item["strategy"],
"code": item["code"],
"legs": item["legs"],
"combo": item["combo"],
"contracts": item["contracts"],
"cost": item["cost"],
"expected_profit": item["expected_profit"],
"expected_return": item["expected_return"],
"max_profit": item["max_profit"],
"max_loss": item["max_loss"],
"breakeven_lower": crossings[0] if crossings else None,
"breakeven_upper": crossings[-1] if len(crossings) > 1 else None,
}
@router.command(
methods=["GET"],
widget_config=_table_widget(
"Strategy Optimizer",
"Every Deribit combo structure the expiration can form, bought and sold,"
" priced from its listed combo book where one trades, sized to the"
" capital it puts at risk, and ranked by what it returns if the"
" underlying reaches a price.",
OPTIMIZER_COLUMNS,
18,
),
examples=[
APIEx(
description="Rank strategies for a move to 100,000 by year end.",
parameters={
"symbol": "BTC",
"target_price": 100000,
"target_date": "2026-12-25",
},
)
],
)
async def optimizer(
symbol: Annotated[str, _underlying()] = "BTC",
target_price: Annotated[
float | None,
FastAPIQuery(description="The price the underlying is expected to reach."),
] = None,
target_date: Annotated[
str | None,
FastAPIQuery(description="When it is expected to reach it."),
] = None,
budget: Annotated[
float,
FastAPIQuery(
description="The capital to put at risk, in the quote currency. Each"
" position is sized so its worst loss across a threefold move either"
" way is this much."
),
] = 5000.0,
limit: Annotated[
int,
FastAPIQuery(
description="How many structures to return, each at its best strikes."
),
] = 40,
legs: Annotated[
str | None,
_legs_query(
"The strategy picked from the Legs column, shared with the payoff"
" chart. It does not change the ranking."
),
] = None,
) -> OBBject:
"""Rank every combo structure against a view on the underlying."""
from openbb_deribit.utils.options.chain import nearest_expiration
from openbb_deribit.utils.options.optimizer import rank
frame, spot, inverse, settles = await _chain_or_404(symbol)
target = float(target_price) if target_price else spot
expiration = nearest_expiration(frame, target_date or frame["expiration"].min())
ranked = rank(
frame, expiration, spot, target, budget, inverse, limit, await _combos(symbol)
)
return OBBject(
results=[_ranked_row(item) for item in ranked],
provider="deribit",
extra={
"results_metadata": {
"symbol": symbol,
"underlying_price": spot,
"target_price": target,
"expiration": str(expiration),
"quote_currency": _quote(symbol),
"settlement_currency": settles,
}
},
)
@router.command(
methods=["GET"],
widget_config=_table_widget(
"Straddle",
"A long straddle at the money, priced at every expiration.",
STRADDLE_COLUMNS,
),
examples=[APIEx(description="Price the BTC straddles.", parameters={})],
)
async def straddle(symbol: Annotated[str, _underlying()] = "BTC") -> OBBject:
"""Price a long straddle at each expiration."""
from openbb_deribit.utils.options.strategies import straddles
frame, spot, _inverse, _settles = await _chain_or_404(symbol)
return OBBject(results=straddles(frame, spot), provider="deribit")
@router.command(
methods=["GET"],
widget_config=_table_widget(
"Strangle",
"A long strangle out of the money, priced at every expiration.",
STRANGLE_COLUMNS,
),
examples=[APIEx(description="Price the BTC strangles.", parameters={})],
)
async def strangle(
symbol: Annotated[str, _underlying()] = "BTC",
moneyness: Annotated[
float,
FastAPIQuery(description="How far out of the money each leg sits, in percent."),
] = 5.0,
) -> OBBject:
"""Price a long strangle at each expiration."""
from openbb_deribit.utils.options.strategies import strangles
frame, spot, _inverse, _settles = await _chain_or_404(symbol)
return OBBject(
results=strangles(frame, spot, abs(moneyness) / 100), provider="deribit"
)
@router.command(
methods=["GET"],
widget_config=_table_widget(
"Vertical Spreads",
"A bull call and a bear put spread at every expiration.",
SPREAD_COLUMNS,
),
examples=[APIEx(description="Price the BTC vertical spreads.", parameters={})],
)
async def spreads(
symbol: Annotated[str, _underlying()] = "BTC",
moneyness: Annotated[
float,
FastAPIQuery(description="How far out the sold leg sits, in percent."),
] = 5.0,
) -> OBBject:
"""Price a bull call and a bear put spread at each expiration."""
from openbb_deribit.utils.options.strategies import verticals
frame, spot, _inverse, _settles = await _chain_or_404(symbol)
return OBBject(
results=verticals(frame, spot, abs(moneyness) / 100), provider="deribit"
)
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get the strategies the optimizer ranks for a view.",
parameters={"symbol": "BTC"},
)
],
)
async def strategy_choices(
symbol: Annotated[str, _underlying()] = "BTC",
target_price: Annotated[float | None, FastAPIQuery()] = None,
target_date: Annotated[str | None, FastAPIQuery()] = None,
budget: Annotated[float, FastAPIQuery()] = 5000.0,
) -> list[dict[str, str]]:
"""``[{label, value}]`` of the strategies the optimizer ranks for a view."""
from openbb_deribit.utils.options.chain import nearest_expiration
from openbb_deribit.utils.options.optimizer import SHORTLIST, rank
frame, spot, inverse, _settles = await _loaded(symbol)
target = float(target_price) if target_price else spot
expiration = nearest_expiration(frame, target_date or frame["expiration"].min())
ranked = rank(
frame,
expiration,
spot,
target,
budget,
inverse,
SHORTLIST,
await _combos(symbol),
)
return [
{"label": f"{item['strategy']} — {item['legs']}", "value": item["legs"]}
for item in ranked
]
async def payoff_chart(
symbol: Annotated[str, _underlying()] = "BTC",
target_price: Annotated[
float | None,
FastAPIQuery(description="The price the underlying is expected to reach."),
] = None,
target_date: Annotated[
str | None, FastAPIQuery(description="When it is expected to reach it.")
] = None,
budget: Annotated[
float,
FastAPIQuery(
description="The capital to put at risk, in the quote currency. Each"
" position is sized so its worst loss across a threefold move either"
" way is this much."
),
] = 5000.0,
legs: Annotated[
str | None,
_legs_query(
"The contracts to draw, as 'Buy SYMBOL / Sell 2 SYMBOL'. Click the"
" Legs cell of a strategy in the optimizer to set it, or pick one"
" here. Left empty, the best strategy for the view is drawn."
),
] = None,
raw: Annotated[bool, _excluded_query()] = False,
theme: Annotated[str, _excluded_query()] = "dark",
):
"""Draw what one position returns across a range of underlying prices.
Raises
------
HTTPException
If the expiration lists no strategy, or does not list the position.
"""
from fastapi import HTTPException
from openbb_deribit.utils.options.chain import nearest_expiration, quotes_at
from openbb_deribit.utils.options.create_payoff import create_payoff
from openbb_deribit.utils.options.optimizer import draw, rank, rebuild, score
frame, spot, inverse, settles = await _chain_or_404(symbol)
target = float(target_price) if target_price else spot
expiration = nearest_expiration(frame, target_date or frame["expiration"].min())
books = await _combos(symbol)
if legs:
try:
built = rebuild(legs, quotes_at(frame), books, spot, inverse)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
scored = score(built, spot, target, inverse)
if scored is None:
raise HTTPException(
status_code=404,
detail="That position puts nothing at risk, so a budget does not"
" size it. Pick one the optimizer ranked.",
)
chosen = draw(scored, spot, target, budget, inverse)
else:
ranked = rank(frame, expiration, spot, target, budget, inverse, 1, books)
if not ranked:
raise HTTPException(
status_code=404,
detail=f"No strategy on the {expiration} expiration can be sized"
f" to {budget:,.0f} {_quote(symbol)}.",
)
chosen = ranked[0]
output = create_payoff(
{
**chosen,
"spot": spot,
"target": target,
"quote": _quote(symbol),
"settles": settles,
},
theme,
)
return _figure_json(output, raw)
async def smile_chart(
symbol: Annotated[str, _underlying()] = "BTC",
expirations: Annotated[
str | None,
_expiry_query("Up to five expiration dates (comma-separated).", multi=True),
] = None,
otm: Annotated[
bool, FastAPIQuery(description="Show only out-of-the-money contracts.")
] = False,
skew: Annotated[
bool, FastAPIQuery(description="Plot skew relative to ATM instead of IV.")
] = False,
raw: Annotated[bool, _excluded_query()] = False,
theme: Annotated[str, _excluded_query()] = "dark",
):
"""Implied-volatility smile / skew across strikes."""
from openbb_deribit.utils.options.create_smile import create_smile
return await _chart_json(
create_smile, symbol, theme, raw, expirations=expirations, otm=otm, skew=skew
)
async def surface_chart(
symbol: Annotated[str, _underlying()] = "BTC",
metric: Annotated[
Literal[
"implied_volatility", "delta", "gamma", "theta", "vega", "rho", "dex", "gex"
],
_choice_query(
"The surface metric.",
[
("Implied Volatility", "implied_volatility"),
("Delta", "delta"),
("Gamma", "gamma"),
("Theta", "theta"),
("Vega", "vega"),
("Rho", "rho"),
("Delta Exposure", "dex"),
("Gamma Exposure", "gex"),
],
),
] = "implied_volatility",
option_type: Annotated[
Literal["otm", "itm", "puts", "calls"],
_choice_query(
"Which side of the chain to plot.",
[
("Out-Of-The-Money", "otm"),
("In-The-Money", "itm"),
("Puts", "puts"),
("Calls", "calls"),
],
),
] = "otm",
dte_min: Annotated[
int | None, FastAPIQuery(description="Minimum days to expiry.")
] = None,
dte_max: Annotated[
int | None, FastAPIQuery(description="Maximum days to expiry.")
] = None,
moneyness: Annotated[
float | None,
FastAPIQuery(description="Percent from the underlying price to include."),
] = None,
oi: Annotated[
bool, FastAPIQuery(description="Drop contracts with no open interest.")
] = False,
volume: Annotated[
bool, FastAPIQuery(description="Drop contracts that have not traded.")
] = False,
raw: Annotated[bool, _excluded_query()] = False,
theme: Annotated[str, _excluded_query()] = "dark",
):
"""Implied-volatility or greeks 3-D surface over DTE and strike."""
from openbb_deribit.utils.options.create_surface import create_surface
dte_range = [dte_min or 0, dte_max or 5000] if (dte_min or dte_max) else None
return await _chart_json(
create_surface,
symbol,
theme,
raw,
option_type=option_type,
metric=metric,
dte_range=dte_range,
moneyness=moneyness,
oi=oi,
volume=volume,
)
async def stats_chart(
symbol: Annotated[str, _underlying()] = "BTC",
by: Annotated[
Literal["strike", "expiration"],
_choice_query(
"Aggregate by strike or expiration.",
[("Expiration", "expiration"), ("Strike", "strike")],
),
] = "expiration",
metric: Annotated[
Literal["oi", "volume"],
_choice_query(
"Open interest or volume.",
[("Open Interest", "oi"), ("Volume", "volume")],
),
] = "oi",
date: Annotated[
str | None,
_expiry_query("Expiry to view by strike (switches 'by' to strike)."),
] = None,
unit: Annotated[
Literal["value", "percent", "pcr"],
_choice_query(
"Raw values, share of total, or put/call ratio.",
[("Value", "value"), ("Percent", "percent"), ("Put/Call Ratio", "pcr")],
),
] = "value",
raw: Annotated[bool, _excluded_query()] = False,
theme: Annotated[str, _excluded_query()] = "dark",
):
"""Open-interest or volume statistics by strike or expiration."""
from openbb_deribit.utils.options.create_stats import create_stats
return await _chart_json(
create_stats, symbol, theme, raw, by=by, metric=metric, date=date, unit=unit
)
async def term_structure_chart(
symbol: Annotated[str, _underlying()] = "BTC",
strike: Annotated[
float | None, _strike_query("Target strike. Default is nearest OTM per expiry.")
] = None,
moneyness: Annotated[
float | None,
FastAPIQuery(description="Percent out-of-the-money instead of a fixed strike."),
] = None,
metric: Annotated[
Literal["iv", "price"],
_choice_query(
"Implied volatility or price.",
[("Implied Volatility", "iv"), ("Price", "price")],
),
] = "iv",
option_type: Annotated[
Literal["both", "calls", "puts"],
_choice_query(
"Which side of the chain to plot.",
[("Both", "both"), ("Calls", "calls"), ("Puts", "puts")],
),
] = "both",
raw: Annotated[bool, _excluded_query()] = False,
theme: Annotated[str, _excluded_query()] = "dark",
):
"""Price or IV term structure across expirations."""
from openbb_deribit.utils.options.create_term_structure import create_term_structure
return await _chart_json(
create_term_structure,
symbol,
theme,
raw,
strike=strike,
moneyness=moneyness,
metric=metric,
option_type=option_type,
)
CHART_ROUTES = (
(
"/payoff",
payoff_chart,
"Options Payoff",
"What one ranked strategy returns across a range of underlying prices.",
18,
),
(
"/smile",
smile_chart,
"Volatility Smile",
"Implied volatility smile or skew across strikes, per expiration.",
16,
),
(
"/term_structure",
term_structure_chart,
"Volatility Term Structure",
"Price or implied volatility at one strike across expirations.",
16,
),
(
"/surface",
surface_chart,
"Volatility Surface",
"Implied volatility or a greek as a surface over DTE and strike.",
20,
),
(
"/stats",
stats_chart,
"Options Statistics",
"Open interest and volume, split between calls and puts.",
16,
),
)
for _path, _endpoint, _name, _description, _height in CHART_ROUTES:
router.api_router.add_api_route(
path=_path,
endpoint=_endpoint,
methods=["GET"],
openapi_extra=_chart_widget(_name, _description, _height),
)

View File

@@ -0,0 +1,146 @@
"""Deribit Rates sub-router."""
from typing import Annotated
from fastapi import Query as FastAPIQuery
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
router = Router(prefix="/rates", description="Deribit funding and yield data.")
@router.command(
model="DeribitFundingRateHistory",
widget_config={
"name": "Deribit Funding Rate History",
"description": "The hourly funding a perpetual has paid.",
"category": "Crypto",
"subCategory": "Rates",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"params": [{"paramName": "symbol", "value": "BTC-PERPETUAL"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The funding the BTC perpetual has paid over the last week.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
)
],
)
async def funding_history(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the hourly funding a Deribit perpetual has paid."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitFundingChart",
widget_config={"exclude": True},
examples=[
APIEx(
description="The funding the BTC perpetual has accrued over eight hours.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
)
],
)
async def funding_chart(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the funding a Deribit perpetual has accrued over the current window."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitAprHistory",
widget_config={
"name": "Deribit Yield History",
"description": "The daily yield a yield-bearing currency has earned.",
"category": "Crypto",
"subCategory": "Rates",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The yield USDe has earned.",
parameters={"currency": "usde", "provider": "deribit"},
)
],
)
async def apr_history(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the daily yield a Deribit yield-bearing currency has earned."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Read the funding one perpetual paid over a span.",
parameters={
"symbol": "BTC-PERPETUAL",
"start_date": "2026-09-01",
"end_date": "2026-09-08",
},
)
],
)
async def funding_value(
symbol: Annotated[
str, FastAPIQuery(description="The perpetual instrument name.")
] = "BTC-PERPETUAL",
start_date: Annotated[
str | None, FastAPIQuery(description="The first date of the span.")
] = None,
end_date: Annotated[
str | None, FastAPIQuery(description="The last date of the span.")
] = None,
) -> float:
"""Get the funding one perpetual paid over a span, as a single rate."""
from datetime import datetime, timedelta, timezone
from openbb_deribit.utils.client import request
from openbb_deribit.utils.helpers import get_perpetual_symbols, to_timestamp
now = datetime.now(timezone.utc)
perpetuals = await get_perpetual_symbols()
return await request(
"get_funding_rate_value",
{
"instrument_name": perpetuals.get(symbol.upper(), symbol),
"start_timestamp": to_timestamp(start_date or now - timedelta(days=7)),
"end_timestamp": to_timestamp(end_date or now),
},
)

View File

@@ -0,0 +1,321 @@
"""Deribit Reference sub-router."""
from typing import Annotated
from fastapi import Query as FastAPIQuery
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
router = Router(prefix="/reference", description="Deribit exchange reference data.")
@router.command(
model="DeribitCurrencies",
widget_config={
"name": "Deribit Currencies",
"description": "Every currency the exchange lists, with its yield and"
+ " withdrawal terms.",
"category": "Crypto",
"subCategory": "Reference",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 12},
},
examples=[
APIEx(
description="List every currency Deribit lists.",
parameters={"provider": "deribit"},
)
],
)
async def currencies(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get every currency Deribit lists, with its yield and withdrawal terms."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitInstruments",
widget_config={
"name": "Deribit Instruments",
"description": "The full contract specification of every listed instrument.",
"category": "Crypto",
"subCategory": "Reference",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 15},
},
examples=[
APIEx(
description="Every instrument the exchange lists.",
parameters={"provider": "deribit"},
),
APIEx(
description="Only the options settling in USDC.",
parameters={"currency": "USDC", "kind": "option", "provider": "deribit"},
),
APIEx(
description="The specification of one named instrument.",
parameters={"symbol": "BTC-PERPETUAL", "provider": "deribit"},
),
],
)
async def instruments(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the full contract specification of every listed Deribit instrument."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitExpirations",
widget_config={
"name": "Deribit Expirations",
"description": "The expirations listed on each currency.",
"category": "Crypto",
"subCategory": "Reference",
"source": ["Deribit"],
"gridData": {"w": 20, "h": 12},
},
examples=[
APIEx(
description="Every expiration listed, kept apart by currency.",
parameters={"provider": "deribit"},
),
APIEx(
description="The option expirations listed on BTC.",
parameters={"currency": "BTC", "kind": "option", "provider": "deribit"},
),
],
)
async def expirations(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the expirations Deribit lists on each currency."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitCombos",
widget_config={
"name": "Deribit Combos",
"description": "The listed multi-leg structures, one row per leg.",
"category": "Crypto",
"subCategory": "Reference",
"source": ["Deribit"],
"gridData": {"w": 30, "h": 12},
},
examples=[
APIEx(
description="Every combo listed on BTC.",
parameters={"currency": "BTC", "provider": "deribit"},
),
APIEx(
description="The legs of one named combo.",
parameters={
"combo_id": "BTC-STRG-30OCT26-66000_90000",
"provider": "deribit",
},
),
],
)
async def combos(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the multi-leg structures Deribit lists, one row per leg."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitAnnouncements",
widget_config={
"name": "Deribit Announcements",
"description": "What the exchange has announced.",
"category": "Crypto",
"subCategory": "Reference",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 12},
},
examples=[
APIEx(
description="The most recent exchange announcements.",
parameters={"provider": "deribit"},
)
],
)
async def announcements(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get what Deribit has announced."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Read the contract size of one instrument.",
parameters={"symbol": "BTC-PERPETUAL"},
)
],
)
async def contract_size(
symbol: Annotated[
str, FastAPIQuery(description="The instrument name.")
] = "BTC-PERPETUAL",
) -> float | None:
"""Get the size of one contract of an instrument."""
from openbb_deribit.utils.client import request
result = await request("get_contract_size", {"instrument_name": symbol})
return (result or {}).get("contract_size")
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[APIEx(description="Read the exchange's platform status.", parameters={})],
)
async def status() -> dict:
"""Get whether the exchange has locked the platform."""
from openbb_deribit.utils.client import request
return await request("status", use_cache=False)
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[APIEx(description="Read the exchange's server time.", parameters={})],
)
async def server_time() -> int:
"""Get the exchange's server time, in milliseconds since the epoch."""
from openbb_deribit.utils.client import request
return await request("get_time", use_cache=False)
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[APIEx(description="Get currency choices.", parameters={})],
)
async def currency_choices() -> list[dict[str, str]]:
"""``[{label, value}]`` of every currency the exchange lists."""
from openbb_deribit.utils.choices import currency_choices as choices
return await choices()
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(description="Get the names of every published index.", parameters={})
],
)
async def index_choices(
supported: Annotated[
bool,
FastAPIQuery(
description="When True, returns only the indexes instruments settle"
+ " against, rather than every published index."
),
] = False,
kind: Annotated[
str,
FastAPIQuery(
description="Narrow the supported names to spot or derivative indexes."
),
] = "all",
) -> list[dict[str, str]]:
"""``[{label, value}]`` of every index the exchange publishes."""
from openbb_deribit.utils.choices import (
_choices,
index_choices as choices,
)
from openbb_deribit.utils.client import request
if not supported:
return await choices()
names = await request("get_supported_index_names", {"type": kind})
return _choices(sorted(str(name) for name in names))
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get instrument choices for one kind.",
parameters={"kind": "future"},
)
],
)
async def instrument_choices(
kind: Annotated[
str | None,
FastAPIQuery(description="The kind of instrument. Default is all of them."),
] = None,
currency: Annotated[
str, FastAPIQuery(description="The settlement currency of the instruments.")
] = "any",
expired: Annotated[
bool, FastAPIQuery(description="When True, returns expired instruments.")
] = False,
) -> list[dict[str, str]]:
"""``[{label, value}]`` of every instrument the exchange lists."""
from openbb_deribit.utils.choices import instrument_choices as choices
return await choices(kind, currency, expired)
@router.command(
methods=["GET"],
widget_config={"exclude": True},
examples=[
APIEx(
description="Get the combo identifiers listed on one currency.",
parameters={"currency": "BTC"},
)
],
)
async def combo_choices(
currency: Annotated[
str, FastAPIQuery(description="The currency the combos settle in.")
] = "BTC",
state: Annotated[
str | None,
FastAPIQuery(description="Narrow to active or inactive combos."),
] = None,
) -> list[dict[str, str]]:
"""``[{label, value}]`` of every combo identifier the exchange lists."""
from openbb_deribit.utils.choices import combo_choices as choices
return await choices(currency, state)

View File

@@ -0,0 +1,90 @@
"""Deribit Volatility sub-router."""
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query as OBBQuery
from openbb_core.app.router import Router
router = Router(prefix="/volatility", description="Deribit volatility data.")
@router.command(
model="DeribitHistoricalVolatility",
widget_config={
"name": "Deribit Realized Volatility",
"description": "The volatility a currency has actually realized.",
"category": "Crypto",
"subCategory": "Volatility",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"params": [{"paramName": "currency", "value": "BTC"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The volatility BTC has realized.",
parameters={"currency": "BTC", "provider": "deribit"},
)
],
)
async def realized(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the volatility a Deribit currency has actually realized."""
return await OBBject.from_query(OBBQuery(**locals()))
@router.command(
model="DeribitVolatilityIndex",
widget_config={
"name": "Deribit Volatility Index",
"description": "The DVOL index, as candles over a span.",
"category": "Crypto",
"subCategory": "Volatility",
"source": ["Deribit"],
"gridData": {"w": 40, "h": 14},
"params": [{"paramName": "currency", "value": "BTC"}],
"data": {
"table": {
"chartView": {"enabled": True, "chartType": "line"},
}
},
},
examples=[
APIEx(
description="The BTC volatility index over the last month.",
parameters={"currency": "BTC", "provider": "deribit"},
),
APIEx(
description="Daily candles over a named span.",
parameters={
"currency": "ETH",
"interval": "1d",
"start_date": "2026-01-01",
"end_date": "2026-06-30",
"provider": "deribit",
},
),
],
)
async def index(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get the Deribit DVOL volatility index, as candles over a span."""
return await OBBject.from_query(OBBQuery(**locals()))

View File

@@ -0,0 +1,146 @@
"""Deribit shared response cache."""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp_client_cache import SQLiteBackend
MINUTE = 60
HOUR = 60 * 60
DAY = 60 * 60 * 24
CACHE_NAME = "deribit"
CACHE_TTL_DEFAULT = 5 * MINUTE
CACHE_MAX_BYTES = 128 * 1024 * 1024
CACHE_SWEEP_PASSES = 3
URL_CACHE_TTL = {
"*/public/get_currencies*": DAY,
"*/public/get_index_price_names*": DAY,
"*/public/get_supported_index_names*": DAY,
"*/public/get_instruments*": HOUR,
"*/public/get_instrument*": HOUR,
"*/public/get_contract_size*": DAY,
"*/public/get_expirations*": HOUR,
"*/public/get_combo_ids*": HOUR,
"*/public/get_combos*": HOUR,
"*/public/get_combo_details*": HOUR,
"*/public/get_delivery_prices*": HOUR,
"*/public/get_apr_history*": HOUR,
"*/public/get_announcements*": HOUR,
"*/public/get_trade_volumes*": 15 * MINUTE,
"*/public/get_historical_volatility*": 15 * MINUTE,
"*/public/get_volatility_index_data*": 15 * MINUTE,
"*/public/get_funding_rate_history*": 15 * MINUTE,
"*/public/get_funding_chart_data*": 5 * MINUTE,
"*/public/get_funding_rate_value*": 5 * MINUTE,
"*/public/get_tradingview_chart_data*": 5 * MINUTE,
"*/public/get_index_chart_data*": 5 * MINUTE,
"*/public/get_last_settlements_by_currency*": 5 * MINUTE,
"*/public/get_last_settlements_by_instrument*": 5 * MINUTE,
"*/public/get_block_rfq_trades*": MINUTE,
"*/public/get_last_trades_by_currency*": MINUTE,
"*/public/get_last_trades_by_instrument*": MINUTE,
"*/public/get_book_summary_by_currency*": MINUTE,
"*/public/get_book_summary_by_instrument*": MINUTE,
"*/public/get_index_price*": MINUTE,
"*/public/get_order_book*": 0,
"*/public/ticker*": 0,
"*/public/get_time*": 0,
"*/public/status*": 0,
}
_swept = False
def cache_path() -> str:
"""Return the file the cached responses are kept in."""
from openbb_core.app.utils import get_user_cache_directory
return f"{get_user_cache_directory()}/http/{CACHE_NAME}"
def get_cache_backend() -> "SQLiteBackend":
"""Build the on-disk response cache, with a TTL per endpoint.
Returns
-------
SQLiteBackend
The shared response cache.
"""
from aiohttp_client_cache import SQLiteBackend
return SQLiteBackend(
cache_name=cache_path(),
expire_after=CACHE_TTL_DEFAULT,
urls_expire_after=URL_CACHE_TTL,
allowed_codes=(200,),
allowed_methods=("GET",),
include_headers=False,
)
async def _oldest(backend: "SQLiteBackend", count: int) -> set:
"""Return the keys of the entries written longest ago."""
keys: set = set()
async for key in backend.responses.keys():
keys.add(key)
if len(keys) >= count:
break
return keys
async def _vacuum(backend: "SQLiteBackend") -> None:
"""Give back the pages the deleted responses were held in."""
from aiohttp_client_cache.backends.sqlite import SQLiteCache
stored = backend.responses
if not isinstance(stored, SQLiteCache):
return
async with stored.get_connection(commit=True) as connection:
await connection.execute("VACUUM")
async def sweep_cache(backend: "SQLiteBackend") -> None:
"""Drop what has expired, then the oldest of what is left if still too big.
An expiry marks a response stale rather than deleting it, so the file grows
by every request ever made until something clears it out. It is swept once
per process, and what the sweep leaves over the limit is trimmed by the
share of the file that overshoots it.
Parameters
----------
backend : SQLiteBackend
The cache to sweep.
"""
from pathlib import Path
global _swept # noqa: PLW0603
if _swept:
return
_swept = True
await backend.delete_expired_responses()
await _vacuum(backend)
stored = Path(f"{cache_path()}.sqlite")
for _ in range(CACHE_SWEEP_PASSES):
if not stored.exists() or stored.stat().st_size <= CACHE_MAX_BYTES:
return
held = await backend.responses.size()
overshoot = 1 - CACHE_MAX_BYTES / stored.stat().st_size
dropped = await _oldest(backend, max(1, round(held * overshoot)))
if not dropped:
return
await backend.responses.bulk_delete(dropped)
await _vacuum(backend)

View File

@@ -0,0 +1,86 @@
"""Choice feeds built from what the exchange lists right now."""
def _choices(values, labels: dict | None = None) -> list[dict[str, str]]:
"""Pair each value with a label for a widget parameter."""
named = labels or {}
return [{"label": named.get(value, value), "value": value} for value in values]
async def currency_choices() -> list[dict[str, str]]:
"""Return every currency the exchange lists."""
from openbb_deribit.utils.helpers import get_currencies
currencies = await get_currencies()
return _choices(
sorted(str(d["currency"]) for d in currencies),
{
str(d["currency"]): str(d.get("currency_long") or d["currency"])
for d in currencies
},
)
async def index_choices(extended: bool = False) -> list[dict[str, str]]:
"""Return the name of every published index."""
from openbb_deribit.utils.helpers import get_index_names
names = await get_index_names(extended)
return _choices(
sorted(
str(name["name"]) if isinstance(name, dict) else str(name) for name in names
)
)
async def instrument_choices(
kind: str | None = None,
currency: str = "any",
expired: bool = False,
) -> list[dict[str, str]]:
"""Return every instrument of one kind, labelled by its name."""
from openbb_deribit.utils.helpers import get_instruments
instruments = await get_instruments(currency, kind, expired)
return _choices(sorted(str(d["instrument_name"]) for d in instruments))
async def perpetual_choices() -> list[dict[str, str]]:
"""Return every perpetual, labelled by its instrument name."""
from openbb_deribit.utils.helpers import get_perpetual_symbols
perpetuals = await get_perpetual_symbols()
return [{"label": name, "value": name} for name in sorted(perpetuals.values())]
async def options_root_choices() -> list[dict[str, str]]:
"""Return every underlying with listed options."""
from openbb_deribit.utils.helpers import get_options_roots
return _choices(await get_options_roots())
async def futures_root_choices() -> list[dict[str, str]]:
"""Return every underlying with a listed futures curve."""
from openbb_deribit.utils.helpers import get_futures_roots
return _choices(await get_futures_roots())
async def combo_choices(
currency: str = "BTC", state: str | None = None
) -> list[dict[str, str]]:
"""Return the identifier of every combo listed on one currency."""
from openbb_deribit.utils.client import request
from openbb_deribit.utils.helpers import normalize_currency
combos = await request(
"get_combo_ids", {"currency": normalize_currency(currency), "state": state}
)
return _choices(sorted(str(combo) for combo in combos))

View File

@@ -0,0 +1,132 @@
"""Transport for the Deribit public JSON-RPC API."""
from typing import Any
BASE_URL = "https://www.deribit.com/api/v2"
REQUEST_TIMEOUT = 60
TOO_MANY_REQUESTS = 429
def build_url(method: str, params: dict[str, Any] | None = None) -> str:
"""Return the request URL for one public method.
Parameters
----------
method : str
The method name, without the 'public/' prefix.
params : dict or None
The query parameters. Entries with a value of None are dropped, and
booleans are sent as the lower-case literals the API expects.
Returns
-------
str
The fully-qualified request URL.
"""
from urllib.parse import urlencode
query = {
key: str(value).lower() if isinstance(value, bool) else value
for key, value in (params or {}).items()
if value is not None
}
url = f"{BASE_URL}/public/{method}"
return f"{url}?{urlencode(query)}" if query else url
async def request(
method: str,
params: dict[str, Any] | None = None,
use_cache: bool = True,
) -> Any:
"""Call one public method and return its result.
Parameters
----------
method : str
The method name, without the 'public/' prefix.
params : dict or None
The query parameters.
use_cache : bool
Whether to read and write the on-disk response cache.
Returns
-------
Any
The 'result' member of the JSON-RPC response.
Raises
------
OpenBBError
If the API reports an error, or the response cannot be read.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_deribit.utils.session import get_cached_session, get_session
url = build_url(method, params)
session = await get_cached_session() if use_cache else await get_session()
try:
response = await session.get(url, timeout=REQUEST_TIMEOUT)
if response.status != 200:
raise OpenBBError(
f"Deribit answered public/{method} with HTTP {response.status}."
+ (
" The request rate was exceeded; narrow the query or retry."
if response.status == TOO_MANY_REQUESTS
else ""
)
)
payload = await response.json(content_type=None)
except OpenBBError:
raise
except Exception as error: # noqa: BLE001
raise OpenBBError(
f"Deribit request failed for public/{method} ->"
f" {error.__class__.__name__}: {error}"
) from error
if not isinstance(payload, dict):
raise OpenBBError(f"Unexpected Deribit response for public/{method}.")
if payload.get("error"):
detail = payload["error"]
message = detail.get("message") if isinstance(detail, dict) else detail
reason = detail.get("data") if isinstance(detail, dict) else None
raise OpenBBError(
f"Deribit returned an error for public/{method} -> {message}"
+ (f" {reason}" if reason else "")
)
return payload.get("result")
async def gather(
calls: "list[tuple[str, dict[str, Any] | None]]",
use_cache: bool = True,
) -> list:
"""Call several public methods at once.
Parameters
----------
calls : list[tuple[str, dict or None]]
Each method name with its query parameters.
use_cache : bool
Whether to read and write the on-disk response cache.
Returns
-------
list
The result of each call, in the order it was given, with an exception
in place of any result that could not be read.
"""
import asyncio
return await asyncio.gather(
*(request(method, params, use_cache) for method, params in calls),
return_exceptions=True,
)

View File

@@ -0,0 +1,102 @@
"""Fixed enumerations and choice endpoints of the Deribit public API."""
from typing import Literal
from openbb_core.app.service.system_service import SystemService
API_PREFIX = (
SystemService()
.system_settings.python_settings.model_dump()
.get("api_settings", {})
.get("prefix", "")
or "/api/v1"
)
CURRENCY_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/reference/currency_choices"
INDEX_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/reference/index_choices"
INSTRUMENT_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/reference/instrument_choices"
COMBO_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/reference/combo_choices"
CURVE_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/futures/curve_choices"
PERPETUAL_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/futures/perpetual_choices"
UNDERLYING_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/options/underlying_choices"
EXPIRATION_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/options/expiration_choices"
STRATEGY_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/options/strategy_choices"
STRIKE_CHOICES_ENDPOINT = f"{API_PREFIX}/deribit/options/strike_choices"
SYMBOL_STYLE = {"popupWidth": 600}
CURRENCIES = ("BTC", "ETH", "USDC", "USDT", "EURR")
Currencies = Literal["BTC", "ETH", "USDC", "USDT", "EURR"]
AnyCurrencies = Literal["BTC", "ETH", "USDC", "USDT", "EURR", "any"]
VOLATILITY_INDEX_CURRENCIES = ("BTC", "ETH")
REALIZED_VOLATILITY_CURRENCIES = ("BTC", "ETH", "EURR")
UNDERLYING_CURRENCIES = ("BTC", "ETH")
LISTING_CURRENCIES = ("BTC", "ETH", "USDC")
ListingCurrencies = Literal["BTC", "ETH", "USDC"]
AnyListingCurrencies = Literal["BTC", "ETH", "USDC", "any"]
OPTION_KINDS = ("option", "option_combo")
INSTRUMENT_KINDS = ("future", "option", "spot", "future_combo", "option_combo")
InstrumentKinds = Literal["future", "option", "spot", "future_combo", "option_combo"]
TradeKinds = Literal["future", "option", "future_combo", "option_combo", "combo"]
ROUTED_SPOT_ERROR = "not_supported_for_coinbase_routed_spot"
ExpirationKinds = Literal["future", "option", "any"]
ExpirationCurrencies = Literal["BTC", "ETH", "any", "grouped"]
SettlementTypes = Literal["settlement", "delivery", "bankruptcy"]
ComboStates = Literal["active", "inactive"]
IndexNameTypes = Literal["all", "spot", "derivative"]
Sorting = Literal["asc", "desc", "default"]
FundingLengths = Literal["8h", "24h", "1m"]
IndexChartRanges = Literal["1h", "1d", "2d", "1m", "1y", "all"]
AprCurrencies = Literal["usde", "steth", "usdc", "build"]
ORDER_BOOK_DEPTHS = (1, 5, 10, 20, 50, 100, 1000, 10000)
INTERVALS = (
"1m",
"3m",
"5m",
"10m",
"15m",
"30m",
"1h",
"2h",
"3h",
"6h",
"12h",
"1d",
)
Intervals = Literal[
"1m", "3m", "5m", "10m", "15m", "30m", "1h", "2h", "3h", "6h", "12h", "1d"
]
INTERVAL_MAP = {
"1m": "1",
"3m": "3",
"5m": "5",
"10m": "10",
"15m": "15",
"30m": "30",
"1h": "60",
"2h": "120",
"3h": "180",
"6h": "360",
"12h": "720",
"1d": "1D",
}
VOLATILITY_RESOLUTIONS = ("1m", "1h", "12h", "1d")
VolatilityResolutions = Literal["1m", "1h", "12h", "1d"]
VOLATILITY_RESOLUTION_MAP = {
"1m": "60",
"1h": "3600",
"12h": "43200",
"1d": "1D",
}
BLOCK_RFQ_MIN_COUNT = 10
BLOCK_RFQ_MAX_COUNT = 50
MAX_TRADE_COUNT = 1000
MAX_SETTLEMENT_COUNT = 1000

View File

@@ -1,402 +1,535 @@
"""Deribit Helpers Module."""
"""Shared accessors for the Deribit public API."""
from typing import Literal
from typing import Any
from async_lru import alru_cache
from openbb_core.app.model.abstract.error import OpenBBError
DERIBIT_OPTIONS_SYMBOLS = ["BTC", "ETH", "SOL", "XRP", "BNB", "PAXG"]
OptionsSymbols = Literal["BTC", "ETH", "SOL", "XRP", "BNB", "PAXG"]
CURRENCIES = ["BTC", "ETH", "USDC", "USDT", "EURR", "all"]
Currencies = Literal["BTC", "ETH", "USDC", "USDT", "EURR", "all"]
DERIVATIVE_TYPES = ["future", "option", "spot", "future_combo", "option_combo"]
DerivativeTypes = Literal["future", "option", "spot", "future_combo", "option_combo"]
DERIBIT_FUTURES_CURVE_SYMBOLS = ["BTC", "ETH", "PAXG"]
FuturesCurveSymbols = Literal["BTC", "ETH", "PAXG"]
BASE_URL = "https://www.deribit.com"
DERIBIT_INTERVALS = [
"1m",
"3m",
"5m",
"10m",
"15m",
"30m",
"1h",
"2h",
"3h",
"6h",
"12h",
"1d",
]
DeribitIntervals = Literal[
"1m", "3m", "5m", "10m", "15m", "30m", "1h", "2h", "3h", "6h", "12h", "1d"
]
INTERVAL_MAP = {
"1m": "1",
"3m": "3",
"5m": "5",
"10m": "10",
"15m": "15",
"30m": "30",
"1h": "60",
"2h": "120",
"3h": "180",
"6h": "360",
"12h": "720",
"1d": "1D",
}
MAX_CHART_WINDOWS = 15
CHART_WINDOW_SIZE = 5000
SECONDS_PER_DAY = 86400
CURVE_LOOKBACK_HOURS = 24
@alru_cache(maxsize=64)
async def get_instruments(
currency: Currencies = "BTC",
derivative_type: DerivativeTypes | None = None,
expired: bool = False,
) -> list[dict]:
"""
Get Deribit instruments.
def to_timestamp(value: Any) -> int:
"""Return a date, datetime, or string as milliseconds since the epoch."""
from pandas import Timestamp, to_datetime
stamp = value if isinstance(value, Timestamp) else to_datetime(value, utc=True)
if stamp.tzinfo is None:
stamp = stamp.tz_localize("UTC")
return int(stamp.timestamp() * 1000)
def from_timestamp(value: Any) -> Any:
"""Return milliseconds since the epoch as a UTC datetime."""
from pandas import to_datetime
return (
to_datetime(value, unit="ms", origin="unix", utc=True)
if value is not None
else None
)
def from_day_number(value: Any) -> Any:
"""Return a count of whole days since the epoch as a date."""
from pandas import to_datetime
return (
to_datetime(
int(value) * SECONDS_PER_DAY, unit="s", origin="unix", utc=True
).date()
if value is not None
else None
)
def reject_options_by_settlement(currency: str, kind: "str | None") -> None:
"""Refuse a listing of option contracts scoped to a settlement currency.
Only BTC and ETH are both a settlement currency and an underlying. USDC
settles options written on seven different underlyings, so a listing scoped
to it returns a mixture of unrelated assets rather than a chain, and USDT
settles no options at all. Options are listed by the underlying they are
written on, or by instrument name.
Parameters
----------
currency : Currencies
The currency to get instruments for. Default is "BTC".
derivative_type : Optional[DerivativeTypes]
The type of derivative to get instruments for. Default is None, which gets all types.
currency : str
The settlement currency asked for.
kind : str or None
The kind of instrument asked for.
Raises
------
ValueError
If option contracts are asked for by a currency that only settles them.
"""
from openbb_deribit.utils.constants import OPTION_KINDS, UNDERLYING_CURRENCIES
if currency.upper() in UNDERLYING_CURRENCIES or kind not in OPTION_KINDS:
return
raise ValueError(
f"{currency.upper()} settles options, it is not an underlying, so there"
" are no options on it to list. Ask for options by the underlying they"
" are written on, such as BTC_USDC or SOL_USDC, using the symbol"
" parameter."
)
def normalize_currency(currency: str) -> str:
"""Return a currency as the exchange spells it, leaving the wildcard alone."""
return "any" if currency.lower() in ("any", "all") else currency.upper()
async def get_currencies() -> list[dict]:
"""Return every currency the exchange lists."""
from openbb_deribit.utils.client import request
return await request("get_currencies")
async def get_instruments(
currency: str = "any",
kind: str | None = None,
expired: bool = False,
) -> list[dict]:
"""Return the instruments of one currency, optionally of one kind.
Parameters
----------
currency : str
The settlement currency, or 'any' for all of them.
kind : str or None
One of the instrument kinds, or None for all of them.
expired : bool
When True, returns instruments that have already expired.
Returns
-------
list[dict]
A list of instrument dictionaries.
Every matching instrument.
"""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import amake_request
from openbb_deribit.utils.client import request
if currency != "all" and currency.upper() not in CURRENCIES:
raise ValueError(
f"Currency {currency} not supported. Supported currencies are: {', '.join(CURRENCIES)}"
)
if derivative_type and derivative_type not in DERIVATIVE_TYPES:
raise ValueError(
f"Kind {derivative_type} not supported. Supported kinds are: {', '.join(DERIVATIVE_TYPES)}"
)
url = f"{BASE_URL}/api/v2/public/get_instruments?currency={currency.upper() if currency != 'all' else 'any'}"
if derivative_type is not None:
url += f"&kind={derivative_type}"
if expired:
url += f"&expired={str(expired).lower()}"
try:
response = await amake_request(url)
return response.get("result", []) # type: ignore
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(
f"Failed to get instruments -> {e.__class__.__name__}: {e}"
) from e
async def get_options_symbols(symbol: OptionsSymbols = "BTC") -> dict:
"""
Get a dictionary of contract symbols by expiry.
Parameters
----------
symbol : OptionsSymbols
The underlying symbol to get options for. Default is "btc".
Returns
-------
dict[str, str]
A dictionary of contract symbols by expiry date.
"""
# pylint: disable=import-outside-toplevel
from pandas import to_datetime
if symbol.upper() not in DERIBIT_OPTIONS_SYMBOLS:
raise ValueError(
f"Invalid Deribit symbol. Supported symbols are: {', '.join(DERIBIT_OPTIONS_SYMBOLS)}",
)
currency = (
"USDC" if symbol.upper() in ["BNB", "PAXG", "SOL", "XRP"] else symbol.upper()
return await request(
"get_instruments",
{
"currency": normalize_currency(currency),
"kind": kind,
"expired": expired,
},
)
instruments = await get_instruments(currency, "option")
expirations: dict = {}
all_options = list(
set(
d.get("instrument_name")
for d in instruments
if d.get("instrument_name").startswith(symbol)
and d.get("instrument_name").endswith(("-C", "-P"))
)
async def get_instrument(symbol: str) -> dict:
"""Return the definition of one instrument."""
from openbb_deribit.utils.client import request
return await request("get_instrument", {"instrument_name": symbol})
def flatten_ticker(data: dict) -> dict:
"""Return a ticker with its nested statistics and greeks at the top level."""
flat = dict(data)
stats = flat.pop("stats", None) or {}
greeks = flat.pop("greeks", None) or {}
return {**flat, **stats, **greeks}
async def get_ticker(symbol: str) -> dict:
"""Return the quote and statistics of one instrument."""
from openbb_deribit.utils.client import request
return flatten_ticker(await request("ticker", {"instrument_name": symbol}))
async def get_tickers(symbols: list[str]) -> list[dict]:
"""Return the quotes of many instruments, skipping the ones that fail."""
from openbb_deribit.utils.client import gather
results = await gather([("ticker", {"instrument_name": s}) for s in symbols])
return [
flatten_ticker(result)
for result in results
if isinstance(result, dict) and result
]
async def get_index_names(extended: bool = False) -> list:
"""Return the names of every published index."""
from openbb_deribit.utils.client import request
return await request("get_index_price_names", {"extended": extended})
async def get_expirations(
currency: str = "any",
kind: str = "any",
currency_pair: str | None = None,
) -> dict:
"""Return the expirations listed for one currency and kind."""
from openbb_deribit.utils.client import request
return await request(
"get_expirations",
{"currency": currency, "kind": kind, "currency_pair": currency_pair},
)
for item in sorted(
list(
set(
(
to_datetime(d.split("-")[1]).date().strftime("%Y-%m-%d"),
d.split("-")[1],
)
for d in all_options
)
)
):
expirations[item[0]] = item[1]
return {k: [d for d in all_options if v in d] for k, v in expirations.items()}
async def get_futures_curve_symbols(symbol: FuturesCurveSymbols = "BTC") -> list[str]:
"""
Get a list of futures symbols for a given symbol.
def _roots(instruments: list[dict]) -> dict[str, list[dict]]:
"""Group instruments by the root of their name."""
grouped: dict[str, list[dict]] = {}
Parameters
----------
symbol : FuturesCurveSymbols
The symbol to get futures symbols for.
for instrument in instruments:
root = str(instrument.get("instrument_name") or "").split("-")[0]
Returns
-------
list[str]
A list of futures symbols.
"""
symbol = symbol.upper() # type: ignore
if symbol not in DERIBIT_FUTURES_CURVE_SYMBOLS:
raise ValueError(
f"Invalid Deribit symbol. Supported symbols are: {', '.join(DERIBIT_FUTURES_CURVE_SYMBOLS)}",
)
if root:
grouped.setdefault(root, []).append(instrument)
currency = "USDC" if symbol == "PAXG" else symbol
instruments = await get_instruments(currency, "future")
symbols: list = []
for d in instruments:
ins_name = d.get("instrument_name", "")
if ins_name.startswith(symbol):
symbols.append(ins_name)
return symbols
return grouped
async def get_ticker_data(symbol: str) -> dict:
"""
Get ticker data.
async def get_options_roots() -> list[str]:
"""Return every underlying the exchange currently lists options on."""
return sorted(_roots(await get_instruments("any", "option")))
async def get_futures_roots() -> list[str]:
"""Return every underlying the exchange currently lists dated futures on."""
grouped = _roots(await get_instruments("any", "future"))
return sorted(
root
for root, listed in grouped.items()
if sum(1 for d in listed if d.get("settlement_period") != "perpetual") > 1
)
async def get_options_symbols(symbol: str = "BTC") -> dict[str, list[str]]:
"""Return the option symbols of one underlying, keyed by expiration.
Parameters
----------
symbol : str
The symbol to get ticker data for.
The underlying root, as it appears in the instrument name.
Returns
-------
dict
The ticker data.
dict[str, list[str]]
Every contract symbol, keyed by its expiration date.
Raises
------
OpenBBError
If the exchange lists no options on the underlying.
"""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import amake_request
from openbb_core.app.model.abstract.error import OpenBBError
from pandas import to_datetime
url = f"{BASE_URL}/api/v2/public/ticker?instrument_name={symbol}"
root = symbol.upper()
grouped = _roots(await get_instruments("any", "option"))
listed = grouped.get(root)
try:
response = await amake_request(url)
if response.get("error"): # type: ignore[union-attr]
raise OpenBBError(response.get("error")) # type: ignore[union-attr]
data = response.get("result", {}) # type: ignore[union-attr]
stats = data.pop("stats", {})
return {**data, **stats}
if not listed:
raise OpenBBError(
f"Deribit lists no options on {root}."
f" Underlyings with listed options are: {', '.join(sorted(grouped))}"
)
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(f"Failed to get ticker data -> {e}: {e.args[0]}") from e
symbols = sorted(
str(d["instrument_name"])
for d in listed
if str(d.get("instrument_name", "")).endswith(("-C", "-P"))
)
expirations: dict[str, list[str]] = {}
for contract in symbols:
code = contract.split("-")[1]
expiration = to_datetime(code).date().strftime("%Y-%m-%d")
expirations.setdefault(expiration, []).append(contract)
return dict(sorted(expirations.items()))
async def get_perpetual_symbols() -> dict:
"""
Get perpetual symbols.
async def get_futures_curve_symbols(symbol: str = "BTC") -> list[str]:
"""Return the dated future symbols of one underlying.
Parameters
----------
symbol : str
The underlying root, as it appears in the instrument name.
Returns
-------
dict
A dictionary of short symbols to full perpetual symbols.
list[str]
Every contract symbol on the curve, nearest expiration first.
Raises
------
OpenBBError
If the exchange lists no curve for the underlying.
"""
instruments = await get_instruments("all", "future")
from openbb_core.app.model.abstract.error import OpenBBError
root = symbol.upper()
grouped = _roots(await get_instruments("any", "future"))
listed = [
d
for d in grouped.get(root, [])
if d.get("settlement_period") != "perpetual"
and str(d.get("instrument_name", "")).split("-")[0] == root
]
if not listed:
roots = await get_futures_roots()
raise OpenBBError(
f"Deribit lists no futures curve for {root}."
f" Underlyings with a curve are: {', '.join(roots)}"
)
return [
str(d["instrument_name"])
for d in sorted(listed, key=lambda d: d.get("expiration_timestamp") or 0)
]
async def get_perpetual_symbols() -> dict[str, str]:
"""Return every perpetual symbol, keyed by its shortened root."""
instruments = await get_instruments("any", "future")
return {
d["instrument_name"].split("-")[0].replace("_", ""): d["instrument_name"]
str(d["instrument_name"]).split("-")[0].replace("_", ""): str(
d["instrument_name"]
)
for d in instruments
if d.get("settlement_period") == "perpetual"
}
async def get_ohlc_data(
symbol: str, start_date: str, end_date: str, interval: DeribitIntervals = "1d"
) -> list[dict]:
"""Get OHLC data for a given symbol. Enter dates in the format 'YYYY-MM-DD'."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import amake_request
from pandas import DataFrame, date_range, to_datetime
async def get_futures_symbols() -> list[str]:
"""Return every listed future symbol."""
instruments = await get_instruments("any", "future")
new_interval = INTERVAL_MAP.get(interval, interval)
all_instruments = await get_instruments("all", None)
all_symbols = {
d.get("instrument_name"): d.get("creation_timestamp") for d in all_instruments
}
creation_date = all_symbols.get(symbol, 0)
use_start = to_datetime(start_date).timestamp() * 1000 > creation_date
if creation_date == 0:
raise ValueError(f"Symbol {symbol} not found")
def generate_urls(symbol, start_date, end_date, interval, window_size=5000):
"""Generate urls for historical data breaking it down into requests of length window_size."""
interval_period = f"{interval}min" if interval.lower() != "1d" else "1d"
interval = "1D" if interval.lower() == "1d" else interval
dates = date_range(
start=start_date if use_start else creation_date,
end=end_date,
freq=interval_period,
)
windows = [
(dates[i], dates[min(i + window_size, len(dates) - 1)])
for i in range(0, len(dates), window_size)
]
urls: list = []
for start, end in windows:
start_timestamp = int(start.timestamp() * 1000)
end_timestamp = int(end.timestamp() * 1000)
url = (
"https://www.deribit.com/api/v2/public/get_tradingview_chart_data?"
f"instrument_name={symbol}&start_timestamp={start_timestamp}&"
f"end_timestamp={end_timestamp}&resolution={interval}"
)
urls.append(url)
return urls
results: list = []
async def get_one(url):
"""Get data from one url."""
json_response = await amake_request(url)
if json_response.get("error"): # type: ignore[union-attr]
raise ValueError(json_response["error"]) # type: ignore[call-overload]
if json_response.get("result"): # type: ignore[union-attr]
result = json_response["result"] # type: ignore[call-overload]
df = DataFrame(result)
df = (
df.drop(columns=["status"])
.rename(columns={"ticks": "date", "cost": "volume_notional"})
.convert_dtypes()
)
df["date"] = to_datetime(df.date, unit="ms", origin="unix", utc=True)
if interval == "1D":
df.date = df.date.dt.date
df["symbol"] = symbol
results.extend(
df[
[
"date",
"symbol",
"open",
"high",
"low",
"close",
"volume",
"volume_notional",
]
].to_dict(orient="records")
)
urls = generate_urls(symbol, start_date, end_date, new_interval)
if len(urls) > 15:
raise OpenBBError(
"The request is too large. Break up the request into smaller chunks."
)
tasks = [get_one(url) for url in urls]
await asyncio.gather(*tasks, return_exceptions=True)
if results:
return sorted(results, key=lambda x: x["date"])
raise EmptyDataError("No data found for the given symbol and dates.")
return sorted(str(d["instrument_name"]) for d in instruments)
async def check_ohlc_symbol(symbol: str) -> bool | str:
"""
Check if the symbol has OHLC data.
async def resolve_symbol(symbol: str) -> str:
"""Return the instrument name a symbol refers to.
Parameters
----------
symbol : str
The symbol to check.
An instrument name, or the shortened root of a perpetual.
Returns
-------
bool
True if the symbol has OHLC data, False otherwise.
str
The instrument name the exchange knows the symbol by.
Raises
------
OpenBBError
If the exchange lists no such instrument.
"""
all_instruments = await get_instruments("all", None)
all_symbols = {
d.get("instrument_name", ""): d.get("creation_timestamp")
for d in all_instruments
from openbb_core.app.model.abstract.error import OpenBBError
candidate = symbol.upper()
perpetuals = await get_perpetual_symbols()
if candidate in perpetuals:
return perpetuals[candidate]
instruments = await get_instruments("any", None)
listed = {str(d.get("instrument_name")) for d in instruments}
if candidate in listed:
return candidate
raise OpenBBError(f"Deribit lists no instrument named {symbol}.")
def _chart_windows(
start: int, end: int, resolution: str, window: int = CHART_WINDOW_SIZE
) -> list[tuple[int, int]]:
"""Split a span into windows of at most one page of candles each."""
from pandas import date_range
freq = "1D" if resolution == "1D" else f"{resolution}min"
stamps = date_range(start=from_timestamp(start), end=from_timestamp(end), freq=freq)
if len(stamps) <= 1:
return [(start, end)]
return [
(
int(stamps[index].timestamp() * 1000),
int(stamps[min(index + window, len(stamps) - 1)].timestamp() * 1000),
)
for index in range(0, len(stamps), window)
]
async def get_ohlc_data(
symbol: str,
start_date: Any,
end_date: Any,
interval: str = "1d",
) -> list[dict]:
"""Return the candles of one instrument over a span.
Parameters
----------
symbol : str
The instrument name.
start_date : Any
The first date of the span.
end_date : Any
The last date of the span.
interval : str
One of the supported candle intervals.
Returns
-------
list[dict]
One record per candle, oldest first.
Raises
------
OpenBBError
If the span needs more pages than the exchange will serve at once.
EmptyDataError
If the instrument published no candles over the span.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.utils.client import gather
from openbb_deribit.utils.constants import INTERVAL_MAP
resolution = INTERVAL_MAP.get(interval, interval)
instruments = await get_instruments("any", None)
created = {
str(d.get("instrument_name")): d.get("creation_timestamp") for d in instruments
}
all_perpetuals = await get_perpetual_symbols()
if symbol in all_perpetuals:
return all_perpetuals.get(symbol, False)
if symbol in all_symbols:
return symbol
return False
if symbol not in created:
raise OpenBBError(f"Deribit lists no instrument named {symbol}.")
start = max(to_timestamp(start_date), int(created[symbol] or 0))
end = to_timestamp(end_date)
windows = _chart_windows(start, end, resolution)
if len(windows) > MAX_CHART_WINDOWS:
raise OpenBBError(
f"The span covers {len(windows)} pages of {interval} candles, and"
f" Deribit serves at most {MAX_CHART_WINDOWS} at a time."
" Narrow the dates, or widen the interval."
)
results = await gather(
[
(
"get_tradingview_chart_data",
{
"instrument_name": symbol,
"start_timestamp": window_start,
"end_timestamp": window_end,
"resolution": resolution,
},
)
for window_start, window_end in windows
]
)
records: dict[int, dict] = {}
for result in results:
if not isinstance(result, dict) or not result.get("ticks"):
continue
for index, tick in enumerate(result["ticks"]):
records[tick] = {
"date": from_timestamp(tick),
"symbol": symbol,
"open": result["open"][index],
"high": result["high"][index],
"low": result["low"][index],
"close": result["close"][index],
"volume": result["volume"][index],
"volume_notional": result["cost"][index],
}
if not records:
raise EmptyDataError(f"Deribit published no {interval} candles for {symbol}.")
ordered = [records[tick] for tick in sorted(records)]
if resolution == "1D":
for record in ordered:
record["date"] = record["date"].date()
return ordered
async def get_futures_symbols() -> list:
"""
Get futures symbols.
async def get_futures_curve_by_hours_ago(symbol: str, hours: int) -> list[dict]:
"""Return the futures curve of one underlying as it stood some hours ago.
Parameters
----------
symbol : str
The underlying root.
hours : int
How many hours back to read the curve.
Returns
-------
list
A list of futures symbols.
list[dict]
The last price of each contract that had traded by that hour. A contract
the exchange published no candle for is left out, because a curve that
carries the current price under a past label is not a past curve.
"""
instruments = await get_instruments("all", "future")
return [d["instrument_name"] for d in instruments]
from datetime import datetime, timedelta, timezone
async def get_futures_curve_by_hours_ago(symbol, hours):
"""Get futures curve N hours ago."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
from datetime import datetime, timedelta
from openbb_deribit.utils.client import gather
symbols = await get_futures_curve_symbols(symbol)
now = datetime.now().replace(microsecond=0, second=0)
now = datetime.now(timezone.utc).replace(microsecond=0, second=0, minute=0)
target = to_timestamp(now - timedelta(hours=hours))
start = to_timestamp(now - timedelta(hours=hours + CURVE_LOOKBACK_HOURS))
results = await gather(
[
(
"get_tradingview_chart_data",
{
"instrument_name": contract,
"start_timestamp": start,
"end_timestamp": target,
"resolution": "60",
},
)
for contract in symbols
]
)
curve: list[dict] = []
if hours < 24:
start = (now - timedelta(days=1)).strftime("%Y-%m-%d")
else:
start = (now - timedelta(hours=hours)).strftime("%Y-%m-%d")
end = (now + timedelta(days=2)).strftime("%Y-%m-%d")
for contract, result in zip(symbols, results):
if not isinstance(result, dict) or not result.get("ticks"):
continue
results: list = []
ticks = result["ticks"]
index = min(
range(len(ticks)), key=lambda position: abs(ticks[position] - target)
)
curve.append(
{
"instrument_name": contract,
"hours_ago": hours,
"last_price": result["close"][index],
}
)
async def get_one(s):
"""Get data for one symbol."""
data = await get_ohlc_data(s, start, end, "1h")
result = {
"instrument_name": data[-1 - hours]["symbol"],
"hours_ago": hours,
"last_price": data[-1 - hours]["close"],
}
results.append(result)
tasks = [get_one(s) for s in symbols]
await asyncio.gather(*tasks, return_exceptions=True)
return results
return curve

View File

@@ -0,0 +1 @@
"""Deribit options analysis."""

View File

@@ -0,0 +1,337 @@
"""Loading a Deribit option chain for analysis.
The chain endpoint subscribes to every contract over a websocket to collect the
greeks the exchange publishes, which takes about twelve seconds for BTC. The
analysis views need quotes, a volatility and the contract specification rather
than the exchange's own greeks, and all three are reachable over REST in under
a second, so this is where they are read from. The greeks are modelled from the
quoted volatility instead.
"""
import asyncio
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pandas import DataFrame
CHAIN_TTL = 120.0
_LOADED: dict = {}
_LOCKS: dict = {}
_SETTLEMENT = {"BTC": "BTC", "ETH": "ETH"}
def settlement_of(root: str) -> str:
"""Return the currency a root's options are collateralised in."""
return _SETTLEMENT.get(root, "USDC")
async def _source(root: str) -> tuple:
"""Read the quotes, specifications, and index level of one underlying.
The specification is what names a contract's strike, side, size, and
settlement style, so it is read rather than parsed back out of the
instrument name, which spells a fractional strike with a letter.
"""
from openbb_deribit.utils.client import request
from openbb_deribit.utils.helpers import get_instruments
currency = settlement_of(root)
summary, instruments = await asyncio.gather(
request(
"get_book_summary_by_currency", {"currency": currency, "kind": "option"}
),
get_instruments(currency, "option"),
)
specs = {
str(spec["instrument_name"]): spec
for spec in instruments
if str(spec["instrument_name"]).split("-")[0] == root
}
quotes = [row for row in summary if str(row["instrument_name"]) in specs]
index = next(iter(specs.values()), {}).get("price_index")
level = await request("get_index_price", {"index_name": index}) if index else {}
return quotes, specs, float((level or {}).get("index_price") or 0)
def _rows(quotes: list, specs: dict, spot: float) -> list:
"""Build one analysis row per quoted contract.
An inverse contract is quoted in the coin, so its premium is carried to the
quote currency at the index level, which is what the chain endpoint does and
what the valuation layer expects.
"""
from datetime import datetime, timezone
from openbb_deribit.utils.helpers import from_timestamp
from openbb_deribit.utils.options.greeks import SECONDS_PER_YEAR, greeks
now = datetime.now(timezone.utc).timestamp()
built: list[dict] = []
for quote in quotes:
name = str(quote["instrument_name"])
spec = specs[name]
option_type = str(spec["option_type"])
strike = float(spec["strike"])
inverse = str(spec.get("instrument_type")) == "reversed"
carry = spot if inverse else 1.0
forward = float(quote.get("underlying_price") or 0)
volatility = quote.get("mark_iv")
years = (float(spec["expiration_timestamp"]) / 1000 - now) / SECONDS_PER_YEAR
modelled = greeks(
forward, strike, years, float(volatility or 0) / 100, option_type
)
built.append(
{
"contract_symbol": name,
"expiration": from_timestamp(spec["expiration_timestamp"]).date(),
"dte": max(int(years * 365), 0),
"strike": strike,
"option_type": option_type,
"contract_size": float(spec.get("contract_size") or 1),
"tick_size": _carried(spec.get("tick_size"), carry),
"is_inverse": inverse,
"mark": _carried(quote.get("mark_price"), carry),
"bid": _carried(quote.get("bid_price"), carry),
"ask": _carried(quote.get("ask_price"), carry),
"last": _carried(quote.get("last"), carry),
"implied_volatility": volatility,
"open_interest": quote.get("open_interest"),
"volume": quote.get("volume"),
"volume_notional": quote.get("volume_usd"),
"underlying_price": forward,
"underlying_spot_price": spot,
**modelled,
}
)
return built
def _carried(value: Any, carry: float) -> "float | None":
"""Carry one quoted price to the currency the position is measured in."""
return None if value is None else float(value) * carry
async def load_chain(symbol: str, use_cache: bool = True) -> "DataFrame":
"""Return the full chain of one underlying as a frame.
Reading a chain is three REST calls, so a dashboard showing several views of
one underlying would make them once per view, and every parameter a reader
changes would make them again. The last read is held and shared instead: a
nine-widget tab costs three calls, and changing what is drawn costs none.
Parameters
----------
symbol : str
The underlying root, as it appears in the instrument name.
use_cache : bool
Whether a read taken moments ago may be reused.
Returns
-------
DataFrame
One row per contract, with its quote, greeks, and settlement style.
Raises
------
EmptyDataError
If the exchange published no option quotes for the underlying.
"""
from time import monotonic
from openbb_core.provider.utils.errors import EmptyDataError
from pandas import DataFrame
root = symbol.upper()
lock = _LOCKS.setdefault(root, asyncio.Lock())
async with lock:
held = _LOADED.get(root)
if use_cache and held and monotonic() - held[0] < CHAIN_TTL:
return held[1].copy()
quotes, specs, spot = await _source(root)
frame = DataFrame(_rows(quotes, specs, spot))
if frame.empty:
raise EmptyDataError(f"Deribit published no option quotes for {root}.")
_LOADED[root] = (monotonic(), frame)
return frame.copy()
async def load_combos(symbol: str, use_cache: bool = True) -> list:
"""Return the combo books listed on an underlying's options, with their quotes.
Parameters
----------
symbol : str
The underlying root, as it appears in the instrument name.
use_cache : bool
Whether a read taken moments ago may be reused.
Returns
-------
list
One record per combo: its name, its legs as (instrument, amount) pairs,
and its bid and ask in the currency the combo is quoted in.
"""
from time import monotonic
from openbb_deribit.utils.client import request
root = symbol.upper()
key = f"{root}#combos"
lock = _LOCKS.setdefault(key, asyncio.Lock())
async with lock:
held = _LOADED.get(key)
if use_cache and held and monotonic() - held[0] < CHAIN_TTL:
return list(held[1])
currency = settlement_of(root)
listed, quoted = await asyncio.gather(
request("get_combos", {"currency": currency}),
request(
"get_book_summary_by_currency",
{"currency": currency, "kind": "option_combo"},
),
)
quotes = {str(row["instrument_name"]): row for row in quoted or []}
books = []
for combo in listed or []:
legs = [
(str(leg["instrument_name"]), int(leg["amount"]))
for leg in combo["legs"]
]
if not all(
name.split("-")[0] == root and len(name.split("-")) == 4
for name, _ in legs
):
continue
quote = quotes.get(str(combo["id"]), {})
books.append(
{
"name": str(combo["id"]),
"legs": legs,
"bid": quote.get("bid_price"),
"ask": quote.get("ask_price"),
}
)
_LOADED[key] = (monotonic(), books)
return list(books)
def underlying_price(frame: "DataFrame") -> float:
"""Return the spot price of the underlying the chain was quoted against."""
spot = frame["underlying_spot_price"].dropna()
return float(spot.iloc[0]) if not spot.empty else 0.0
def is_inverse(frame: "DataFrame") -> bool:
"""Return whether the chain's contracts settle in the underlying."""
return bool(frame["is_inverse"].any())
def settlement_currency(frame: "DataFrame", symbol: str) -> str:
"""Return what a position in this chain settles in."""
root = symbol.split("_", maxsplit=1)[0].upper()
return root if is_inverse(frame) else "USDC"
def expirations(frame: "DataFrame") -> list:
"""Return every expiration on the chain, nearest first."""
return sorted(frame["expiration"].dropna().unique())
def nearest_expiration(frame: "DataFrame", target: Any) -> Any:
"""Return the expiration closest to a date.
Parameters
----------
frame : DataFrame
The chain.
target : Any
The date the position is held to.
Returns
-------
Any
The listed expiration nearest the target.
"""
from pandas import to_datetime
wanted = to_datetime(target).date()
listed = expirations(frame)
return min(listed, key=lambda day: abs((day - wanted).days))
def far_expiration(frame: "DataFrame", near: Any, gap: int = 30) -> Any:
"""Return the expiration a calendar sells the near one against.
Parameters
----------
frame : DataFrame
The chain.
near : Any
The expiration being traded.
gap : int
How many days past it the far expiration is sought.
Returns
-------
Any
The later expiration nearest the gap, or None when nothing is later.
"""
later = [day for day in expirations(frame) if day > near]
if not later:
return None
return min(later, key=lambda day: abs((day - near).days - gap))
def quotes_at(frame: "DataFrame", expiration: Any = None) -> "DataFrame":
"""Return the tradeable contracts, entered at no less than one tick.
Parameters
----------
frame : DataFrame
The chain.
expiration : Any
The expiration to keep. None keeps every expiration.
Returns
-------
DataFrame
The contracts with a price, each with the price it is entered and left at.
"""
at = (
frame.copy()
if expiration is None
else frame[frame["expiration"] == expiration].copy()
)
for column in ("bid", "ask", "mark", "tick_size"):
at[column] = at[column].astype(float)
entry = at["ask"].where(at["ask"] > 0, at["mark"])
at["entry"] = entry.combine(at["tick_size"].fillna(0), max).where(entry > 0, 0.0)
at["exit"] = at["bid"].where(at["bid"] > 0, at["mark"])
return at[at["entry"] > 0].sort_values(["option_type", "strike"])

View File

@@ -0,0 +1,145 @@
"""The payoff diagram of one Deribit option position."""
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from openbb_core.app.model.obbject import OBBject
def _mark(figure, price: float, text: str, color: str, height: float) -> None:
"""Draw one labelled vertical marker at a price."""
figure.add_vline(x=price, line_width=1, line_dash="dash", line_color=color)
figure.add_annotation(
x=price,
y=height,
yref="paper",
text=text,
showarrow=False,
font=dict(size=10, color=color),
bordercolor=color,
borderwidth=1,
borderpad=3,
bgcolor="rgba(255, 255, 255, 0.85)",
yanchor="middle",
)
def create_payoff(data: dict, theme: str = "dark", **kwargs) -> "OBBject":
"""Draw what one position returns across a range of underlying prices.
Parameters
----------
data : dict
The sized position, the prices it was valued at, the profit at each,
the spot and target prices, and what it settles in.
theme : str
Either 'dark' or 'light'.
Returns
-------
OBBject
The figure, with the evaluated curve as its results.
"""
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import df_to_basemodel
from pandas import DataFrame
from openbb_deribit.utils.options.theme import finalize, new_figure
prices = data["prices"]
values = data["payoff"]
spot = float(data.get("spot") or 0)
target = float(data.get("target") or spot)
settles = str(data.get("settles") or "")
quote = str(data.get("quote") or "USD")
figure, text_color, background = new_figure(theme)
figure.add_scatter(
x=prices,
y=[max(value, 0) for value in values],
mode="lines",
name="Profit",
line=dict(width=0),
fill="tozeroy",
fillcolor="rgba(63, 185, 80, 0.22)",
hoverinfo="skip",
)
figure.add_scatter(
x=prices,
y=[min(value, 0) for value in values],
mode="lines",
name="Loss",
line=dict(width=0),
fill="tozeroy",
fillcolor="rgba(227, 93, 106, 0.22)",
hoverinfo="skip",
)
figure.add_scatter(
x=prices,
y=values,
mode="lines",
name="P/L",
line=dict(color="#2f6fed", width=2),
hovertemplate="<b>%{y:,.6f}</b><extra></extra>",
)
low, high = min(prices), max(prices)
markers = [(spot, f"Spot {spot:,.6g}", "#8b949e")]
if abs(target - spot) > (high - low) / 50:
markers.append((target, f"Target {target:,.6g}", "#8b949e"))
markers += [
(level, f"B/E {level:,.6g}", "#e35d6a")
for level in data.get("breakevens") or []
]
markers = sorted(marker for marker in markers if low <= marker[0] <= high)
for index, (price, text, color) in enumerate(markers):
_mark(figure, price, text, color, 0.95 if index % 2 == 0 else 0.83)
profit = float(data.get("expected_profit") or 0)
cost = float(data.get("cost") or 0)
coin = f" &nbsp;|&nbsp; settled in {settles}" if settles != quote else ""
priced = (
f"for {cost:,.0f} {quote}"
if cost >= 0
else f"for a {abs(cost):,.0f} {quote} credit"
)
listed = f" &nbsp;|&nbsp; {data['combo']}" if data.get("combo") else ""
figure.set_title(
f"{data.get('strategy', '')} {priced}"
f"<br><span style='font-size:12px'>{data.get('legs', '')}{listed}"
f" &nbsp;|&nbsp; expected {profit:+,.0f} {quote}"
f" &nbsp;|&nbsp; spot {spot:,.6g}{coin}</span>",
x=0.5,
font=dict(size=15),
)
figure.update_layout(
paper_bgcolor=background,
plot_bgcolor=background,
font=dict(color=text_color),
margin=dict(l=10, r=10, t=76, b=10),
hovermode="x",
showlegend=False,
xaxis=dict(
title="Underlying price",
showgrid=False,
linecolor=text_color,
hoverformat=",.6g",
autorange=False,
range=[low, high],
),
yaxis=dict(
title=f"Profit and loss ({quote})",
side="left",
showgrid=True,
zeroline=True,
zerolinecolor=text_color,
linecolor=text_color,
),
)
frame = DataFrame({"underlying_price": prices, "profit_or_loss": values})
output: Any = OBBject(results=df_to_basemodel(frame))
return finalize(output, figure, theme)

View File

@@ -0,0 +1,209 @@
"""Deribit implied-volatility smile and skew chart."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
MAX_EXPIRATIONS = 5
COLORS = [
"royalblue",
"red",
"orange",
"green",
"grey",
"burlywood",
"magenta",
"cyan",
"indigo",
"yellowgreen",
]
def _first_priced_expiration(data: OptionsChainsData) -> str:
"""Return the front expiration that still carries non-zero implied volatility.
Parameters
----------
data : OptionsChainsData
The loaded Deribit options chain.
Returns
-------
str
The earliest expiration with usable implied volatility.
"""
df = data.dataframe
priced = df[df["implied_volatility"] > 0]
if priced.empty:
from openbb_deribit.utils.options.data_handler import chain_expirations
expirations = chain_expirations(data)
return expirations[0] if expirations else data.expirations[0]
return sorted(priced["expiration"].astype(str).unique())[0]
def create_smile(
data: OptionsChainsData,
expirations: str | None = None,
otm: bool = False,
skew: bool = False,
**kwargs,
) -> OBBject:
"""Build the IV smile/skew across strikes for up to five expirations.
Parameters
----------
data : OptionsChainsData
The loaded Deribit options chain.
expirations : str | None
Up to five comma-separated expiration dates. Defaults to the front expiry.
otm : bool
When True, restrict each curve to out-of-the-money contracts.
skew : bool
When True, plot the skew relative to ATM instead of raw implied volatility.
Returns
-------
OBBject
The plotted rows, with the Plotly figure attached to ``chart``.
Raises
------
OpenBBError
If the chain carries no implied volatility, or more than five expirations
were requested.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import df_to_basemodel
from pandas import DataFrame, concat, to_datetime
from openbb_deribit.utils.options.theme import finalize, new_figure
if data.has_iv is False:
raise OpenBBError(
"Implied Volatility was not found in the data and is required here."
)
exp_list = (
expirations.split(",")
if isinstance(expirations, str)
else expirations
if isinstance(expirations, list)
else [_first_priced_expiration(data)]
)
exp_list = [data._get_nearest_expiration(e) for e in exp_list]
if len(exp_list) > MAX_EXPIRATIONS:
raise OpenBBError("Too many dates! Up to five can be selected.")
df = concat(
[data.skew(date=cast(str, to_datetime(exp).date())) for exp in exp_list]
)
output_df = DataFrame()
symbol = data.underlying_symbol[0]
index_name = exp_list[0] if len(exp_list) <= 1 else None
target_col = "Skew" if skew is True else "IV"
title = (
f"{symbol} {'OTM ' if otm is True else ''}"
f"{'IV Skew' if skew is True else 'Implied Volatility'}"
)
theme = kwargs.get("theme") or "dark"
fig, text_color, background = new_figure(theme)
color = -1
for expiration in exp_list:
calls = (
df.query("`Expiration` == @expiration & `Option Type` == 'call'")
.copy()
.reset_index(drop=True)
)
puts = (
df.query("`Expiration` == @expiration & `Option Type` == 'put'")
.copy()
.reset_index(drop=True)
)
if otm is True:
put_idx = puts[puts["Skew"] == 0].index.values[0]
call_idx = calls[calls["Skew"] == 0].index.values[0]
puts = puts.iloc[0 : put_idx + 1].reset_index(drop=True)
calls = calls.iloc[call_idx:-1].reset_index(drop=True)
output_df = (
concat([output_df, calls, puts], axis=0)
if not output_df.empty
else concat([calls, puts], axis=0)
)
color = color + 1
fig.add_scatter(
x=calls["Strike"].unique().tolist(),
y=calls[target_col],
mode="lines+markers",
name="Calls" if len(exp_list) <= 1 else f"Calls at {expiration}",
marker_color=COLORS[color],
hoverinfo="x+y+name",
)
color = color + 1
fig.add_scatter(
x=puts["Strike"].unique().tolist(),
y=puts[target_col],
mode="lines+markers",
name="Puts" if len(exp_list) <= 1 else f"Puts at {expiration}",
marker_color=COLORS[color],
hoverinfo="x+y+name",
)
fig.update_layout(
title=dict(text=title, x=0.5, font=dict(size=18)),
yaxis=dict(
ticklen=0,
showline=False,
linecolor=text_color,
tickfont=dict(size=14),
nticks=7,
),
xaxis=dict(
showgrid=False,
autorange=True,
ticklen=5,
showline=False,
linecolor=text_color,
title=dict(text=index_name if index_name else "", font=dict(size=16)),
tickfont=dict(size=16),
nticks=7,
showspikes=False,
tickprefix="$",
),
legend=dict(
font=dict(size=14),
orientation="v",
yanchor="top",
y=1,
xanchor="center",
x=0.5,
itemdoubleclick="toggleothers",
),
font=dict(color=text_color),
paper_bgcolor=background,
plot_bgcolor=background,
hoverdistance=1,
hovermode="x unified",
dragmode="pan",
)
output_df = (
output_df.set_index(["Expiration", "Strike", "Option Type"])
.sort_index()
.reset_index()
)
output: Any = OBBject(results=df_to_basemodel(output_df))
return finalize(output, fig, theme)

View File

@@ -0,0 +1,143 @@
"""Deribit open-interest and volume statistics chart."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
def create_stats(
data: OptionsChainsData,
by: Literal["strike", "expiration"] = "expiration",
metric: Literal["oi", "volume"] = "oi",
date: str | None = None,
unit: Literal["value", "percent", "pcr"] = "value",
**kwargs,
) -> OBBject:
"""Chart open interest or volume by strike or expiration.
Parameters
----------
data : OptionsChainsData
The loaded Deribit options chain.
by : Literal["strike", "expiration"]
The axis to aggregate over.
metric : Literal["oi", "volume"]
Open interest or contract volume.
date : str | None
Restrict to a single expiration, which forces the strike axis.
unit : Literal["value", "percent", "pcr"]
Raw values, share of total, or the put/call ratio.
Returns
-------
OBBject
The aggregated rows, with the Plotly figure attached to ``chart``.
"""
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import df_to_basemodel
from openbb_deribit.utils.options.theme import finalize, new_figure
stat = "open_interest" if metric == "oi" else metric
if date is not None:
date = data._get_nearest_expiration(date)
stats = data.filter_data(by=by, date=date, stat=stat)
else:
stats = data.filter_data(by=by, stat=stat)
symbol = data.underlying_symbol[-1]
stat_type = stat.replace("_", " ").title()
index_name = "Expiration" if by == "expiration" and not date else "Strike"
stats = stats.set_index(index_name)
stats.Puts = stats.Puts * (-1)
title = (
f"{symbol} {stat_type} (%)"
if unit == "percent"
else f"{symbol} {stat_type} By {index_name}"
)
theme = kwargs.get("theme") or "dark"
fig, text_color, background = new_figure(theme)
xtick_vals = (
[
round(d) if "." in str(d) and str(d).endswith(".0") else round(d, 2)
for d in stats.index.tolist()
]
if by == "strike"
else stats.index
)
if unit == "percent":
stats = stats.dropna(subset=["Net Percent"])
fig.add_bar(
x=stats.index,
y=stats["Net Percent"],
name="% of Total",
orientation="v",
hovertemplate="%{y:.4f}%",
)
elif unit == "pcr":
title = f"{symbol} {stat_type} Put/Call Ratio"
fig.add_bar(
x=xtick_vals,
y=stats.PCR,
orientation="v",
name="Put/Call Ratio",
hovertemplate="%{y:.4f}",
)
else:
fig.add_bar(
x=xtick_vals,
y=stats.Calls,
name="Calls",
orientation="v",
marker=dict(color="royalblue"),
hovertemplate="%{y:.0f}",
)
fig.add_bar(
x=xtick_vals,
y=stats.Puts,
name="Puts",
orientation="v",
marker=dict(color="red"),
hovertemplate="%{y:.0f}",
)
fig.update_traces(width=0.95, selector=dict(type="bar"))
fig.set_title(title, x=0.5, font=dict(size=16))
fig.update_layout(
paper_bgcolor=background,
plot_bgcolor=background,
barmode="relative",
yaxis=dict(
ticklen=0,
showgrid=True,
tickfont=dict(size=12),
automargin=True,
linecolor=text_color,
showline=True,
),
xaxis=dict(
showgrid=False,
tickfont=dict(size=11),
ticklen=0,
type="category",
linecolor=text_color,
showline=True,
nticks=10,
tickprefix="$" if by == "strike" else "",
showspikes=False,
),
legend=dict(orientation="v", yanchor="top", y=0.90, xanchor="right", x=-0.01),
font=dict(color=text_color),
margin=dict(l=10, r=10, t=10, b=10),
hovermode="x unified",
)
stats.Puts = stats.Puts * (-1)
output: Any = OBBject(results=df_to_basemodel(stats))
return finalize(output, fig, theme)

View File

@@ -0,0 +1,199 @@
"""Deribit options 3-D surface chart over DTE and strike."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
SurfaceMetric = Literal[
"implied_volatility", "delta", "gamma", "theta", "vega", "rho", "dex", "gex"
]
METRIC_TITLES = {
"implied_volatility": "IV",
"delta": "Delta",
"gamma": "Gamma",
"theta": "Theta",
"vega": "Vega",
"rho": "Rho",
"dex": "DEX",
"gex": "GEX",
}
OPTION_TYPE_LABELS = {"calls": "Call", "puts": "Put", "otm": "OTM", "itm": "ITM"}
def create_surface(
data: OptionsChainsData,
option_type: Literal["otm", "itm", "puts", "calls"] = "otm",
metric: SurfaceMetric = "implied_volatility",
dte_range: list[int] | None = None,
moneyness: float | None = None,
oi: bool = False,
volume: bool = False,
**kwargs,
) -> OBBject:
"""Chart an option metric as a 3-D surface over DTE and strike.
Parameters
----------
data : OptionsChainsData
The loaded Deribit options chain.
option_type : Literal["otm", "itm", "puts", "calls"]
Which side of the chain to plot.
metric : SurfaceMetric
The Z-axis metric.
dte_range : list[int] | None
Inclusive lower and upper days-to-expiry bounds.
moneyness : float | None
Restrict strikes to within this percent of the underlying price.
oi : bool
When True, drop contracts with no open interest.
volume : bool
When True, drop contracts that have not traded.
Returns
-------
OBBject
The plotted rows, with the Plotly figure attached to ``chart``.
Raises
------
OpenBBError
If the requested metric is not present in the chain.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.charts.chart import Chart
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import df_to_basemodel
from pandas import concat
from openbb_deribit import CHARTING_INSTALLED
from openbb_deribit.utils.options.theme import CHART_CONFIG
options = data.dataframe
column_map = {c.lower(): c for c in options.columns}
target = column_map.get(metric.lower(), metric)
if target not in options.columns:
raise OpenBBError(f"No {metric} data available in this chain.")
last_price = float(data.underlying_price[0] or 0.0)
symbol = data.underlying_symbol[0]
calls = options.query(f"`option_type` == 'call' & `dte` >= 0 & `{target}` != 0")
puts = options.query(f"`option_type` == 'put' & `dte` >= 0 & `{target}` != 0")
if oi:
calls = calls[calls["open_interest"] > 0]
puts = puts[puts["open_interest"] > 0]
if volume:
calls = calls[calls["volume"] > 0]
puts = puts[puts["volume"] > 0]
def _between(frame, col, low, high):
"""Filter a frame to an inclusive range on one column."""
return frame[(frame[col] >= low) & (frame[col] <= high)]
if dte_range is not None and len(dte_range) > 1:
calls = _between(calls, "dte", min(dte_range), max(dte_range))
puts = _between(puts, "dte", min(dte_range), max(dte_range))
if moneyness is not None and moneyness > 0:
low = (1 - (moneyness / 100)) * last_price
high = (1 + (moneyness / 100)) * last_price
calls = _between(calls, "strike", low, high)
puts = _between(puts, "strike", low, high)
keys = ["expiration", "strike", "option_type"]
if option_type == "otm":
df = (
concat(
[
calls.query("`strike` > @last_price").set_index(keys),
puts.query("`strike` < @last_price").set_index(keys),
]
)
.sort_index()
.reset_index()
)
elif option_type == "itm":
df = (
concat(
[
calls.query("`strike` < @last_price").set_index(keys),
puts.query("`strike` > @last_price").set_index(keys),
]
)
.sort_index()
.reset_index()
)
elif option_type == "calls":
df = calls
else:
df = puts
columns = [
"expiration",
"strike",
"option_type",
"dte",
target,
"open_interest",
"volume",
]
df = df[[c for c in dict.fromkeys(columns) if c in df.columns]]
metric_title = METRIC_TITLES.get(metric, metric.replace("_", " ").title())
label = f"{symbol} {OPTION_TYPE_LABELS[option_type]} {metric_title} Surface"
if oi:
label += " With Open Interest"
if volume:
label += " Excluding Untraded Contracts"
theme = kwargs.get("theme") or "dark"
df.expiration = df.expiration.astype(str)
output: Any = OBBject(results=df_to_basemodel(df))
if not CHARTING_INSTALLED:
return output
from openbb_charting.charts.generic_charts import surface3d
fig = surface3d(
X=df["dte"],
Y=df["strike"],
Z=df[target],
xtitle="DTE",
ytitle="Strike",
ztitle=metric_title,
title=label,
theme=theme,
)
text_color = "white" if theme == "dark" else "black"
grid_color = "rgba(255,255,255,0.15)" if theme == "dark" else "rgba(0,0,0,0.15)"
wall_color = "rgba(255,255,255,0.04)" if theme == "dark" else "rgba(0,0,0,0.04)"
scene_axis = dict(
color=text_color, gridcolor=grid_color, backgroundcolor=wall_color
)
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font=dict(color=text_color),
title=dict(font=dict(color=text_color)),
scene=dict(xaxis=scene_axis, yaxis=scene_axis, zaxis=scene_axis),
hoverlabel=dict(
bgcolor="#0E0E0E" if theme == "dark" else "#FFFFFF",
bordercolor=text_color,
font=dict(color=text_color, size=12),
),
)
content = fig.show(config=CHART_CONFIG, external=True).to_plotly_json()
output.chart = Chart(fig=fig, content=content, format="plotly")
return output

View File

@@ -0,0 +1,191 @@
"""Deribit options term-structure chart across expirations."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
def create_term_structure(
data: OptionsChainsData,
strike: float | None = None,
moneyness: float | None = None,
metric: Literal["price", "iv"] = "iv",
option_type: Literal["both", "calls", "puts"] = "both",
**kwargs,
) -> OBBject:
"""Chart price or implied volatility at the nearest strike across expirations.
Parameters
----------
data : OptionsChainsData
The loaded Deribit options chain.
strike : float | None
Target strike. Defaults to the nearest out-of-the-money strike per expiry.
moneyness : float | None
Select strikes this percent out-of-the-money instead of a fixed strike.
metric : Literal["price", "iv"]
Contract price or implied volatility.
option_type : Literal["both", "calls", "puts"]
Which side of the chain to plot.
Returns
-------
OBBject
The plotted rows, with the Plotly figure attached to ``chart``.
Raises
------
OpenBBError
If implied volatility was requested but is absent from the chain.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import df_to_basemodel
from pandas import DataFrame, concat
from openbb_deribit.utils.options.theme import finalize, new_figure
if metric == "iv" and not data.has_iv:
raise OpenBBError("No implied volatility data available.")
from openbb_deribit.utils.options.data_handler import chain_expirations
df = data.dataframe.copy()
expirations = chain_expirations(data)
symbol = data.underlying_symbol[0]
price_col = (
"last_trade_price"
if "last_trade_price" in df.columns
else data._identify_price_col(df, "call", "ask")
)
target_col_map = {"price": price_col, "iv": "implied_volatility"}
df.expiration = df.expiration.astype(str)
base = f"{symbol} {'Implied Volatility' if metric == 'iv' else 'Price'}"
if strike:
title = f"{base} @ Strike Nearest To ${strike}"
elif moneyness:
title = f"{base} @ {moneyness}% Moneyness"
else:
title = f"{base} Nearest OTM Strikes"
calls = DataFrame()
puts = DataFrame()
for expiration in expirations:
nearest_otm = (
data._get_nearest_otm_strikes(expiration, moneyness=moneyness)
if moneyness
else {}
)
call_strike = (
nearest_otm.get("call")
if nearest_otm
else data._get_nearest_strike(
option_type="call", days=expiration, strike=strike
)
)
put_strike = (
nearest_otm.get("put")
if nearest_otm
else data._get_nearest_strike(
option_type="put", days=expiration, strike=strike
)
)
calls_filtered = df[
(df.expiration == expiration)
& (df.option_type == "call")
& (df[price_col] > 0)
]
if not calls_filtered.empty:
calls = concat(
[
calls,
calls_filtered.iloc[
(calls_filtered.strike - call_strike).abs().argsort()[:1]
],
]
)
puts_filtered = df[
(df.expiration == expiration)
& (df.option_type == "put")
& (df[price_col] > 0)
]
if not puts_filtered.empty:
puts = concat(
[
puts,
puts_filtered.iloc[
(puts_filtered.strike - put_strike).abs().argsort()[:1]
],
]
)
output_df = concat([calls, puts], axis=0).reset_index(drop=True)
theme = kwargs.get("theme") or "dark"
fig, text_color, background = new_figure(theme)
hovertemplate = (
"$<b>%{y} @ $</b>%{customdata} Strike<extra></extra>"
if metric == "price"
else "<b>%{y} @ $</b>%{customdata} Strike<extra></extra>"
)
if option_type in ["calls", "both"] and not calls.empty:
fig.add_scatter(
x=calls["expiration"],
y=calls[target_col_map[metric]],
mode="lines+markers",
name="Calls",
marker_color="royalblue",
hovertemplate=hovertemplate,
customdata=calls["strike"],
)
if option_type in ["puts", "both"] and not puts.empty:
fig.add_scatter(
x=puts["expiration"],
y=puts[target_col_map[metric]],
mode="lines+markers",
name="Puts",
marker_color="red",
hovertemplate=hovertemplate,
customdata=puts["strike"],
)
fig.set_title(title, x=0.5, font=dict(size=16))
fig.update_layout(
paper_bgcolor=background,
plot_bgcolor=background,
yaxis=dict(
ticklen=0,
showgrid=True,
tickfont=dict(size=12),
automargin=True,
linecolor=text_color,
showline=True,
),
xaxis=dict(
showgrid=False,
tickfont=dict(size=11),
ticklen=0,
type="category",
linecolor=text_color,
showline=True,
nticks=10,
showspikes=False,
),
legend=dict(orientation="v", yanchor="top", y=0.90, xanchor="right", x=-0.01),
hovermode="x unified",
font=dict(color=text_color),
margin=dict(l=10, r=10, t=10, b=10),
)
output: Any = OBBject(results=df_to_basemodel(output_df))
return finalize(output, fig, theme)

View File

@@ -0,0 +1,144 @@
"""Shared Deribit options chain, as the core model the options charts draw from."""
from typing import Any
CHAIN_COLUMNS = {
"contract_symbol": "contract_symbol",
"expiration": "expiration",
"dte": "dte",
"strike": "strike",
"option_type": "option_type",
"contract_size": "contract_size",
"open_interest": "open_interest",
"volume": "volume",
"last": "last_trade_price",
"bid": "bid",
"ask": "ask",
"mark": "mark",
"implied_volatility": "implied_volatility",
"delta": "delta",
"gamma": "gamma",
"theta": "theta",
"vega": "vega",
"rho": "rho",
"underlying_price": "underlying_price",
}
def chain_expirations(data: Any) -> list[str]:
"""Return the expirations that still have contracts in the chain.
Parameters
----------
data : Any
The loaded ``OptionsChainsData``.
Returns
-------
list[str]
Sorted expirations that have rows, falling back to the model's own list
if the frame cannot be built.
"""
try:
frame = data.dataframe
except Exception: # noqa: BLE001
return list(data.expirations)
if "expiration" not in frame.columns:
return list(data.expirations)
return sorted({str(value) for value in frame["expiration"]})
def to_chain(frame: Any, symbol: str) -> Any:
"""Return a loaded chain frame as the core ``OptionsChainsData``.
Parameters
----------
frame : DataFrame
The chain, as ``load_chain`` returns it.
symbol : str
The underlying root, as it appears in the instrument name.
Returns
-------
OptionsChainsData
One entry per contract, with implied volatility as a decimal.
"""
from numpy import nan
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
columns = frame.reindex(columns=list(CHAIN_COLUMNS)).rename(columns=CHAIN_COLUMNS)
spot = frame["underlying_spot_price"].astype(float)
forward = columns["underlying_price"].astype(float)
columns["underlying_price"] = forward.where(forward > 0, spot)
columns["implied_volatility"] = columns["implied_volatility"].astype(float) / 100
columns["underlying_symbol"] = symbol.upper()
columns = columns.sort_values(["expiration", "strike", "option_type"])
columns = columns.astype(object).replace({nan: None}).reset_index(drop=True)
return OptionsChainsData.model_validate(columns.to_dict(orient="list"))
async def load_symbol(symbol: str) -> Any:
"""Return the chain of one underlying as the core ``OptionsChainsData``.
Parameters
----------
symbol : str
The underlying root, as it appears in the instrument name.
Returns
-------
OptionsChainsData
The chain the options charts are drawn from.
Raises
------
OpenBBError
If the exchange lists no options on the underlying.
"""
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.utils.options import chain
root = symbol.upper()
try:
frame = await chain.load_chain(root)
except (OpenBBError, EmptyDataError) as exc:
raise OpenBBError(f"No options available for {root}.") from exc
return to_chain(frame, root)
def get_strikes(data: Any) -> list[dict]:
"""Return the strike choices of a chain for its dropdown.
Parameters
----------
data : OptionsChainsData
The loaded chain.
Returns
-------
list[dict]
``[{label, value}]`` strike choices, led by the nearest out of the money.
"""
underlying = data.underlying_price[0] if data.underlying_price else None
choices: list[dict] = [{"label": "Nearest OTM", "value": None}]
for strike in data.strikes:
extra = (
{"rightOfDescription": f"Underlying: ${underlying}"} if underlying else {}
)
choices.append(
{
"label": f"${int(strike) if strike.is_integer() else strike}",
"value": strike,
"extraInfo": extra,
}
)
return choices

View File

@@ -0,0 +1,79 @@
"""Greeks for a Deribit option, off its forward price and quoted volatility.
The book summary the analysis suite reads publishes a volatility but no greeks,
so they are modelled here. Measured against the greeks Deribit publishes over
all 972 BTC contracts, delta agrees to a median of 0.0001, gamma to 0.000002
and vega to 0.03.
Theta follows the exchange's own two rules rather than the bare derivative: it
is scaled down inside the last day, where the derivative diverges but the
contract cannot decay faster than it expires, and it is floored at the
contract's own value, because nothing decays by more than it is worth. Both
rules bind on real contracts and together bring the worst disagreement across
the chain from 193 to 0.11 per day.
"""
from math import exp, log, pi, sqrt
SECONDS_PER_YEAR = 365 * 86400
def _pdf(x: float) -> float:
"""Return the standard normal density at a point."""
return exp(-0.5 * x * x) / sqrt(2 * pi)
def greeks(
forward: float, strike: float, years: float, sigma: float, option_type: str
) -> dict:
"""Return the greeks of one unit of the underlying.
Parameters
----------
forward : float
The forward price of the underlying at expiration.
strike : float
The strike of the contract.
years : float
The time to expiration, in years.
sigma : float
The implied volatility, as a decimal.
option_type : str
Either 'call' or 'put'.
Returns
-------
dict
Delta, gamma, theta, vega, and rho. Every value is None when the
contract has expired or carries no volatility to model it with.
"""
from statistics import NormalDist
if years <= 0 or sigma <= 0 or forward <= 0 or strike <= 0:
return {name: None for name in ("delta", "gamma", "theta", "vega", "rho")}
normal = NormalDist()
deviation = sigma * sqrt(years)
d1 = (log(forward / strike) + 0.5 * sigma**2 * years) / deviation
d2 = d1 - deviation
density = _pdf(d1)
call = option_type.startswith("c")
value = (
forward * normal.cdf(d1) - strike * normal.cdf(d2)
if call
else strike * normal.cdf(-d2) - forward * normal.cdf(-d1)
)
decay = -forward * density * sigma / (2 * sqrt(years)) / 365
return {
"delta": normal.cdf(d1) if call else -normal.cdf(-d1),
"gamma": density / (forward * deviation),
"theta": max(decay * min(1.0, years * 365), -value),
"vega": forward * density * sqrt(years) / 100,
"rho": (
strike * years * normal.cdf(d2) / 100
if call
else -strike * years * normal.cdf(-d2) / 100
),
}

View File

@@ -0,0 +1,248 @@
"""Valuing Deribit option positions.
The chain quotes every contract in its quote currency per unit of the
underlying, so a position is valued the same way whichever settlement style a
contract has. What differs is where the profit lands: an inverse contract pays
in the coin, so its result is also reported there, which is the number the
account actually moves by.
"""
MIN_VOL = 0.0001
MAX_VOL = 5.0
VOL_STEPS = 100
def intrinsic(price: float, strike: float, option_type: str) -> float:
"""Return the exercise value of one unit of the underlying."""
return (
max(0.0, price - strike)
if option_type.startswith("c")
else max(0.0, strike - price)
)
def option_value(
forward: float,
strike: float,
years: float,
sigma: float,
discount: float,
option_type: str,
) -> float:
"""Value one unit of the underlying off its forward price.
Parameters
----------
forward : float
The forward price of the underlying at expiration.
strike : float
The strike of the contract.
years : float
The time to expiration, in years.
sigma : float
The implied volatility, as a decimal.
discount : float
The discount factor to expiration.
option_type : str
Either 'call' or 'put'.
Returns
-------
float
The value of one unit, in the quote currency.
"""
from math import log, sqrt
from statistics import NormalDist
if years <= 0 or sigma <= 0 or forward <= 0 or strike <= 0:
return discount * intrinsic(forward, strike, option_type)
normal = NormalDist()
deviation = sigma * sqrt(years)
d1 = (log(forward / strike) + 0.5 * sigma**2 * years) / deviation
d2 = d1 - deviation
if option_type.startswith("c"):
return discount * (forward * normal.cdf(d1) - strike * normal.cdf(d2))
return discount * (strike * normal.cdf(-d2) - forward * normal.cdf(-d1))
def implied_vol(
price: float,
forward: float,
strike: float,
years: float,
discount: float,
option_type: str,
) -> "float | None":
"""Return the volatility one quoted price implies.
A chain publishes a volatility alongside a price, and the two do not always
agree. Valuing a position at a volatility its own quote does not imply
prices the exit off a different market than the entry, which shows a
position as losing before it has moved.
Parameters
----------
price : float
The quoted price of one unit.
forward : float
The forward price of the underlying at expiration.
strike : float
The strike of the contract.
years : float
The time to expiration, in years.
discount : float
The discount factor to expiration.
option_type : str
Either 'call' or 'put'.
Returns
-------
float or None
The volatility the price implies, or None when no volatility does.
"""
if price <= 0 or years <= 0 or forward <= 0 or strike <= 0:
return None
if price <= discount * intrinsic(forward, strike, option_type):
return None
low, high = MIN_VOL, MAX_VOL
if option_value(forward, strike, years, high, discount, option_type) < price:
return None
for _ in range(VOL_STEPS):
middle = (low + high) / 2
if option_value(forward, strike, years, middle, discount, option_type) < price:
low = middle
else:
high = middle
return (low + high) / 2
def leg_value(leg: dict, price: float, elapsed: int) -> float:
"""Return what one unit of a leg is worth at a price.
Parameters
----------
leg : dict
The contract, its volatility, and its days to expiration.
price : float
The price of the underlying at the valuation date.
elapsed : int
Days from now to the valuation date.
Returns
-------
float
The exercise value of a leg that has expired by the valuation date,
otherwise its modelled value, in the quote currency per unit.
"""
remaining = int(leg.get("dte") or 0) - elapsed
if remaining <= 0 or not leg.get("implied_volatility"):
return intrinsic(price, float(leg["strike"]), str(leg["option_type"]))
return option_value(
price,
float(leg["strike"]),
remaining / 365,
float(leg["implied_volatility"]),
1.0,
str(leg["option_type"]),
)
def net_cost(legs: "list[dict]") -> float:
"""Return what a position costs to open, in the quote currency.
A negative result is a credit.
"""
return sum(
float(leg["quantity"]) * float(leg["size"]) * float(leg["price"])
for leg in legs
)
def position_value(legs: "list[dict]", price: float, elapsed: int) -> float:
"""Return what a position is worth at a price, in the quote currency."""
return sum(
float(leg["quantity"]) * float(leg["size"]) * leg_value(leg, price, elapsed)
for leg in legs
)
def payoff(
legs: "list[dict]",
prices: "list[float]",
elapsed: int = 0,
inverse: bool = False,
entry_price: "float | None" = None,
cost: "float | None" = None,
) -> "list[float]":
"""Return the profit and loss of a position across a price range, in the quote currency.
Parameters
----------
legs : list[dict]
Each leg's strike, type, quantity, contract size, price, days to
expiration, and implied volatility.
prices : list[float]
The underlying prices to evaluate.
elapsed : int
Days from now to the valuation date.
inverse : bool
Whether the contracts settle in the underlying.
entry_price : float or None
The price of the underlying when the position was opened, which an
inverse premium was paid at. Required when inverse.
cost : float or None
What the position was opened for, in the quote currency, when it was
traded as one combo rather than leg by leg. None prices it off its legs.
Returns
-------
list[float]
The profit and loss at each price, in the quote currency.
Raises
------
ValueError
If an inverse position is valued without the price it was opened at.
"""
entry = net_cost(legs) if cost is None else cost
if not inverse:
return [position_value(legs, price, elapsed) - entry for price in prices]
if not entry_price:
raise ValueError(
"An inverse position needs the price it was opened at, because its"
" premium was paid in the coin at that price."
)
paid = entry / entry_price
return [position_value(legs, price, elapsed) - paid * price for price in prices]
def breakevens(prices: "list[float]", values: "list[float]") -> "list[float]":
"""Return every price where a payoff crosses zero."""
crossings: list[float] = []
for index in range(1, len(values)):
if (values[index - 1] < 0) == (values[index] < 0):
continue
low, high = values[index - 1], values[index]
weight = abs(low) / (abs(low) + abs(high)) if (low or high) else 0
crossings.append(
prices[index - 1] + weight * (prices[index] - prices[index - 1])
)
return crossings

View File

@@ -0,0 +1,764 @@
"""Ranking every Deribit combo structure against a view."""
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pandas import DataFrame
PROBE_STEPS = 400
PROBE_SPREAD = 0.6
SCOUT_STEPS = 300
WINDOW_PAD = 0.06
MIN_PAD = 0.0025
FLAT_SLOPE = 0.02
ANCHOR_REACH = 0.35
SHORTLIST = 40
WING_REACH = 4
STRIKE_WINDOW = 0.35
STRESS_MOVE = 3.0
STRESS_STEPS = 60
DIAGONAL_REACH = 3
NAMES = {
("C", 1): "Long Call",
("C", -1): "Short Call",
("P", 1): "Long Put",
("P", -1): "Short Put",
("CS", 1): "Bull Call Spread",
("CS", -1): "Bear Call Spread",
("PS", 1): "Bear Put Spread",
("PS", -1): "Bull Put Spread",
("STRD", 1): "Long Straddle",
("STRD", -1): "Short Straddle",
("STRG", 1): "Long Strangle",
("STRG", -1): "Short Strangle",
("RR", 1): "Bearish Risk Reversal",
("RR", -1): "Bullish Risk Reversal",
("REV", 1): "Synthetic Long",
("REV", -1): "Synthetic Short",
("CSR12", 1): "Call Ratio Spread",
("CSR12", -1): "Call Backspread",
("PSR12", 1): "Put Ratio Spread",
("PSR12", -1): "Put Backspread",
("CBUT", 1): "Long Call Butterfly",
("CBUT", -1): "Short Call Butterfly",
("PBUT", 1): "Long Put Butterfly",
("PBUT", -1): "Short Put Butterfly",
("IBUT", 1): "Iron Butterfly",
("IBUT", -1): "Reverse Iron Butterfly",
("CCOND", 1): "Long Call Condor",
("CCOND", -1): "Short Call Condor",
("PCOND", 1): "Long Put Condor",
("PCOND", -1): "Short Put Condor",
("ICOND", 1): "Iron Condor",
("ICOND", -1): "Reverse Iron Condor",
("CCAL", 1): "Long Call Calendar",
("CCAL", -1): "Short Call Calendar",
("PCAL", 1): "Long Put Calendar",
("PCAL", -1): "Short Put Calendar",
("CDIAG", 1): "Long Call Diagonal",
("CDIAG", -1): "Short Call Diagonal",
("PDIAG", 1): "Long Put Diagonal",
("PDIAG", -1): "Short Put Diagonal",
}
def _leg(row: Any, quantity: int) -> dict:
"""Describe one contract as a leg of a position, volatility as a decimal."""
quoted = row["implied_volatility"]
return {
"symbol": str(row["contract_symbol"]),
"expiration": str(row["expiration"]),
"strike": float(row["strike"]),
"option_type": str(row["option_type"]),
"quantity": quantity,
"size": float(row["contract_size"] or 1),
"price": float(row["entry"] if quantity > 0 else row["exit"]),
"dte": int(row["dte"] or 0),
"implied_volatility": None if quoted is None else float(quoted) / 100,
}
def _describe(legs: "list[dict]") -> str:
"""Name the contracts a position trades, and how many of each."""
parts: list[str] = []
for leg in legs:
side = "Buy" if leg["quantity"] > 0 else "Sell"
count = abs(int(leg["quantity"]))
parts.append(
f"{side} {leg['symbol']}"
if count == 1
else f"{side} {count} {leg['symbol']}"
)
return " / ".join(parts)
def _key(legs: "list[dict]") -> frozenset:
"""Return what identifies a position: each contract and how much of it."""
return frozenset((leg["symbol"], int(leg["quantity"])) for leg in legs)
def _sign(value: float) -> int:
"""Return the direction of a quantity."""
return 1 if value > 0 else -1
def _two_legs(first: dict, second: dict) -> tuple:
"""Classify a two-legged position on one expiration."""
kinds = {first["option_type"][0], second["option_type"][0]}
if len(kinds) == 2:
call, put = (
(first, second) if first["option_type"][0] == "c" else (second, first)
)
if abs(call["quantity"]) != 1 or abs(put["quantity"]) != 1:
return "", 1
together = call["quantity"] == put["quantity"]
if put["strike"] == call["strike"]:
return (
("STRD", _sign(call["quantity"]))
if together
else (
"REV",
_sign(call["quantity"]),
)
)
if put["strike"] < call["strike"]:
return (
("STRG", _sign(call["quantity"]))
if together
else (
"RR",
_sign(put["quantity"]),
)
)
return "", 1
low, high = sorted((first, second), key=lambda leg: leg["strike"])
kind = "C" if low["option_type"][0] == "c" else "P"
units = (int(low["quantity"]), int(high["quantity"]))
if low["strike"] == high["strike"]:
return "", 1
if units in ((1, -1), (-1, 1)):
return f"{kind}S", _sign(units[0] if kind == "C" else units[1])
ratios = {"C": ((1, -2), (-1, 2)), "P": ((-2, 1), (2, -1))}
if units in ratios[kind]:
return f"{kind}SR12", 1 if units == ratios[kind][0] else -1
return "", 1
def classify(legs: "list[dict]") -> tuple:
"""Return the Deribit combo code of a position and the side it is taken on.
Parameters
----------
legs : list[dict]
The unit legs of the position.
Returns
-------
tuple
The code, such as 'CS' or 'ICOND', and 1 when the combo is bought or -1
when it is sold. An unrecognised position has an empty code.
"""
terms = sorted({int(leg["dte"]) for leg in legs})
ordered = sorted(legs, key=lambda leg: (leg["strike"], leg["option_type"]))
kinds = {leg["option_type"][0] for leg in legs}
if len(legs) == 1:
leg = legs[0]
return ("C" if leg["option_type"][0] == "c" else "P"), _sign(leg["quantity"])
if len(terms) == 2 and len(legs) == 2 and len(kinds) == 1:
near, far = sorted(legs, key=lambda leg: leg["dte"])
kind = "C" if near["option_type"][0] == "c" else "P"
if near["quantity"] != -far["quantity"] or abs(far["quantity"]) != 1:
return "", 1
shape = "CAL" if near["strike"] == far["strike"] else "DIAG"
return f"{kind}{shape}", _sign(far["quantity"])
if len(terms) != 1:
return "", 1
if len(legs) == 2:
return _two_legs(*legs)
units = tuple(int(leg["quantity"]) for leg in ordered)
strikes = [leg["strike"] for leg in ordered]
if len(legs) == 3 and len(kinds) == 1 and len(set(strikes)) == 3:
kind = "C" if ordered[0]["option_type"][0] == "c" else "P"
even = strikes[1] - strikes[0] == strikes[2] - strikes[1]
if even and units in ((1, -2, 1), (-1, 2, -1)):
return f"{kind}BUT", units[0]
if len(legs) == 4 and len(kinds) == 1 and len(set(strikes)) == 4:
kind = "C" if ordered[0]["option_type"][0] == "c" else "P"
if units in ((1, -1, -1, 1), (-1, 1, 1, -1)):
return f"{kind}COND", units[0]
if len(legs) == 4 and len(kinds) == 2:
puts = sorted(
(leg for leg in legs if leg["option_type"][0] == "p"),
key=lambda leg: leg["strike"],
)
calls = sorted(
(leg for leg in legs if leg["option_type"][0] == "c"),
key=lambda leg: leg["strike"],
)
shape = (
[int(leg["quantity"]) for leg in puts],
[int(leg["quantity"]) for leg in calls],
)
if len(puts) == 2 and puts[1]["strike"] <= calls[0]["strike"]:
body = "IBUT" if puts[1]["strike"] == calls[0]["strike"] else "ICOND"
if shape == ([1, -1], [-1, 1]):
return body, 1
if shape == ([-1, 1], [1, -1]):
return body, -1
return "", 1
def _position(pairs: "list[tuple]", book: dict) -> dict:
"""Build one position from contracts and quantities, priced off a combo book."""
legs = [_leg(row, quantity) for row, quantity in pairs]
code, side = classify(legs)
listed = book.get(_key(legs))
return {
"strategy": NAMES.get((code, side), "Custom"),
"code": code or None,
"combo": listed[0] if listed else None,
"premium": listed[1] * legs[0]["size"] if listed else None,
"legs": legs,
}
def _both(pairs: "list[tuple]", book: dict) -> "list[dict]":
"""Return a position bought and the same position sold."""
flipped = [(row, -quantity) for row, quantity in pairs]
return [_position(pairs, book), _position(flipped, book)]
def combo_book(books: "list | tuple", spot: float, inverse: bool) -> dict:
"""Return each listed combo's price, keyed by the legs that make it up.
Parameters
----------
books : list or tuple
The listed combos, as ``load_combos`` returns them.
spot : float
The current price of the underlying, which a coin quote is carried at.
inverse : bool
Whether the combos are quoted in the coin.
Returns
-------
dict
The combo name and what one unit costs in the quote currency, for the
combo bought at its offer and for it sold at its bid.
"""
carry = spot if inverse else 1.0
lookup: dict = {}
for book in books:
bought = frozenset(book["legs"])
sold = frozenset((name, -amount) for name, amount in book["legs"])
if book["ask"] is not None:
lookup[bought] = (book["name"], float(book["ask"]) * carry)
if book["bid"] is not None:
lookup[sold] = (book["name"], -float(book["bid"]) * carry)
return lookup
def _spaced(rows: list) -> "list[tuple]":
"""Return every pair of strikes, lower first."""
return [(low, high) for index, low in enumerate(rows) for high in rows[index + 1 :]]
def _single_expiry(calls: list, puts: list, book: dict) -> "list[dict]":
"""Build every structure that trades one expiration."""
built: list[dict] = []
call_at = {float(row["strike"]): row for row in calls}
put_at = {float(row["strike"]): row for row in puts}
for row in [*calls, *puts]:
built += _both([(row, 1)], book)
for low, high in _spaced(calls):
built += _both([(low, 1), (high, -1)], book)
built += _both([(low, 1), (high, -2)], book)
for low, high in _spaced(puts):
built += _both([(high, 1), (low, -1)], book)
built += _both([(high, 1), (low, -2)], book)
for strike, call in call_at.items():
if strike in put_at:
built += _both([(call, 1), (put_at[strike], 1)], book)
built += _both([(call, 1), (put_at[strike], -1)], book)
for put in puts:
for call in calls:
if float(put["strike"]) < float(call["strike"]):
built += _both([(put, 1), (call, 1)], book)
built += _both([(put, 1), (call, -1)], book)
for rows, at in ((calls, call_at), (puts, put_at)):
for low, middle in _spaced(rows):
width = float(middle["strike"]) - float(low["strike"])
high = at.get(float(middle["strike"]) + width)
if high is not None:
built += _both([(low, 1), (middle, -2), (high, 1)], book)
for index, first in enumerate(rows):
for second in rows[index + 1 : index + 1 + WING_REACH]:
wing = float(second["strike"]) - float(first["strike"])
for third in rows:
fourth = at.get(float(third["strike"]) + wing)
if float(third["strike"]) > float(second["strike"]) and fourth:
built += _both(
[(first, 1), (second, -1), (third, -1), (fourth, 1)],
book,
)
for index, low in enumerate(puts):
for high in puts[index + 1 : index + 1 + WING_REACH]:
wing = float(high["strike"]) - float(low["strike"])
for call in calls:
upper = call_at.get(float(call["strike"]) + wing)
if float(call["strike"]) >= float(high["strike"]) and upper:
built += _both([(low, 1), (high, -1), (call, -1), (upper, 1)], book)
return built
def _two_expiries(near: list, far: list, book: dict) -> "list[dict]":
"""Build every calendar and diagonal between two expirations."""
built: list[dict] = []
for kind in ("call", "put"):
later = [row for row in far if row["option_type"] == kind]
strikes = [float(row["strike"]) for row in later]
for row in (row for row in near if row["option_type"] == kind):
position = min(
range(len(strikes)),
key=lambda index: abs(strikes[index] - float(row["strike"])),
default=None,
)
if position is None:
continue
reach = later[
max(position - DIAGONAL_REACH, 0) : position + DIAGONAL_REACH + 1
]
for other in reach:
built += _both([(row, -1), (other, 1)], book)
return built
def _window_rows(quotes: "DataFrame", spot: float) -> list:
"""Return the tradeable contracts near the money, as records."""
near = quotes[
(quotes["strike"] >= spot * (1 - STRIKE_WINDOW))
& (quotes["strike"] <= spot * (1 + STRIKE_WINDOW))
]
return near.sort_values("strike").to_dict("records")
def candidates(
frame: "DataFrame",
expiration: Any,
spot: float,
books: "list | tuple" = (),
inverse: bool = False,
) -> "list[dict]":
"""Build every combo structure an expiration can form, both bought and sold.
Parameters
----------
frame : DataFrame
The chain.
expiration : Any
The expiration the position is held to.
spot : float
The current price of the underlying.
books : list or tuple
The listed combos, as ``load_combos`` returns them.
inverse : bool
Whether the contracts settle in the underlying.
Returns
-------
list[dict]
Each position, named and coded, with its legs and any listed combo that
trades it as one instrument.
"""
from openbb_deribit.utils.options.chain import far_expiration, quotes_at
book = combo_book(books, spot, inverse)
near = _window_rows(quotes_at(frame, expiration), spot)
calls = [row for row in near if row["option_type"] == "call"]
puts = [row for row in near if row["option_type"] == "put"]
built = _single_expiry(calls, puts, book)
later = far_expiration(frame, expiration)
if later is not None:
built += _two_expiries(near, _window_rows(quotes_at(frame, later), spot), book)
tradeable = {
str(row["contract_symbol"]): row for row in quotes_at(frame).to_dict("records")
}
for listed in books:
rows = [
tradeable[name] for name, _amount in listed["legs"] if name in tradeable
]
if len(rows) != len(listed["legs"]) or (
min(row["expiration"] for row in rows) != expiration
):
continue
built += _both(
[
(row, amount)
for row, (_name, amount) in zip(rows, listed["legs"], strict=True)
],
book,
)
unique: dict = {}
for item in built:
unique.setdefault(_key(item["legs"]), item)
return list(unique.values())
def rebuild(
described: str,
quotes: "DataFrame",
books: "list | tuple" = (),
spot: float = 0.0,
inverse: bool = False,
) -> dict:
"""Return the position a description names, priced at the current quotes.
Parameters
----------
described : str
The legs, as ``_describe`` writes them: 'Buy SYMBOL / Sell 2 SYMBOL'.
quotes : DataFrame
The tradeable contracts of every expiration.
books : list or tuple
The listed combos, as ``load_combos`` returns them.
spot : float
The current price of the underlying.
inverse : bool
Whether the contracts settle in the underlying.
Returns
-------
dict
The position, named and coded, with its legs.
Raises
------
ValueError
If the description names no contract, or one not listed.
"""
pairs: list[tuple] = []
for part in described.split("/"):
words = part.split()
count = words[1] if len(words) == 3 else "1"
if (
len(words) not in (2, 3)
or words[0].lower() not in ("buy", "sell")
or not count.isdigit()
or int(count) < 1
):
raise ValueError(
f"{part.strip()!r} does not name a leg. A position reads"
" 'Buy SYMBOL / Sell SYMBOL', with a count before the symbol"
" when there is more than one, which is what the optimizer"
" prints in its Legs column."
)
symbol = words[-1]
listed = quotes[quotes["contract_symbol"] == symbol]
if listed.empty:
raise ValueError(
f"{symbol} is not a listed contract, so the position cannot be"
" priced. Pick a strategy from the optimizer."
)
quantity = int(count) if words[0].lower() == "buy" else -int(count)
pairs.append((listed.to_dict("records")[0], quantity))
return _position(pairs, combo_book(books, spot, inverse))
def _stress(legs: "list[dict]", spot: float, elapsed: int) -> "list[float]":
"""Return the prices a position's worst loss is sought over."""
low, high = spot / STRESS_MOVE, spot * STRESS_MOVE
points = {low, spot, high, *(leg["strike"] for leg in legs)}
if any(int(leg["dte"] or 0) > elapsed for leg in legs):
points.update(_grid(low, high, STRESS_STEPS))
return sorted(point for point in points if low <= point <= high)
def score(strategy: dict, spot: float, target: float, inverse: bool) -> "dict | None":
"""Size one position to one unit of capital at risk and value it at the target.
Parameters
----------
strategy : dict
The position, with its legs and any combo premium.
spot : float
The current price of the underlying.
target : float
The price the view expects.
inverse : bool
Whether the contracts settle in the underlying.
Returns
-------
dict or None
The position sized so its worst loss across a threefold move either way
is one unit, or None when it has nothing at risk.
"""
from openbb_deribit.utils.options.legs import net_cost, payoff
legs = strategy["legs"]
premium = strategy.get("premium")
unit_cost = net_cost(legs) if premium is None else premium
elapsed = max(min(int(leg["dte"] or 0) for leg in legs), 0)
points = _stress(legs, spot, elapsed)
losses = payoff(legs, points, elapsed, inverse, spot, unit_cost)
if inverse:
losses = [
loss / price * spot for loss, price in zip(losses, points, strict=True)
]
risk = -min(losses)
if risk <= 0:
return None
lots = 1.0 / risk
sized = [{**leg, "quantity": leg["quantity"] * lots} for leg in legs]
expected = payoff(sized, [target], elapsed, inverse, spot, unit_cost * lots)[0]
return {
**strategy,
"sized": sized,
"lots": lots,
"elapsed": elapsed,
"cost": unit_cost * lots,
"expected": expected,
}
def _grid(low: float, high: float, steps: int) -> "list[float]":
"""Return an evenly spaced range of prices."""
return [low + (high - low) * step / steps for step in range(steps + 1)]
def _moving(prices: "list[float]", curve: "list[float]") -> tuple:
"""Return the price range over which a payoff is still changing."""
slopes = [
abs((curve[step] - curve[step - 1]) / (prices[step] - prices[step - 1]))
for step in range(1, len(prices))
]
steepest = max(slopes)
moving = [
step for step, slope in enumerate(slopes) if slope > steepest * FLAT_SLOPE
]
if not moving:
return prices[0], prices[-1]
return prices[moving[0]], prices[moving[-1] + 1]
def _window(
legs: "list[dict]",
spot: float,
target: float,
elapsed: int,
inverse: bool,
cost: "float | None" = None,
) -> tuple:
"""Return the price range worth drawing for one position.
Parameters
----------
legs : list[dict]
The sized position.
spot : float
The current price of the underlying.
target : float
The price the view expects.
elapsed : int
Days from now to the valuation date.
inverse : bool
Whether the contracts settle in the underlying.
cost : float or None
What the position was opened for, when traded as one combo.
Returns
-------
tuple
The lowest and highest price to draw.
"""
from openbb_deribit.utils.options.legs import breakevens, payoff, position_value
scout = _grid(spot * (1 - PROBE_SPREAD), spot * (1 + PROBE_SPREAD), SCOUT_STEPS)
curve = payoff(legs, scout, elapsed, inverse, spot, cost)
low, high = _moving(
scout, [position_value(legs, price, elapsed) for price in scout]
)
anchors = [target, *breakevens(scout, curve)]
if low <= scout[0]:
low = min([anchor for anchor in anchors if anchor < high] or [spot])
if high >= scout[-1]:
high = max([anchor for anchor in anchors if anchor > low] or [spot])
reach = max(high - low, spot * MIN_PAD) * ANCHOR_REACH
nearby = [anchor for anchor in anchors if low - reach <= anchor <= high + reach]
low, high = min([low, *nearby]), max([high, *nearby])
pad = max((high - low) * WINDOW_PAD, spot * MIN_PAD)
return max(low - pad, 0.0), high + pad
def draw(
scored: dict, spot: float, target: float, budget: float, inverse: bool
) -> dict:
"""Value one scored position across the price range worth drawing."""
from openbb_deribit.utils.options.legs import breakevens, payoff
legs = [{**leg, "quantity": leg["quantity"] * budget} for leg in scored["sized"]]
cost = scored["cost"] * budget
elapsed = scored["elapsed"]
low, high = _window(legs, spot, target, elapsed, inverse, cost)
probe = _grid(low, high, PROBE_STEPS)
curve = payoff(legs, probe, elapsed, inverse, spot, cost)
return {
"strategy": scored["strategy"],
"code": scored["code"],
"combo": scored["combo"],
"legs": _describe(scored["legs"]),
"contracts": scored["lots"] * budget,
"cost": cost,
"expected_profit": scored["expected"] * budget,
"expected_return": scored["expected"],
"max_profit": max(curve),
"max_loss": min(curve),
"breakevens": breakevens(probe, curve),
"position": legs,
"elapsed": elapsed,
"prices": probe,
"payoff": curve,
}
def rank(
frame: "DataFrame",
expiration: Any,
spot: float,
target: float,
budget: float,
inverse: bool,
limit: int = SHORTLIST,
books: "list | tuple" = (),
) -> "list[dict]":
"""Return the best position of each structure, most profitable first.
Parameters
----------
frame : DataFrame
The chain.
expiration : Any
The expiration the position is held to.
spot : float
The current price of the underlying.
target : float
The price the view expects.
budget : float
The capital to put at risk, in the quote currency.
inverse : bool
Whether the contracts settle in the underlying.
limit : int
How many structures to return.
books : list or tuple
The listed combos, as ``load_combos`` returns them.
Returns
-------
list[dict]
The best-returning position of each structure, sized to the budget,
with its payoff.
"""
scored = [
result
for strategy in candidates(frame, expiration, spot, books, inverse)
if (result := score(strategy, spot, target, inverse))
]
scored.sort(key=lambda item: item["expected"], reverse=True)
best: dict = {}
for item in scored:
best.setdefault(item["strategy"], item)
return [
draw(item, spot, target, budget, inverse)
for item in list(best.values())[:limit]
]

View File

@@ -0,0 +1,162 @@
"""Pricing the standard option structures at every expiration.
Prices are quoted in the chain's quote currency per unit of the underlying, so
a cost is comparable across expirations of the same underlying but not across
underlyings. The cost as a share of spot is, which is why it is reported too.
"""
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pandas import DataFrame
def _closest(side: "DataFrame", target: float) -> "Any | None":
"""Return the contract whose strike is nearest a price."""
if side.empty:
return None
return side.iloc[(side["strike"] - target).abs().argmin()]
def _sides(quotes: "DataFrame") -> tuple:
"""Split one expiration into its calls and its puts."""
return (
quotes[quotes["option_type"] == "call"],
quotes[quotes["option_type"] == "put"],
)
def straddles(frame: "DataFrame", spot: float) -> "list[dict]":
"""Price a long straddle at the money, at every expiration."""
from openbb_deribit.utils.options.chain import expirations, quotes_at
priced: list[dict] = []
for expiration in expirations(frame):
quotes = quotes_at(frame, expiration)
calls, puts = _sides(quotes)
call = _closest(calls, spot)
put = _closest(puts, spot)
if call is None or put is None:
continue
if float(call["strike"]) != float(put["strike"]):
continue
strike = float(call["strike"])
cost = float(call["entry"]) + float(put["entry"])
priced.append(
{
"expiration": expiration,
"dte": int(call["dte"] or 0),
"underlying_price": spot,
"strike": strike,
"call_premium": float(call["entry"]),
"put_premium": float(put["entry"]),
"cost": cost,
"cost_percent": cost / spot if spot else None,
"breakeven_lower": strike - cost,
"breakeven_upper": strike + cost,
"max_loss": -cost,
}
)
return priced
def strangles(frame: "DataFrame", spot: float, moneyness: float) -> "list[dict]":
"""Price a long strangle the given distance out of the money."""
from openbb_deribit.utils.options.chain import expirations, quotes_at
priced: list[dict] = []
for expiration in expirations(frame):
quotes = quotes_at(frame, expiration)
calls, puts = _sides(quotes)
call = _closest(calls, spot * (1 + moneyness))
put = _closest(puts, spot * (1 - moneyness))
if call is None or put is None:
continue
upper = float(call["strike"])
lower = float(put["strike"])
if upper <= lower:
continue
cost = float(call["entry"]) + float(put["entry"])
priced.append(
{
"expiration": expiration,
"dte": int(call["dte"] or 0),
"underlying_price": spot,
"call_strike": upper,
"put_strike": lower,
"call_premium": float(call["entry"]),
"put_premium": float(put["entry"]),
"cost": cost,
"cost_percent": cost / spot if spot else None,
"breakeven_lower": lower - cost,
"breakeven_upper": upper + cost,
"max_loss": -cost,
}
)
return priced
def verticals(frame: "DataFrame", spot: float, moneyness: float) -> "list[dict]":
"""Price a bull call and a bear put spread of the given width."""
from openbb_deribit.utils.options.chain import expirations, quotes_at
priced: list[dict] = []
for expiration in expirations(frame):
quotes = quotes_at(frame, expiration)
calls, puts = _sides(quotes)
for name, side, long_target, short_target in (
("Bull Call Spread", calls, spot, spot * (1 + moneyness)),
("Bear Put Spread", puts, spot, spot * (1 - moneyness)),
):
bought = _closest(side, long_target)
sold = _closest(side, short_target)
if bought is None or sold is None:
continue
width = abs(float(sold["strike"]) - float(bought["strike"]))
cost = float(bought["entry"]) - float(sold["exit"])
if width <= 0 or cost <= 0:
continue
breakeven = (
float(bought["strike"]) + cost
if name.startswith("Bull")
else float(bought["strike"]) - cost
)
priced.append(
{
"strategy": name,
"expiration": expiration,
"dte": int(bought["dte"] or 0),
"underlying_price": spot,
"bought_strike": float(bought["strike"]),
"sold_strike": float(sold["strike"]),
"bought_premium": float(bought["entry"]),
"sold_premium": float(sold["exit"]),
"cost": cost,
"cost_percent": cost / spot if spot else None,
"breakeven": breakeven,
"max_profit": width - cost,
"max_loss": -cost,
}
)
return priced

View File

@@ -0,0 +1,119 @@
"""Shared Plotly theming for the Deribit options charts."""
from typing import Any
CHART_CONFIG = {"scrollZoom": True, "displayModeBar": False, "responsive": True}
class Unplotted:
"""Accept every drawing call and draw nothing."""
def __getattr__(self, name: str):
"""Return a call that does nothing and keeps the chain going."""
def drawn(*args, **kwargs):
return self
return drawn
def supported_layout(layout: dict) -> dict:
"""Drop the layout keys the installed Plotly no longer accepts.
Parameters
----------
layout : dict
The template layout to filter.
Returns
-------
dict
Only the keys the installed Plotly's ``Layout`` accepts.
"""
from plotly.graph_objects import Layout
valid = getattr(Layout, "_valid_props", None)
if not valid:
return layout
return {key: value for key, value in layout.items() if key in valid}
def new_figure(theme: str) -> "tuple[Any, str, str]":
"""Create a themed figure with its text and background colors.
Parameters
----------
theme : str
Either 'dark' or 'light'.
Returns
-------
tuple[Any, str, str]
The figure, the text color, and the transparent background color. The
figure draws nothing when the charting extension is absent.
"""
from openbb_deribit import CHARTING_INSTALLED
text_color = "white" if theme == "dark" else "black"
background = "rgba(0,0,0,0)" if theme == "dark" else "rgba(255,255,255,0)"
if not CHARTING_INSTALLED:
return Unplotted(), text_color, background
from openbb_charting.core.chart_style import ChartStyle
from openbb_charting.core.openbb_figure import OpenBBFigure
figure = OpenBBFigure(create_backend=True)
figure.update_layout(
supported_layout(ChartStyle().plotly_template.get("layout", {}))
)
return figure, text_color, background
def finalize(output: Any, figure: Any, theme: str) -> Any:
"""Apply the chart style and attach the chart to the result.
Parameters
----------
output : Any
The OBBject carrying the tabular results.
figure : Any
The figure to attach.
theme : str
Either 'dark' or 'light'.
Returns
-------
Any
The same result, with a chart attached, or unchanged when there is no
charting extension to draw with.
"""
from openbb_core.app.model.charts.chart import Chart
from openbb_deribit import CHARTING_INSTALLED
if not CHARTING_INSTALLED:
return output
text_color = "white" if theme == "dark" else "black"
output.charting._charting_settings.chart_style = theme
figure = output.charting._set_chart_style(figure)
figure.update_layout(
hoverlabel=dict(
bgcolor="#0E0E0E" if theme == "dark" else "#FFFFFF",
bordercolor=text_color,
font=dict(color=text_color, size=12),
)
)
figure = figure.show(config=CHART_CONFIG, external=True)
for trace in figure.data:
if getattr(trace, "hoverinfo", None) == "skip":
trace.hovertemplate = None
output.chart = Chart(fig=figure, content=figure.to_plotly_json(), format="plotly")
return output

View File

@@ -0,0 +1,116 @@
"""Pooled HTTP sessions for the Deribit API.
One session is held per event loop so connections are kept alive across the
many parallel calls a chain or curve needs. The session is closed when its loop
shuts down, so nothing is left open once the work that opened it is done.
"""
from contextlib import suppress
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp import ClientSession
from aiohttp_client_cache.session import CachedSession
_SESSIONS: dict = {}
_CACHED_SESSIONS: dict = {}
def _hook_loop_close(loop) -> None:
"""Close this loop's sessions when the loop itself is closed.
The Python interface runs every command in its own short-lived loop, so the
pool is emptied there without waiting for an API shutdown that never comes.
Loops that do not accept the hook, such as uvloop's, are left to the
shutdown handler.
"""
if getattr(loop, "_deribit_close_hooked", False):
return
original = loop.close
def close() -> None:
try:
if not loop.is_closed() and not loop.is_running():
loop.run_until_complete(close_sessions(loop))
except Exception: # noqa: BLE001, S110
pass
original()
try:
loop.close = close
loop._deribit_close_hooked = True
except (AttributeError, TypeError):
pass
async def get_session() -> "ClientSession":
"""Return the uncached session for the running loop.
Returns
-------
ClientSession
A session shared by every caller on this loop.
"""
import asyncio
from aiohttp import ClientSession
loop = asyncio.get_running_loop()
session = _SESSIONS.get(loop)
if session is None or session.closed:
session = ClientSession()
_SESSIONS[loop] = session
_hook_loop_close(loop)
return session
async def get_cached_session() -> "CachedSession":
"""Return the cache-backed session for the running loop.
Returns
-------
CachedSession
A cache-backed session shared by every caller on this loop.
"""
import asyncio
from aiohttp_client_cache.session import CachedSession
from openbb_deribit.utils.cache import get_cache_backend, sweep_cache
loop = asyncio.get_running_loop()
session = _CACHED_SESSIONS.get(loop)
if session is None or session.closed:
backend = get_cache_backend()
session = CachedSession(cache=backend)
_CACHED_SESSIONS[loop] = session
_hook_loop_close(loop)
with suppress(Exception):
await sweep_cache(backend)
return session
async def close_sessions(loop=None) -> None:
"""Close pooled sessions and forget the loops that owned them.
Parameters
----------
loop : AbstractEventLoop or None
Close only this loop's sessions, or every pooled session when None.
"""
for pool in (_SESSIONS, _CACHED_SESSIONS):
keys = [loop] if loop is not None else list(pool)
for key in keys:
session = pool.pop(key, None)
if session is not None and not getattr(session, "closed", True):
with suppress(Exception):
await session.close()

View File

@@ -0,0 +1,85 @@
"""Ticker subscriptions over the Deribit websocket."""
WS_URL = "wss://www.deribit.com/ws/api/v2"
SUBSCRIBE_TIMEOUT = 15.0
async def _collect(websocket, wanted: set, received: dict, messages: set) -> None:
"""Read tickers off one subscription until every symbol has arrived."""
import json
from websockets import ConnectionClosed
while True:
try:
frame = await websocket.recv()
except ConnectionClosed:
return
payload = json.loads(frame)
if payload.get("error"):
messages.add(f"Deribit returned an error -> {payload['error']}")
return
ticker = (payload.get("params") or {}).get("data") or {}
symbol = ticker.get("instrument_name")
if not symbol or symbol in received:
continue
received[symbol] = ticker
if wanted <= set(received):
return
async def subscribe_tickers(symbols: list[str], messages: set) -> dict[str, dict]:
"""Return the ticker of every symbol on one subscription.
Parameters
----------
symbols : list[str]
The instrument names to subscribe to.
messages : set
Collects what went wrong, for the caller to warn about.
Returns
-------
dict[str, dict]
The ticker of each symbol that published one.
"""
import asyncio
import json
from websockets.asyncio.client import connect
received: dict[str, dict] = {}
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "public/subscribe",
"params": {"channels": [f"ticker.{symbol}.100ms" for symbol in symbols]},
}
try:
async with connect(WS_URL) as websocket:
await websocket.send(json.dumps(request))
try:
await asyncio.wait_for(
_collect(websocket, set(symbols), received, messages),
timeout=SUBSCRIBE_TIMEOUT,
)
except asyncio.TimeoutError:
missing = len(symbols) - len(received)
messages.add(
f"Deribit did not publish {missing} of {len(symbols)} contracts"
" before the subscription timed out."
)
except Exception as error: # noqa: BLE001
messages.add(
f"The Deribit subscription failed -> {error.__class__.__name__}: {error}"
)
return received

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,96 @@
[tool.poetry]
[project]
name = "openbb-deribit"
version = "1.2.1"
description = "Deribit is a crypto-native derivatives exchange."
authors = ["OpenBB Team <hello@openbb.co>"]
license = "AGPL-3.0-only"
version = "2.0.0"
description = "Deribit provider extension for OpenBB Platform."
authors = [{ name = "OpenBB Team", email = "hello@openbb.co" }]
license = { text = "AGPL-3.0-only" }
readme = "README.md"
packages = [{ include = "openbb_deribit" }]
requires-python = ">=3.10,<4"
dependencies = [
"openbb-core[pandas]>=2.0.0",
"aiohttp-client-cache>=0.11",
"aiosqlite>=0.20",
]
[tool.poetry.dependencies]
python = ">=3.10,<4"
openbb-core = "^1.6.10"
async-lru = "^2.0.4"
[project.entry-points."openbb_provider_extension"]
deribit = "openbb_deribit:deribit_provider"
[project.entry-points."openbb_core_extension"]
deribit = "openbb_deribit.deribit_router:router"
[project.urls]
Homepage = "https://openbb.co"
Repository = "https://github.com/OpenBB-finance/OpenBB"
[dependency-groups]
dev = [
"openbb-devtools>=2.0.0",
"openbb-charting>=3.0.0",
"openbb-platform-api>=2.0.0",
]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling>=1.25"]
build-backend = "hatchling.build"
[tool.poetry.plugins."openbb_provider_extension"]
deribit = "openbb_deribit:deribit_provider"
[tool.hatch.build.targets.wheel]
packages = ["openbb_deribit"]
[tool.uv.sources]
openbb-core = { path = "../../core", editable = true }
openbb-devtools = { path = "../../extensions/devtools", editable = true }
openbb-charting = { path = "../../obbject_extensions/charting", editable = true }
openbb-platform-api = { path = "../../extensions/platform_api", editable = true }
[tool.ruff]
line-length = 88
target-version = "py310"
fix = true
[tool.ruff.lint]
select = ["D", "E", "W", "F", "S", "UP", "I", "PLC", "PLE", "PLR", "PLW", "SIM", "T20"]
ignore = [
"PLR2004", "PLR0913", "PLR0915", "PLC0415",
"E402", "E501",
"D202", "D203", "D205", "D206", "D210", "D212", "D213", "D214",
"D215", "D300", "D400", "D404", "D405", "D406", "D407", "D408",
"D409", "D410", "D411", "D413", "D415", "D416", "D417",
]
[tool.ruff.lint.per-file-ignores]
"openbb_deribit/routers/*.py" = ["ARG001", "PLC0415", "PLR0913", "PLR0917"]
"openbb_deribit/models/*.py" = ["ARG002", "PLC0415", "PLR0912"]
"openbb_deribit/utils/options/*.py" = ["PLC0415", "PLR0911", "PLR0912", "PLR0913", "PLR0914", "PLR0915", "PLR0917", "SLF001"]
"tests/**" = ["S101", "S108", "D", "SIM105", "SIM117"]
"integration/**" = ["S101", "S108", "D", "SIM105", "SIM117", "PLC0415", "ARG001"]
"**/__init__.py" = ["F401"]
[tool.ruff.lint.isort]
combine-as-imports = true
force-wrap-aliases = true
[tool.ruff.lint.pydocstyle]
convention = "numpy"
[tool.ty]
[tool.ty.environment]
python-version = "3.10"
[tool.ty.src]
include = ["openbb_deribit"]
exclude = ["tests", "integration"]
[tool.ty.rules]
unresolved-import = "warn"
[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "@overload"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q --strict-markers"
markers = [
"record_http: VCR-backed HTTP cassette tests",
"integration: end-to-end tests that hit live services (excluded from the coverage gate)",
"uses_session: builds real pooled sessions instead of patching them",
]

View File

@@ -0,0 +1,160 @@
"""Shared fixtures for the openbb_deribit test suite."""
import json
from pathlib import Path
import pytest
DATA_DIR = Path(__file__).parent / "data"
def payload(name: str):
"""Return one captured Deribit payload."""
return json.loads((DATA_DIR / f"{name}.json").read_text(encoding="utf-8"))
@pytest.fixture
def load():
"""Return the loader for a captured Deribit payload."""
return payload
@pytest.fixture(autouse=True)
def _no_network(request, monkeypatch):
"""Fail loudly rather than reaching the exchange from a unit test.
A test marked ``uses_session`` builds its own sessions and is left alone;
a session is only a connection pool until something asks it for a URL.
"""
if request.node.get_closest_marker("uses_session"):
return
async def _forbidden(*args, **kwargs):
raise AssertionError(
"A unit test opened a session. Patch the client or the session."
)
monkeypatch.setattr("openbb_deribit.utils.session.get_session", _forbidden)
monkeypatch.setattr("openbb_deribit.utils.session.get_cached_session", _forbidden)
@pytest.fixture
def responder(monkeypatch):
"""Answer client calls from a mapping of method name to payload."""
def install(mapping: dict):
async def _request(method, params=None, use_cache=True):
if method not in mapping:
raise AssertionError(f"No fixture registered for public/{method}.")
answer = mapping[method]
return answer(params) if callable(answer) else answer
async def _gather(calls, use_cache=True):
return [await _request(method, params) for method, params in calls]
monkeypatch.setattr("openbb_deribit.utils.client.request", _request)
monkeypatch.setattr("openbb_deribit.utils.client.gather", _gather)
return mapping
return install
def _chain_rows(inverse: bool, spot: float, size: float):
"""Build a small, well-formed chain around a spot price."""
from datetime import date, timedelta
today = date.today()
rows = []
for days in (1, 30):
expiration = today + timedelta(days=days)
for step in range(-4, 5):
strike = round(spot * (1 + step * 0.02), 4)
for option_type in ("call", "put"):
moneyness = (
max(0.0, spot - strike)
if option_type == "call"
else max(0.0, strike - spot)
)
extrinsic = spot * 0.01 * (days / 30) ** 0.5
mark = moneyness + extrinsic
rows.append(
{
"contract_symbol": (
f"TEST-{days}-{strike:g}-{option_type[0].upper()}"
),
"expiration": expiration,
"dte": days,
"strike": float(strike),
"option_type": option_type,
"contract_size": size,
"tick_size": 0.0001,
"is_inverse": inverse,
"mark": mark,
"bid": mark * 0.98,
"ask": mark * 1.02,
"last": mark,
"implied_volatility": 50.0 + abs(step) * 2,
"open_interest": 10.0 + step,
"volume": 5.0,
"underlying_price": spot,
"underlying_spot_price": spot,
"delta": 0.5,
"gamma": 0.001,
"theta": -1.0,
"vega": 10.0,
"rho": 1.0,
}
)
return rows
@pytest.fixture
def chain_frame():
"""Return a builder for a synthetic option chain."""
from pandas import DataFrame
def build(inverse: bool = True, spot: float = 100.0, size: float = 1.0):
return DataFrame(_chain_rows(inverse, spot, size))
return build
@pytest.fixture
def options_chain(chain_frame):
"""Return a synthetic chain as the model the options charts draw from."""
from openbb_deribit.utils.options.data_handler import to_chain
return to_chain(chain_frame(), "BTC")
@pytest.fixture
def loaded_chain(monkeypatch, chain_frame):
"""Serve a synthetic chain wherever a chain would be loaded."""
def install(
inverse: bool = True,
spot: float = 100.0,
size: float = 1.0,
books: list | None = None,
):
frame = chain_frame(inverse, spot, size)
async def _load(symbol, use_cache=True):
return frame.copy()
async def _combos(symbol, use_cache=True):
return list(books or [])
monkeypatch.setattr("openbb_deribit.utils.options.chain.load_chain", _load)
monkeypatch.setattr("openbb_deribit.utils.options.chain.load_combos", _combos)
return frame
return install

View File

@@ -0,0 +1,26 @@
[
{
"id": 1789741775268,
"title": "\ud83d\udea8 Coin50 Index, Pre-IPO OpenAI, & EURC Perpetuals. Coming to Deribit",
"body": "<p style=\"font-family: Arial; font-size: 16px;\">On Monday September 21, 2026 at 10:00 AM UTC, Deribit will launch the following USDC-settled perpetual markets:<br><br>\u2022<span style=\"font-family: Arial; font-size: 16px;\">&nbsp;<span><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">COIN50_USDC</b></span>, offering exposure to the&nbsp;<a href=\"https://www.coinbase.com/en-nl/coin50\" style=\"font-family: Arial; font-size: 16px;\"></a><a href=\"https://www.coinbase.com/coin50\" style=\"font-family: Arial; font-size: 16px;\">Coin50 Index</a>&nbsp;through a perpetual contract<br>\u2022&nbsp;<span style=\"font-family: Arial; font-size: 16px;\"><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">OPENAI_USDC</b>,</span>&nbsp;a pre-IPO perpetual linked to OpenAI<br>\u2022&nbsp;<span><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">EURC_USDC</b></span></span>, The euro-backed stablecoin&nbsp;issued by Circle</p><p style=\"font-family: Arial; font-size: 16px;\"><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Contract details:</b><br>\ud83d\udd39&nbsp;<span style=\"font-family: Arial; font-size: 16px;\">Settlement</span>: USDC<br>\ud83d\udd39 Full specifications:<span>&nbsp;</span><a href=\"https://support.deribit.com/hc/en-us/articles/38325634622493-RWA-Perpetual\" style=\"font-family: Arial; font-size: 16px;\">Deribit RWA Perpetual contract details</a><br><br>Read more<span>&nbsp;</span><a href=\"https://insights.deribit.com/exchange-updates/deribit-launches-its-biggest-ever-product-expansion/\"><b><u style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">here</u></b></a>.<br><br><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Deribit. Always Open.</b><br><br><i style=\"font-family: Arial; font-size: 16px;\">Disclaimer: Deribit FZE is authorised and regulated by the Virtual Assets Regulatory Authority (VARA) in Dubai. Availability of products and services is subject to applicable regulatory requirements and investor classification. Virtual Assets are subject to extreme market volatility, involve a high degree of risk, and can lose value</i></p><div></div>",
"important": true,
"confirmation": false,
"publication_timestamp": 1789739944023
},
{
"id": 1789656759479,
"title": "\ud83d\udd14 Update: Spot Routing Completed",
"body": "All announced spot routing markets are now live and routed through Coinbase Exchange liquidity on Deribit:<br><br>\u2022 BTC-USDC<br>\u2022 ETH-USDC<br>\u2022 XRP-USDC<br>\u2022 BNB-USDC<br>\u2022 SOL-USDC<br>\u2022 PAXG-USDC<br><br>Benefit from deeper liquidity and improved execution quality. Routed pairs will follow Deribit\u2019s applicable<a href=\"https://support.deribit.com/hc/en-us/articles/25944746248989-Fees\" style=\"font-family: Arial; font-size: 16px;\"> Fee Schedule</a> and CBE market specifications. For pair-level specifications, see the <a href=\"https://support.deribit.com/hc/en-us/articles/31424969480093-Spot-Instruments\" style=\"font-family: Arial; font-size: 16px;\">Spot Instruments</a> page.<br><br><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Deribit. Always Open.</b><div></div>",
"important": true,
"confirmation": false,
"publication_timestamp": 1789651389316
},
{
"id": 1789659935624,
"title": "\ud83d\udea8 More new perpetual futures are coming to Deribit",
"body": "We are pleased to announce that on September 18, 2026, at 10:00 AM UTC, Deribit is launching a new lineup of USDC-settled perpetuals.<br><br>MORPHO \u00b7 AERO \u00b7 BASED1 \u00b7 S \u00b7 EIGEN \u00b7 JTO \u00b7 NIGHT \u00b7 AVNT \u00b7 STRK \u00b7 COMP \u00b7 ALT \u00b7 DYDX \u00b7 OPN \u00b7 TOWNS \u00b7 PYTH \u00b7 RUNE \u00b7 RESOLV \u00b7 ATOM<br><br><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Contract details:</b><br>\ud83d\udd39<b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Settlement</b>: USDC<br>\ud83d\udd39<a href=\"https://support.deribit.com/hc/en-us/articles/31424969384605-Linear-Perpetual\"><b><u style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Click here</u></b></a> for all contract details and specifications.<br><br>See <a href=\"https://insights.deribit.com/exchange-updates/deribit-launches-its-biggest-ever-product-expansion/\"><b><u style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">here</u></b></a> for more information.<br><br>Please note that these newly listed perpetual contracts are subject <a href=\"https://insights.deribit.com/exchange-updates/deribit-launches-its-biggest-ever-product-expansion/\" style=\"font-family: Arial; font-size: 16px;\">t</a>o our Auto-Deleveraging Mechanism (ADL) and are covered by the Liquidity Support Program (LSP). By trading any of the products listed above, you acknowledge and agree that the terms of ADL and LSP will apply.<br><br>For more information on how ADL works, when it may be triggered, and which Instruments are in scope, please refer to the<a href=\"https://support.deribit.com/hc/en-us/articles/38374258465437-Auto-Deleveraging-ADL\"><u style=\"font-family: Arial; font-size: 16px;\"> ADL article</u></a> available in our Knowledge Base. For more information about the LSP, please refer to the<a href=\"https://support.deribit.com/hc/en-us/articles/38376103377949-Liquidity-Support-Program-LSP\"><u style=\"font-family: Arial; font-size: 16px;\"> LSP article</u></a> available in our Knowledge Base.<br><br><b style=\"font-family: Arial; font-size: 16px; font-weight: bold;\">Deribit. Always Open.</b><br><div></div>",
"important": true,
"confirmation": false,
"publication_timestamp": 1789651341429
}
]

View File

@@ -0,0 +1,25 @@
{
"data": [
{
"day": 20715,
"apr": 4.75
},
{
"day": 20714,
"apr": 4.75
},
{
"day": 20713,
"apr": 4.75
},
{
"day": 20712,
"apr": 4.75
},
{
"day": 20711,
"apr": 4.75
}
],
"continuation": 20711
}

View File

@@ -0,0 +1,343 @@
{
"continuation": "1789886834420:55537",
"block_rfqs": [
{
"id": 55558,
"timestamp": 1789918919275,
"amount": 84.0,
"direction": "buy",
"mark_price": -0.001760294,
"trades": [
{
"price": -0.0016,
"amount": 84.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0026,
"direction": "buy",
"instrument_name": "ETH-21SEP26-2580-P",
"ratio": 1
},
{
"price": 0.0052,
"direction": "buy",
"instrument_name": "ETH-21SEP26-2600-P",
"ratio": 1
},
{
"price": 0.0094,
"direction": "sell",
"instrument_name": "ETH-21SEP26-2620-P",
"ratio": 1
}
],
"combo_id": null,
"index_prices": {
"eth_usd": 2602.81
}
},
{
"id": 55557,
"timestamp": 1789918897736,
"amount": 84.0,
"direction": "buy",
"mark_price": -0.001321293,
"trades": [
{
"price": -0.001,
"amount": 84.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0059,
"direction": "sell",
"instrument_name": "ETH-21SEP26-2600-C",
"ratio": 1
},
{
"price": 0.0032,
"direction": "buy",
"instrument_name": "ETH-21SEP26-2620-C",
"ratio": 1
},
{
"price": 0.0017,
"direction": "buy",
"instrument_name": "ETH-21SEP26-2640-C",
"ratio": 1
}
],
"combo_id": null,
"index_prices": {
"eth_usd": 2602.51
}
},
{
"id": 55555,
"timestamp": 1789908858693,
"amount": 60.0,
"direction": "buy",
"mark_price": 0.005304094,
"trades": [
{
"price": 0.0057,
"amount": 60.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0091,
"direction": "buy",
"instrument_name": "BTC-9OCT26-86000-C",
"ratio": 1
},
{
"price": 0.0034,
"direction": "sell",
"instrument_name": "BTC-9OCT26-90000-C",
"ratio": 1
}
],
"combo_id": "BTC-CS-9OCT26-86000_90000",
"index_prices": {
"btc_usd": 80415.41
}
},
{
"id": 55549,
"timestamp": 1789896450568,
"amount": 50.0,
"direction": "buy",
"mark_price": 0.005195826,
"trades": [
{
"price": 0.0057,
"amount": 50.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0093,
"direction": "buy",
"instrument_name": "BTC-9OCT26-86000-C",
"ratio": 1
},
{
"price": 0.0036,
"direction": "sell",
"instrument_name": "BTC-9OCT26-90000-C",
"ratio": 1
}
],
"combo_id": "BTC-CS-9OCT26-86000_90000",
"index_prices": {
"btc_usd": 80371.04
}
},
{
"id": 55548,
"timestamp": 1789896384838,
"amount": 100.0,
"direction": "buy",
"mark_price": 0.005186311,
"trades": [
{
"price": 0.0056,
"amount": 100.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.009,
"direction": "buy",
"instrument_name": "BTC-9OCT26-86000-C",
"ratio": 1
},
{
"price": 0.0034,
"direction": "sell",
"instrument_name": "BTC-9OCT26-90000-C",
"ratio": 1
}
],
"combo_id": "BTC-CS-9OCT26-86000_90000",
"index_prices": {
"btc_usd": 80356.16
}
},
{
"id": 55547,
"timestamp": 1789896337667,
"amount": 100.0,
"direction": "buy",
"mark_price": 0.005164996,
"trades": [
{
"price": 0.0056,
"amount": 100.0,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0091,
"direction": "buy",
"instrument_name": "BTC-9OCT26-86000-C",
"ratio": 1
},
{
"price": 0.0035,
"direction": "sell",
"instrument_name": "BTC-9OCT26-90000-C",
"ratio": 1
}
],
"combo_id": "BTC-CS-9OCT26-86000_90000",
"index_prices": {
"btc_usd": 80340.78
}
},
{
"id": 55540,
"timestamp": 1789891560285,
"amount": 25.0,
"direction": "sell",
"mark_price": 0.030700065,
"trades": [
{
"price": 0.0299,
"amount": 25.0,
"direction": "sell"
}
],
"legs": [
{
"price": 0.0175,
"direction": "buy",
"instrument_name": "BTC-25SEP26-80000-C",
"ratio": 1
},
{
"price": 0.0124,
"direction": "buy",
"instrument_name": "BTC-25SEP26-80000-P",
"ratio": 1
}
],
"combo_id": "BTC-STRD-25SEP26-80000",
"index_prices": {
"btc_usd": 80399.5
}
},
{
"id": 55539,
"timestamp": 1789890623307,
"amount": 12.5,
"direction": "buy",
"mark_price": -0.056176534,
"trades": [
{
"price": -0.0549,
"amount": 12.5,
"direction": "buy"
}
],
"legs": [
{
"price": 0.0016,
"direction": "buy",
"instrument_name": "BTC-21SEP26-79500-P",
"ratio": 2
},
{
"price": 0.0053,
"direction": "sell",
"instrument_name": "BTC-25SEP26-83000-C",
"ratio": 2
},
{
"price": 0.0475,
"direction": "sell",
"instrument_name": "BTC-25SEP26-84000-P",
"ratio": 1
}
],
"combo_id": null,
"index_prices": {
"btc_usd": 80312.58
}
},
{
"id": 55536,
"timestamp": 1789886838687,
"amount": 20.0,
"direction": "sell",
"mark_price": 0.01431241,
"trades": [
{
"price": 0.0136,
"amount": 20.0,
"direction": "sell"
}
],
"legs": [
{
"price": 0.3167,
"direction": "sell",
"instrument_name": "BTC-25SEP26-55000-C",
"ratio": 1
},
{
"price": 0.3303,
"direction": "buy",
"instrument_name": "BTC-25DEC26-55000-C",
"ratio": 1
}
],
"combo_id": "BTC-CCAL-25DEC26_25SEP26-55000",
"index_prices": {
"btc_usd": 80393.37
}
},
{
"id": 55537,
"timestamp": 1789886834420,
"amount": 30.0,
"direction": "sell",
"mark_price": 0.014293477,
"trades": [
{
"price": 0.0136,
"amount": 30.0,
"direction": "sell"
}
],
"legs": [
{
"price": 0.3168,
"direction": "sell",
"instrument_name": "BTC-25SEP26-55000-C",
"ratio": 1
},
{
"price": 0.3304,
"direction": "buy",
"instrument_name": "BTC-25DEC26-55000-C",
"ratio": 1
}
],
"combo_id": "BTC-CCAL-25DEC26_25SEP26-55000",
"index_prices": {
"btc_usd": 80395.44
}
}
]
}

View File

@@ -0,0 +1,251 @@
[
{
"high": 82220.0,
"low": 81012.5,
"last": 81407.5,
"instrument_name": "BTC-27NOV26",
"bid_price": 81587.5,
"ask_price": 81607.5,
"open_interest": 15846850,
"mark_price": 81599.51,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.4311,
"volume": 17.28326384,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 1407140.0,
"volume_notional": 1407140.0,
"mid_price": 81597.5
},
{
"high": 81625.0,
"low": 80435.0,
"last": 80835.0,
"instrument_name": "BTC-9OCT26",
"bid_price": 81017.5,
"ask_price": 81040.0,
"open_interest": 1456590,
"mark_price": 81025.68,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.3575,
"volume": 3.98398075,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 321910.0,
"volume_notional": 321910.0,
"mid_price": 81028.75
},
{
"high": 81895.0,
"low": 80630.0,
"last": 81132.5,
"instrument_name": "BTC-30OCT26",
"bid_price": 81275.0,
"ask_price": 81287.5,
"open_interest": 48225710,
"mark_price": 81280.79,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.4753,
"volume": 17.64207412,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 1428710.0,
"volume_notional": 1428710.0,
"mid_price": 81281.25
},
{
"high": 81445.0,
"low": 80145.0,
"last": 80930.0,
"instrument_name": "BTC-21SEP26",
"bid_price": 80810.0,
"ask_price": 80825.0,
"open_interest": 1047880,
"mark_price": 80815.99,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.1357,
"volume": 1.60535557,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 129670.0,
"volume_notional": 129670.0,
"mid_price": 80817.5
},
{
"high": 81542.5,
"low": 80362.5,
"last": 80775.0,
"instrument_name": "BTC-2OCT26",
"bid_price": 80950.0,
"ask_price": 80965.0,
"open_interest": 2466040,
"mark_price": 80957.16,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.502,
"volume": 6.13587707,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 495940.0,
"volume_notional": 495940.0,
"mid_price": 80957.5
},
{
"high": 84572.5,
"low": 83330.0,
"last": 83755.0,
"instrument_name": "BTC-25JUN27",
"bid_price": 83925.0,
"ask_price": 83970.0,
"open_interest": 20255740,
"mark_price": 83945.35,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.4457,
"volume": 12.61012996,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 1055470.0,
"volume_notional": 1055470.0,
"mid_price": 83947.5
},
{
"high": 81462.5,
"low": 80192.5,
"last": 80702.5,
"instrument_name": "BTC-22SEP26",
"bid_price": 80822.5,
"ask_price": 80825.0,
"open_interest": 950130,
"mark_price": 80831.9,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.4165,
"volume": 2.61038568,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 211330.0,
"volume_notional": 211330.0,
"mid_price": 80823.75
},
{
"high": 81497.5,
"low": 80149.5,
"last": 80832.5,
"instrument_name": "BTC-PERPETUAL",
"bid_price": 80836.5,
"ask_price": 80837.0,
"open_interest": 787799210,
"mark_price": 80831.57,
"creation_timestamp": 1789939036004,
"current_funding": 0.0,
"estimated_delivery_price": 80815.88,
"funding_8h": 1.583e-05,
"price_change": -0.2493,
"volume": 3099.91787897,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 250336360.0,
"volume_notional": 250336360.0,
"mid_price": 80836.75
},
{
"high": 83547.5,
"low": 82220.0,
"last": 82757.5,
"instrument_name": "BTC-26MAR27",
"bid_price": 82907.5,
"ask_price": 82960.0,
"open_interest": 72755930,
"mark_price": 82934.37,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.3432,
"volume": 8.74194039,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 722750.0,
"volume_notional": 722750.0,
"mid_price": 82933.75
},
{
"high": 81460.0,
"low": 80197.5,
"last": 80752.5,
"instrument_name": "BTC-23SEP26",
"bid_price": 80835.0,
"ask_price": 80855.0,
"open_interest": 680650,
"mark_price": 80845.52,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.3117,
"volume": 5.33023474,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 431280.0,
"volume_notional": 431280.0,
"mid_price": 80845.0
},
{
"high": 82587.5,
"low": 81300.0,
"last": 81780.0,
"instrument_name": "BTC-25DEC26",
"bid_price": 81932.5,
"ask_price": 81955.0,
"open_interest": 275741350,
"mark_price": 81945.21,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.4504,
"volume": 60.58913468,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 4975480.0,
"volume_notional": 4975480.0,
"mid_price": 81943.75
},
{
"high": 81525.0,
"low": 80197.5,
"last": 80775.0,
"instrument_name": "BTC-25SEP26",
"bid_price": 80872.5,
"ask_price": 80882.5,
"open_interest": 470803600,
"mark_price": 80875.33,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": -0.3208,
"volume": 88.95297416,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 7192670.0,
"volume_notional": 7192670.0,
"mid_price": 80877.5
},
{
"high": 81470.0,
"low": 80272.5,
"last": 80757.5,
"instrument_name": "BTC-24SEP26",
"bid_price": 80840.0,
"ask_price": 80875.0,
"open_interest": 6340,
"mark_price": 80858.26,
"creation_timestamp": 1789939036004,
"estimated_delivery_price": 80815.88,
"price_change": 0.4665,
"volume": 0.08452116,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 6840.0,
"volume_notional": 6840.0,
"mid_price": 80857.5
}
]

View File

@@ -0,0 +1,23 @@
[
{
"high": 81497.5,
"low": 80149.5,
"last": 80832.5,
"instrument_name": "BTC-PERPETUAL",
"bid_price": 80836.5,
"ask_price": 80837.0,
"open_interest": 787799210,
"mark_price": 80831.54,
"creation_timestamp": 1789939037058,
"current_funding": 0.0,
"estimated_delivery_price": 80815.66,
"funding_8h": 1.583e-05,
"price_change": -0.2487,
"volume": 3100.02328212,
"base_currency": "BTC",
"quote_currency": "USD",
"volume_usd": 250344880.0,
"volume_notional": 250344880.0,
"mid_price": 80836.75
}
]

View File

@@ -0,0 +1,17 @@
{
"id": "BTC-STRG-30OCT26-66000_90000",
"state": "active",
"legs": [
{
"amount": 1,
"instrument_name": "BTC-30OCT26-66000-P"
},
{
"amount": 1,
"instrument_name": "BTC-30OCT26-90000-C"
}
],
"instrument_id": 698294,
"creation_timestamp": 1789845798000,
"state_timestamp": 1789845798201
}

View File

@@ -0,0 +1,202 @@
[
"BTC-BOX-25DEC26-30000_130000",
"BTC-BOX-25DEC26-35000_100000",
"BTC-BOX-25DEC26-35000_70000",
"BTC-BOX-25DEC26-40000_80000",
"BTC-BOX-25DEC26-45000_110000",
"BTC-BOX-25DEC26-50000_100000",
"BTC-BOX-25DEC26-55000_105000",
"BTC-BOX-25DEC26-56000_78000",
"BTC-BOX-25DEC26-56000_80000",
"BTC-BOX-25DEC26-60000_100000",
"BTC-BOX-25DEC26-60000_80000",
"BTC-BOX-25DEC26-60000_85000",
"BTC-BOX-25DEC26-60000_86000",
"BTC-BOX-25DEC26-60000_90000",
"BTC-BOX-25DEC26-65000_85000",
"BTC-BOX-25DEC26-70000_72000",
"BTC-BOX-25DEC26-80000_100000",
"BTC-BOX-25SEP26-160000_170000",
"BTC-BOX-25SEP26-30000_120000",
"BTC-BOX-25SEP26-40000_110000",
"BTC-BOX-25SEP26-40000_90000",
"BTC-BOX-25SEP26-50000_100000",
"BTC-BOX-25SEP26-50000_130000",
"BTC-BOX-25SEP26-50000_90000",
"BTC-BOX-25SEP26-56000_75000",
"BTC-BOX-25SEP26-60000_105000",
"BTC-BOX-25SEP26-60000_110000",
"BTC-BOX-25SEP26-60000_115000",
"BTC-BOX-25SEP26-60000_120000",
"BTC-BOX-25SEP26-60000_130000",
"BTC-BOX-25SEP26-60000_140000",
"BTC-BOX-25SEP26-60000_68000",
"BTC-BOX-25SEP26-60000_95000",
"BTC-BOX-25SEP26-70000_105000",
"BTC-BOX-25SEP26-70000_80000",
"BTC-BOX-25SEP26-80000_100000",
"BTC-BOX-25SEP26-80000_120000",
"BTC-BOX-25SEP26-80000_130000",
"BTC-BOX-25SEP26-85000_135000",
"BTC-BOX-25SEP26-85000_90000",
"BTC-BOX-25SEP26-90000_100000",
"BTC-BOX-26MAR27-50000_150000",
"BTC-CBUT-25DEC26-62000_72000_82000",
"BTC-CBUT-25DEC26-64000_74000_84000",
"BTC-CBUT-25DEC26-64000_75000_84000",
"BTC-CBUT-25DEC26-64000_76000_88000",
"BTC-CBUT-25DEC26-65000_75000_85000",
"BTC-CBUT-25DEC26-68000_70000_74000",
"BTC-CBUT-25DEC26-75000_125000_180000",
"BTC-CBUT-25JUN27-100000_125000_150000",
"BTC-CBUT-25JUN27-72000_74000_78000",
"BTC-CBUT-25JUN27-76000_80000_85000",
"BTC-CBUT-25SEP26-60000_62000_65000",
"BTC-CBUT-25SEP26-62000_75000_86000",
"BTC-CBUT-25SEP26-64000_75000_84000",
"BTC-CBUT-25SEP26-65000_70000_75000",
"BTC-CBUT-25SEP26-65000_75000_85000",
"BTC-CBUT-25SEP26-70000_75000_80000",
"BTC-CBUT-25SEP26-75000_130000_180000",
"BTC-CBUT-26MAR27-60000_80000_100000",
"BTC-CBUT-26MAR27-62000_72000_82000",
"BTC-CBUT-26MAR27-64000_74000_84000",
"BTC-CBUT-26MAR27-80000_90000_100000",
"BTC-CBUT-30OCT26-79000_81000_82000",
"BTC-CBUT111-25SEP26-125000_135000_145000",
"BTC-CBUT111-26MAR27-100000_115000_130000",
"BTC-CBUT111-26MAR27-60000_80000_100000",
"BTC-CCAL-22SEP26_21SEP26-82000",
"BTC-CCAL-25DEC26_25SEP26-140000",
"BTC-CCAL-25DEC26_25SEP26-45000",
"BTC-CCAL-25DEC26_25SEP26-55000",
"BTC-CCAL-25DEC26_25SEP26-66000",
"BTC-CCAL-25DEC26_25SEP26-72000",
"BTC-CCAL-25DEC26_25SEP26-75000",
"BTC-CCAL-25DEC26_25SEP26-80000",
"BTC-CCAL-25DEC26_25SEP26-90000",
"BTC-CCAL-25DEC26_25SEP26-92000",
"BTC-CCAL-25JUN27_25SEP26-115000",
"BTC-CCAL-25JUN27_25SEP26-60000",
"BTC-CCAL-25JUN27_26MAR27-100000",
"BTC-CCAL-25JUN27_26MAR27-70000",
"BTC-CCAL-26MAR27_25DEC26-100000",
"BTC-CCAL-26MAR27_25DEC26-140000",
"BTC-CCAL-26MAR27_25DEC26-70000",
"BTC-CCAL-26MAR27_25DEC26-90000",
"BTC-CCAL-26MAR27_25DEC26-94000",
"BTC-CCAL-26MAR27_25DEC26-95000",
"BTC-CCAL-26MAR27_25SEP26-100000",
"BTC-CCAL-26MAR27_25SEP26-130000",
"BTC-CCAL-26MAR27_25SEP26-70000",
"BTC-CCAL-2OCT26_25SEP26-85000",
"BTC-CCAL-30OCT26_25SEP26-70000",
"BTC-CCAL-30OCT26_25SEP26-78000",
"BTC-CCAL-30OCT26_25SEP26-85000",
"BTC-CCAL-30OCT26_25SEP26-86000",
"BTC-CCAL-30OCT26_25SEP26-90000",
"BTC-CCAL-30OCT26_9OCT26-85000",
"BTC-CCAL-9OCT26_2OCT26-85000",
"BTC-CCOND-25DEC26-68000_72000_75000_78000",
"BTC-CCOND-25SEP26-105000_110000_115000_120000",
"BTC-CCOND-25SEP26-75000_78000_83000_86000",
"BTC-CCOND-25SEP26-77000_79000_84000_110000",
"BTC-CCOND-25SEP26-85000_90000_94000_96000",
"BTC-CDIAG-25DEC26_25SEP26-100000_125000",
"BTC-CDIAG-25DEC26_25SEP26-100000_86000",
"BTC-CDIAG-25DEC26_25SEP26-100000_88000",
"BTC-CDIAG-25DEC26_25SEP26-120000_105000",
"BTC-CDIAG-25DEC26_25SEP26-125000_95000",
"BTC-CDIAG-25DEC26_25SEP26-130000_125000",
"BTC-CDIAG-25DEC26_25SEP26-170000_150000",
"BTC-CDIAG-25DEC26_25SEP26-45000_66000",
"BTC-CDIAG-25DEC26_25SEP26-50000_58000",
"BTC-CDIAG-25DEC26_25SEP26-50000_66000",
"BTC-CDIAG-25DEC26_25SEP26-70000_85000",
"BTC-CDIAG-25DEC26_25SEP26-80000_75000",
"BTC-CDIAG-25DEC26_25SEP26-82000_72000",
"BTC-CDIAG-25DEC26_25SEP26-82000_76000",
"BTC-CDIAG-25DEC26_25SEP26-82000_78000",
"BTC-CDIAG-25DEC26_25SEP26-90000_76000",
"BTC-CDIAG-25DEC26_25SEP26-90000_78000",
"BTC-CDIAG-25DEC26_25SEP26-90000_79000",
"BTC-CDIAG-25DEC26_25SEP26-90000_80000",
"BTC-CDIAG-25DEC26_25SEP26-90000_81000",
"BTC-CDIAG-25DEC26_25SEP26-95000_86000",
"BTC-CDIAG-25DEC26_25SEP26-98000_86000",
"BTC-CDIAG-25DEC26_25SEP26-98000_88000",
"BTC-CDIAG-25DEC26_27NOV26-84000_81000",
"BTC-CDIAG-25DEC26_27NOV26-90000_85000",
"BTC-CDIAG-25DEC26_30OCT26-105000_96000",
"BTC-CDIAG-25DEC26_30OCT26-106000_86000",
"BTC-CDIAG-25DEC26_30OCT26-108000_86000",
"BTC-CDIAG-25DEC26_30OCT26-74000_78000",
"BTC-CDIAG-25DEC26_30OCT26-76000_72000",
"BTC-CDIAG-25DEC26_30OCT26-76000_73000",
"BTC-CDIAG-25DEC26_30OCT26-78000_73000",
"BTC-CDIAG-25DEC26_30OCT26-80000_74000",
"BTC-CDIAG-25DEC26_30OCT26-80000_75000",
"BTC-CDIAG-25DEC26_30OCT26-82000_75000",
"BTC-CDIAG-25DEC26_30OCT26-82000_76000",
"BTC-CDIAG-25DEC26_30OCT26-82000_78000",
"BTC-CDIAG-25DEC26_30OCT26-84000_76000",
"BTC-CDIAG-25DEC26_30OCT26-86000_75000",
"BTC-CDIAG-25JUN27_25DEC26-96000_80000",
"BTC-CDIAG-25JUN27_25DEC26-98000_82000",
"BTC-CDIAG-25JUN27_25SEP26-100000_68000",
"BTC-CDIAG-25JUN27_26MAR27-88000_82000",
"BTC-CDIAG-25JUN27_30OCT26-110000_81000",
"BTC-CDIAG-26MAR27_25DEC26-110000_84000",
"BTC-CDIAG-26MAR27_25DEC26-110000_85000",
"BTC-CDIAG-26MAR27_25DEC26-110000_88000",
"BTC-CDIAG-26MAR27_25DEC26-130000_140000",
"BTC-CDIAG-26MAR27_25DEC26-130000_85000",
"BTC-CDIAG-26MAR27_25DEC26-130000_86000",
"BTC-CDIAG-26MAR27_25DEC26-130000_88000",
"BTC-CDIAG-26MAR27_25DEC26-130000_90000",
"BTC-CDIAG-26MAR27_25DEC26-130000_92000",
"BTC-CDIAG-26MAR27_25DEC26-50000_68000",
"BTC-CDIAG-26MAR27_25DEC26-80000_72000",
"BTC-CDIAG-26MAR27_25DEC26-80000_75000",
"BTC-CDIAG-26MAR27_25DEC26-82000_75000",
"BTC-CDIAG-26MAR27_25DEC26-84000_78000",
"BTC-CDIAG-26MAR27_25DEC26-85000_75000",
"BTC-CDIAG-26MAR27_25DEC26-85000_80000",
"BTC-CDIAG-26MAR27_25DEC26-86000_80000",
"BTC-CDIAG-26MAR27_25DEC26-88000_80000",
"BTC-CDIAG-26MAR27_25DEC26-90000_82000",
"BTC-CDIAG-26MAR27_25SEP26-125000_78000",
"BTC-CDIAG-26MAR27_25SEP26-130000_76000",
"BTC-CDIAG-26MAR27_25SEP26-35000_56000",
"BTC-CDIAG-26MAR27_25SEP26-58000_72000",
"BTC-CDIAG-26MAR27_30OCT26-58000_75000",
"BTC-CDIAG-26MAR27_30OCT26-82000_71000",
"BTC-CDIAG-26MAR27_30OCT26-82000_75000",
"BTC-CDIAG-26MAR27_30OCT26-88000_75000",
"BTC-CDIAG-27NOV26_25SEP26-74000_70000",
"BTC-CDIAG-27NOV26_25SEP26-75000_70000",
"BTC-CDIAG-27NOV26_25SEP26-76000_72000",
"BTC-CDIAG-27NOV26_25SEP26-77000_72000",
"BTC-CDIAG-27NOV26_25SEP26-78000_74000",
"BTC-CDIAG-27NOV26_25SEP26-80000_74000",
"BTC-CDIAG-27NOV26_25SEP26-81000_75000",
"BTC-CDIAG-27NOV26_25SEP26-82000_76000",
"BTC-CDIAG-27NOV26_25SEP26-83000_76000",
"BTC-CDIAG-27NOV26_30OCT26-100000_88000",
"BTC-CDIAG-27NOV26_30OCT26-72000_71000",
"BTC-CDIAG-27NOV26_30OCT26-73000_72000",
"BTC-CDIAG-27NOV26_30OCT26-74000_72000",
"BTC-CDIAG-27NOV26_30OCT26-82000_80000",
"BTC-CDIAG-27NOV26_30OCT26-89000_84000",
"BTC-CDIAG-27NOV26_30OCT26-93000_96000",
"BTC-CDIAG-27NOV26_30OCT26-94000_96000",
"BTC-CDIAG-2OCT26_21SEP26-79000_78000",
"BTC-CDIAG-2OCT26_25SEP26-74000_75000",
"BTC-CDIAG-30OCT26_25SEP26-100000_95000",
"BTC-CDIAG-30OCT26_25SEP26-71000_68000",
"BTC-CDIAG-30OCT26_25SEP26-73000_70000",
"BTC-CDIAG-30OCT26_25SEP26-74000_72000",
"BTC-CDIAG-30OCT26_25SEP26-75000_70000",
"BTC-CDIAG-30OCT26_25SEP26-75000_72000",
"BTC-CDIAG-30OCT26_25SEP26-76000_72000"
]

View File

@@ -0,0 +1,342 @@
[
{
"id": "BTC-CCAL-22SEP26_21SEP26-82000",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-21SEP26-82000-C"
},
{
"amount": 1,
"instrument_name": "BTC-22SEP26-82000-C"
}
],
"instrument_id": 698874,
"creation_timestamp": 1789903657000,
"state_timestamp": 1789903657878
},
{
"id": "BTC-CCAL-25DEC26_25SEP26-55000",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-25SEP26-55000-C"
},
{
"amount": 1,
"instrument_name": "BTC-25DEC26-55000-C"
}
],
"instrument_id": 698013,
"creation_timestamp": 1789804851000,
"state_timestamp": 1789886736196
},
{
"id": "BTC-CCAL-30OCT26_9OCT26-85000",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-9OCT26-85000-C"
},
{
"amount": 1,
"instrument_name": "BTC-30OCT26-85000-C"
}
],
"instrument_id": 699021,
"creation_timestamp": 1789935402000,
"state_timestamp": 1789935402766
},
{
"id": "BTC-CS-25SEP26-70000_81000",
"state": "active",
"legs": [
{
"amount": 1,
"instrument_name": "BTC-25SEP26-70000-C"
},
{
"amount": -1,
"instrument_name": "BTC-25SEP26-81000-C"
}
],
"instrument_id": 698867,
"creation_timestamp": 1789899898000,
"state_timestamp": 1789899898697
},
{
"id": "BTC-CS-25SEP26-80000_85000",
"state": "active",
"legs": [
{
"amount": 1,
"instrument_name": "BTC-25SEP26-80000-C"
},
{
"amount": -1,
"instrument_name": "BTC-25SEP26-85000-C"
}
],
"instrument_id": 698859,
"creation_timestamp": 1789892310000,
"state_timestamp": 1789892310502
},
{
"id": "BTC-CS-30OCT26-85000_95000",
"state": "active",
"legs": [
{
"amount": 1,
"instrument_name": "BTC-30OCT26-85000-C"
},
{
"amount": -1,
"instrument_name": "BTC-30OCT26-95000-C"
}
],
"instrument_id": 686334,
"creation_timestamp": 1788236302000,
"state_timestamp": 1789918883563
},
{
"id": "BTC-CS-30OCT26-90000_96000",
"state": "active",
"legs": [
{
"amount": 1,
"instrument_name": "BTC-30OCT26-90000-C"
},
{
"amount": -1,
"instrument_name": "BTC-30OCT26-96000-C"
}
],
"instrument_id": 681583,
"creation_timestamp": 1787577101000,
"state_timestamp": 1789930557812
},
{
"id": "BTC-FS-21SEP26_PERP",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-PERPETUAL"
},
{
"amount": 1,
"instrument_name": "BTC-21SEP26"
}
],
"instrument_id": 696453,
"creation_timestamp": 1789632397000,
"state_timestamp": 1789632397153
},
{
"id": "BTC-FS-22SEP26_21SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-21SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-22SEP26"
}
],
"instrument_id": 697148,
"creation_timestamp": 1789718801000,
"state_timestamp": 1789718801472
},
{
"id": "BTC-FS-22SEP26_PERP",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-PERPETUAL"
},
{
"amount": 1,
"instrument_name": "BTC-22SEP26"
}
],
"instrument_id": 697145,
"creation_timestamp": 1789718797000,
"state_timestamp": 1789718797046
},
{
"id": "BTC-FS-23SEP26_21SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-21SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-23SEP26"
}
],
"instrument_id": 698119,
"creation_timestamp": 1789805208000,
"state_timestamp": 1789805208337
},
{
"id": "BTC-FS-23SEP26_22SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-22SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-23SEP26"
}
],
"instrument_id": 698120,
"creation_timestamp": 1789805208000,
"state_timestamp": 1789805208708
},
{
"id": "BTC-FS-23SEP26_PERP",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-PERPETUAL"
},
{
"amount": 1,
"instrument_name": "BTC-23SEP26"
}
],
"instrument_id": 698117,
"creation_timestamp": 1789805200000,
"state_timestamp": 1789805200468
},
{
"id": "BTC-FS-24SEP26_21SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-21SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-24SEP26"
}
],
"instrument_id": 698832,
"creation_timestamp": 1789891599000,
"state_timestamp": 1789891599976
},
{
"id": "BTC-FS-24SEP26_22SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-22SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-24SEP26"
}
],
"instrument_id": 698833,
"creation_timestamp": 1789891600000,
"state_timestamp": 1789891600277
},
{
"id": "BTC-FS-24SEP26_23SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-23SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-24SEP26"
}
],
"instrument_id": 698834,
"creation_timestamp": 1789891600000,
"state_timestamp": 1789891600576
},
{
"id": "BTC-FS-24SEP26_PERP",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-PERPETUAL"
},
{
"amount": 1,
"instrument_name": "BTC-24SEP26"
}
],
"instrument_id": 698831,
"creation_timestamp": 1789891597000,
"state_timestamp": 1789891597281
},
{
"id": "BTC-FS-25DEC26_21SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-21SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-25DEC26"
}
],
"instrument_id": 696466,
"creation_timestamp": 1789632402000,
"state_timestamp": 1789632402806
},
{
"id": "BTC-FS-25DEC26_22SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-22SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-25DEC26"
}
],
"instrument_id": 697154,
"creation_timestamp": 1789718803000,
"state_timestamp": 1789718803121
},
{
"id": "BTC-FS-25DEC26_23SEP26",
"state": "active",
"legs": [
{
"amount": -1,
"instrument_name": "BTC-23SEP26"
},
{
"amount": 1,
"instrument_name": "BTC-25DEC26"
}
],
"instrument_id": 698126,
"creation_timestamp": 1789805212000,
"state_timestamp": 1789805212770
}
]

View File

@@ -0,0 +1,3 @@
{
"contract_size": 10.0
}

View File

@@ -0,0 +1,162 @@
[
{
"decimals": 6,
"currency": "USDT",
"apr": 0.0,
"min_withdrawal_fee": 2.0,
"withdrawal_fee": 2.0,
"coin_type": "USDT",
"withdrawal_priorities": [],
"coinbase_networks": [
{
"display_name": "Ethereum",
"resource_name": "networks/ethereum-mainnet"
}
],
"network_fee": 3.69791145e-06,
"network_currency": "ETH",
"min_confirmations": 1,
"currency_long": "Tether",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 6,
"currency_uuid": "b26327c1-9a34-51d9-b982-9b29e6012648"
},
{
"decimals": 6,
"currency": "USDE",
"apr": 4.75,
"min_withdrawal_fee": 0.0001,
"withdrawal_fee": 0.007617,
"coin_type": "USDE",
"withdrawal_priorities": [],
"network_fee": 2.9071511e-06,
"network_currency": "ETH",
"min_confirmations": 1,
"currency_long": "Ethena USDe",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 6,
"currency_uuid": "e98ecced-8122-45a8-8bce-b842323af19d"
},
{
"decimals": 8,
"currency": "USDC",
"apr": 3.3999999999999995,
"min_withdrawal_fee": 5.0,
"withdrawal_fee": 5.0,
"coin_type": "USDC",
"withdrawal_priorities": [],
"coinbase_networks": [
{
"display_name": "Arbitrum",
"resource_name": "networks/arbitrum-mainnet"
},
{
"display_name": "Avalanche C-Chain",
"resource_name": "networks/avacchain-mainnet"
},
{
"display_name": "Base",
"resource_name": "networks/base-mainnet"
},
{
"display_name": "Ethereum",
"resource_name": "networks/ethereum-mainnet"
},
{
"display_name": "Optimism",
"resource_name": "networks/optimism-mainnet"
},
{
"display_name": "Polygon",
"resource_name": "networks/polygon-mainnet"
},
{
"display_name": "Solana",
"resource_name": "networks/solana-mainnet"
}
],
"network_fee": 3.0608777e-06,
"network_currency": "ETH",
"min_confirmations": 50,
"currency_long": "USD Coin",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 6,
"currency_uuid": "2b92315d-eab7-5bef-84fa-089a131333f5"
},
{
"decimals": 6,
"currency": "STETH",
"apr": 2.251857142857143,
"min_withdrawal_fee": 0.0001,
"withdrawal_fee": 0.0001,
"coin_type": "STETH",
"withdrawal_priorities": [],
"network_fee": 2.9071511e-06,
"network_currency": "ETH",
"min_confirmations": 1,
"currency_long": "Staked ETH",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 6,
"currency_uuid": "997e0978-a0a0-502d-a51b-9ae33bb54a49"
},
{
"decimals": 6,
"currency": "EURR",
"apr": 0.0,
"min_withdrawal_fee": 0.0001,
"withdrawal_fee": 0.006177,
"coin_type": "EURR",
"withdrawal_priorities": [],
"network_fee": 2.71280495e-06,
"network_currency": "ETH",
"min_confirmations": 1,
"currency_long": "StablR Euro",
"in_cross_collateral_pool": false,
"onchain_operations_precision": 6,
"currency_uuid": "20d8642e-36ad-4f92-becf-a68b2b0d05ef"
},
{
"decimals": 6,
"currency": "ETH",
"apr": 0.0,
"min_withdrawal_fee": 0.001,
"withdrawal_fee": 0.001,
"coin_type": "ETH",
"withdrawal_priorities": [],
"coinbase_networks": [
{
"display_name": "Ethereum",
"resource_name": "networks/ethereum-mainnet"
}
],
"network_fee": 1.7949723900000002e-06,
"network_currency": "ETH",
"min_confirmations": 50,
"currency_long": "Ethereum",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 6,
"currency_uuid": "d85dce9b-5b73-5c3c-8978-522ce1d1c1b4"
},
{
"decimals": 8,
"currency": "BTC",
"apr": 0.0,
"min_withdrawal_fee": 1e-05,
"withdrawal_fee": 1e-05,
"coin_type": "BTC",
"withdrawal_priorities": [],
"coinbase_networks": [
{
"display_name": "Bitcoin",
"resource_name": "networks/bitcoin-mainnet"
}
],
"network_fee": 3.072e-06,
"network_currency": "BTC",
"min_confirmations": 1,
"currency_long": "Bitcoin",
"in_cross_collateral_pool": true,
"onchain_operations_precision": 8,
"currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5"
}
]

View File

@@ -0,0 +1,25 @@
{
"data": [
{
"date": "2026-09-20",
"delivery_price": 80321.21
},
{
"date": "2026-09-19",
"delivery_price": 81053.19
},
{
"date": "2026-09-18",
"delivery_price": 77720.28
},
{
"date": "2026-09-17",
"delivery_price": 76422.2
},
{
"date": "2026-09-16",
"delivery_price": 75764.23
}
],
"records_total": 2614
}

View File

@@ -0,0 +1,32 @@
{
"option": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27"
],
"future": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"30SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27",
"PERPETUAL"
]
}

View File

@@ -0,0 +1,280 @@
{
"steth": {
"option": [],
"future": []
},
"pepe": {
"option": [],
"future": []
},
"ltc": {
"option": [],
"future": []
},
"usyc": {
"option": [],
"future": []
},
"doge": {
"option": [],
"future": []
},
"dot": {
"option": [],
"future": []
},
"usdc": {
"option": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"30OCT26",
"25DEC26"
],
"future": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"30SEP26",
"2OCT26",
"30OCT26",
"25DEC26",
"PERPETUAL"
]
},
"zora": {
"option": [],
"future": []
},
"xlm": {
"option": [],
"future": []
},
"ondo": {
"option": [],
"future": []
},
"bnb": {
"option": [],
"future": []
},
"jasmy": {
"option": [],
"future": []
},
"fil": {
"option": [],
"future": []
},
"eurr": {
"option": [],
"future": []
},
"uni": {
"option": [],
"future": []
},
"atom": {
"option": [],
"future": []
},
"arb": {
"option": [],
"future": []
},
"bonk": {
"option": [],
"future": []
},
"link": {
"option": [],
"future": []
},
"xrp": {
"option": [],
"future": []
},
"vet": {
"option": [],
"future": []
},
"avax": {
"option": [],
"future": []
},
"bch": {
"option": [],
"future": []
},
"pump": {
"option": [],
"future": []
},
"usde": {
"option": [],
"future": []
},
"paxg": {
"option": [],
"future": []
},
"op": {
"option": [],
"future": []
},
"sui": {
"option": [],
"future": []
},
"sol": {
"option": [],
"future": []
},
"eth": {
"option": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27"
],
"future": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27",
"PERPETUAL"
]
},
"aero": {
"option": [],
"future": []
},
"hype": {
"option": [],
"future": []
},
"pengu": {
"option": [],
"future": []
},
"tao": {
"option": [],
"future": []
},
"buidl": {
"option": [],
"future": []
},
"hbar": {
"option": [],
"future": []
},
"usdt": {
"option": [],
"future": []
},
"matic": {
"option": [],
"future": []
},
"fartcoin": {
"option": [],
"future": []
},
"cbeth": {
"option": [],
"future": []
},
"aave": {
"option": [],
"future": []
},
"algo": {
"option": [],
"future": []
},
"shib": {
"option": [],
"future": []
},
"inj": {
"option": [],
"future": []
},
"ada": {
"option": [],
"future": []
},
"near": {
"option": [],
"future": []
},
"btc": {
"option": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27"
],
"future": [
"21SEP26",
"22SEP26",
"23SEP26",
"24SEP26",
"25SEP26",
"2OCT26",
"9OCT26",
"30OCT26",
"27NOV26",
"25DEC26",
"26MAR27",
"25JUN27",
"PERPETUAL"
]
},
"eurc": {
"option": [],
"future": []
},
"apt": {
"option": [],
"future": []
},
"icp": {
"option": [],
"future": []
},
"crv": {
"option": [],
"future": []
}
}

View File

@@ -0,0 +1,306 @@
{
"data": [
{
"timestamp": 1789910220000,
"index_price": 80478.71,
"interest_8h": 3.41745351036981e-05
},
{
"timestamp": 1789910280000,
"index_price": 80461.03,
"interest_8h": 5.499236711247856e-05
},
{
"timestamp": 1789910340000,
"index_price": 80497.56,
"interest_8h": 4.976063870761619e-05
},
{
"timestamp": 1789910400000,
"index_price": 80470.1,
"interest_8h": 3.656606615373497e-05
},
{
"timestamp": 1789910460000,
"index_price": 80491.32,
"interest_8h": 6.158639217231364e-05
},
{
"timestamp": 1789910520000,
"index_price": 80490.9,
"interest_8h": 9.115657794864267e-05
},
{
"timestamp": 1789910580000,
"index_price": 80502.97,
"interest_8h": 4.7504551695385744e-05
},
{
"timestamp": 1789910640000,
"index_price": 80526.04,
"interest_8h": 4.257616542440465e-05
},
{
"timestamp": 1789910700000,
"index_price": 80516.17,
"interest_8h": 1.988367678189268e-05
},
{
"timestamp": 1789910760000,
"index_price": 80535.28,
"interest_8h": 6.017462160677812e-05
},
{
"timestamp": 1789910820000,
"index_price": 80503.64,
"interest_8h": 2.5888146175830073e-05
},
{
"timestamp": 1789910880000,
"index_price": 80499.48,
"interest_8h": 6.896069390852815e-06
},
{
"timestamp": 1789910940000,
"index_price": 80480.81,
"interest_8h": 1.1157628757265574e-06
},
{
"timestamp": 1789911000000,
"index_price": 80480.89,
"interest_8h": 1.3640194584817102e-06
},
{
"timestamp": 1789911060000,
"index_price": 80477.35,
"interest_8h": 5.3513504108716075e-06
},
{
"timestamp": 1789911120000,
"index_price": 80475.24,
"interest_8h": 6.103616466388657e-06
},
{
"timestamp": 1789911180000,
"index_price": 80445.49,
"interest_8h": 0.0
},
{
"timestamp": 1789911240000,
"index_price": 80402.98,
"interest_8h": 0.0
},
{
"timestamp": 1789911300000,
"index_price": 80440.61,
"interest_8h": 0.0
},
{
"timestamp": 1789911360000,
"index_price": 80433.97,
"interest_8h": 2.712171859717478e-05
},
{
"timestamp": 1789911420000,
"index_price": 80424.68,
"interest_8h": 4.150840264485127e-06
},
{
"timestamp": 1789911480000,
"index_price": 80428.84,
"interest_8h": 4.9270759095953434e-05
},
{
"timestamp": 1789911540000,
"index_price": 80453.16,
"interest_8h": 2.9665832889596872e-05
},
{
"timestamp": 1789911600000,
"index_price": 80444.94,
"interest_8h": 1.4404448558281916e-05
},
{
"timestamp": 1789911660000,
"index_price": 80442.65,
"interest_8h": 0.0
},
{
"timestamp": 1789911720000,
"index_price": 80472.27,
"interest_8h": 3.568847380583005e-05
},
{
"timestamp": 1789911780000,
"index_price": 80505.23,
"interest_8h": 6.202941721935202e-05
},
{
"timestamp": 1789911840000,
"index_price": 80535.21,
"interest_8h": 7.097761960254734e-05
},
{
"timestamp": 1789911900000,
"index_price": 80542.03,
"interest_8h": 7.2569874387467355e-06
},
{
"timestamp": 1789911960000,
"index_price": 80519.08,
"interest_8h": 0.0
},
{
"timestamp": 1789912020000,
"index_price": 80511.51,
"interest_8h": 4.0009496780229505e-06
},
{
"timestamp": 1789912080000,
"index_price": 80511.82,
"interest_8h": 0.0
},
{
"timestamp": 1789912140000,
"index_price": 80521.37,
"interest_8h": 2.085977300184626e-05
},
{
"timestamp": 1789912200000,
"index_price": 80511.36,
"interest_8h": 4.026959673759505e-05
},
{
"timestamp": 1789912260000,
"index_price": 80512.72,
"interest_8h": 2.001944537465927e-05
},
{
"timestamp": 1789912320000,
"index_price": 80554.59,
"interest_8h": 8.629368605806955e-05
},
{
"timestamp": 1789912380000,
"index_price": 80535.97,
"interest_8h": 4.6513470937329694e-05
},
{
"timestamp": 1789912440000,
"index_price": 80531.07,
"interest_8h": 9.073805302708352e-05
},
{
"timestamp": 1789912500000,
"index_price": 80539.43,
"interest_8h": 4.823901162454312e-05
},
{
"timestamp": 1789912560000,
"index_price": 80526.07,
"interest_8h": 4.816430877596233e-05
},
{
"timestamp": 1789912620000,
"index_price": 80525.6,
"interest_8h": 5.139483592782319e-05
},
{
"timestamp": 1789912680000,
"index_price": 80528.09,
"interest_8h": 3.2261754873491686e-05
},
{
"timestamp": 1789912740000,
"index_price": 80538.33,
"interest_8h": 4.600812433032208e-05
},
{
"timestamp": 1789912800000,
"index_price": 80553.72,
"interest_8h": 7.773160569113602e-05
},
{
"timestamp": 1789912860000,
"index_price": 80608.02,
"interest_8h": 5.840603701716753e-05
},
{
"timestamp": 1789912920000,
"index_price": 80671.08,
"interest_8h": 8.444451220928148e-05
},
{
"timestamp": 1789912980000,
"index_price": 80750.82,
"interest_8h": 0.00010405213222598241
},
{
"timestamp": 1789913040000,
"index_price": 80686.09,
"interest_8h": 6.28172402455645e-05
},
{
"timestamp": 1789913100000,
"index_price": 80633.77,
"interest_8h": 5.7067373880630395e-05
},
{
"timestamp": 1789913160000,
"index_price": 80650.99,
"interest_8h": 9.457109577940204e-05
},
{
"timestamp": 1789913220000,
"index_price": 80649.47,
"interest_8h": 9.122976877596435e-05
},
{
"timestamp": 1789913280000,
"index_price": 80640.07,
"interest_8h": 8.730129450528856e-05
},
{
"timestamp": 1789913340000,
"index_price": 80627.14,
"interest_8h": 0.00012493578465013052
},
{
"timestamp": 1789913400000,
"index_price": 80606.03,
"interest_8h": 0.0001334700704153708
},
{
"timestamp": 1789913460000,
"index_price": 80585.89,
"interest_8h": 8.752807098112448e-05
},
{
"timestamp": 1789913520000,
"index_price": 80576.14,
"interest_8h": 7.093867986230891e-05
},
{
"timestamp": 1789913580000,
"index_price": 80555.73,
"interest_8h": 8.641306459522893e-05
},
{
"timestamp": 1789913640000,
"index_price": 80563.01,
"interest_8h": 7.322526181690768e-05
},
{
"timestamp": 1789913700000,
"index_price": 80515.67,
"interest_8h": 6.770213177138991e-05
},
{
"timestamp": 1789913760000,
"index_price": 80510.3,
"interest_8h": 8.5236609477181e-05
}
],
"interest_8h": 1.58883550430331e-05,
"current_interest": 0.0
}

View File

@@ -0,0 +1,170 @@
[
{
"timestamp": 1789855200000,
"index_price": 81079.79,
"interest_8h": 0.00020037536421028146,
"interest_1h": 2.517506579644087e-05,
"prev_index_price": 81134.69
},
{
"timestamp": 1789858800000,
"index_price": 81237.22,
"interest_8h": 0.0002110867228833884,
"interest_1h": 2.348022400972763e-05,
"prev_index_price": 81079.79
},
{
"timestamp": 1789862400000,
"index_price": 81241.25,
"interest_8h": 0.00020390510396112773,
"interest_1h": 1.5820572974398556e-05,
"prev_index_price": 81237.22
},
{
"timestamp": 1789866000000,
"index_price": 81280.55,
"interest_8h": 0.0001866800598579058,
"interest_1h": 1.168079644171095e-05,
"prev_index_price": 81241.25
},
{
"timestamp": 1789869600000,
"index_price": 81078.04,
"interest_8h": 0.00017209580603703834,
"interest_1h": 1.6878746070219188e-05,
"prev_index_price": 81280.55
},
{
"timestamp": 1789873200000,
"index_price": 80301.73,
"interest_8h": 0.00016377878201952157,
"interest_1h": 1.618723881084165e-05,
"prev_index_price": 81078.04
},
{
"timestamp": 1789876800000,
"index_price": 80466.11,
"interest_8h": 0.00015410304928169024,
"interest_1h": 1.6038603796340042e-05,
"prev_index_price": 80301.73
},
{
"timestamp": 1789880400000,
"index_price": 80406.86,
"interest_8h": 0.00013640125860698012,
"interest_1h": 1.0921269949361024e-05,
"prev_index_price": 80466.11
},
{
"timestamp": 1789884000000,
"index_price": 80467.53,
"interest_8h": 0.00011438115897154783,
"interest_1h": 3.2034180506026057e-06,
"prev_index_price": 80406.86
},
{
"timestamp": 1789887600000,
"index_price": 80284.73,
"interest_8h": 9.32111909045898e-05,
"interest_1h": 2.116139038499294e-06,
"prev_index_price": 80467.53
},
{
"timestamp": 1789891200000,
"index_price": 80292.92,
"interest_8h": 8.225487389838831e-05,
"interest_1h": 4.800598032729875e-06,
"prev_index_price": 80284.73
},
{
"timestamp": 1789894800000,
"index_price": 80236.52,
"interest_8h": 7.930890611540408e-05,
"interest_1h": 8.592850151835521e-06,
"prev_index_price": 80292.92
},
{
"timestamp": 1789898400000,
"index_price": 80439.57,
"interest_8h": 7.711337096560992e-05,
"interest_1h": 1.4684741829057852e-05,
"prev_index_price": 80236.52
},
{
"timestamp": 1789902000000,
"index_price": 80290.85,
"interest_8h": 7.764449418518562e-05,
"interest_1h": 1.731978357173989e-05,
"prev_index_price": 80439.57
},
{
"timestamp": 1789905600000,
"index_price": 80455.12,
"interest_8h": 7.064469331448168e-05,
"interest_1h": 8.904895798545535e-06,
"prev_index_price": 80290.85
},
{
"timestamp": 1789909200000,
"index_price": 80462.51,
"interest_8h": 6.356288774768103e-05,
"interest_1h": 3.8857716514232716e-06,
"prev_index_price": 80455.12
},
{
"timestamp": 1789912800000,
"index_price": 80538.42,
"interest_8h": 6.371065529221617e-05,
"interest_1h": 3.3054358812664526e-06,
"prev_index_price": 80462.51
},
{
"timestamp": 1789916400000,
"index_price": 80652.54,
"interest_8h": 6.88962968161614e-05,
"interest_1h": 7.465221177076952e-06,
"prev_index_price": 80538.42
},
{
"timestamp": 1789920000000,
"index_price": 80886.2,
"interest_8h": 6.66477755274478e-05,
"interest_1h": 2.559066476957397e-06,
"prev_index_price": 80652.54
},
{
"timestamp": 1789923600000,
"index_price": 81314.35,
"interest_8h": 6.029910188939407e-05,
"interest_1h": 2.303457349941978e-06,
"prev_index_price": 80886.2
},
{
"timestamp": 1789927200000,
"index_price": 81133.09,
"interest_8h": 4.613047154922585e-05,
"interest_1h": 4.0705398072082257e-07,
"prev_index_price": 81314.35
},
{
"timestamp": 1789930800000,
"index_price": 81022.44,
"interest_8h": 2.895317790831924e-05,
"interest_1h": 1.9579548790115389e-07,
"prev_index_price": 81133.09
},
{
"timestamp": 1789934400000,
"index_price": 81163.58,
"interest_8h": 2.0166039693856925e-05,
"interest_1h": 7.728119084922554e-08,
"prev_index_price": 81022.44
},
{
"timestamp": 1789938000000,
"index_price": 81080.82,
"interest_8h": 1.6317565552435866e-05,
"interest_1h": 3.6114071476582626e-08,
"prev_index_price": 81163.58
}
]

View File

@@ -0,0 +1,242 @@
[
[
1788566400000,
45.882391305049836
],
[
1788566400000,
45.882391305049836
],
[
1788570000000,
45.82361062125136
],
[
1788573600000,
45.59941784506282
],
[
1788577200000,
44.612462181646315
],
[
1788580800000,
44.374337703551326
],
[
1788584400000,
44.37190609736692
],
[
1788588000000,
44.24576674351413
],
[
1788591600000,
44.15370915488827
],
[
1788595200000,
44.14077093901336
],
[
1788598800000,
43.922274146490885
],
[
1788602400000,
39.712802749423844
],
[
1788606000000,
38.79826012590313
],
[
1788609600000,
38.79452396048651
],
[
1788613200000,
38.16053217245645
],
[
1788616800000,
38.0423846788947
],
[
1788620400000,
38.04257037517399
],
[
1788624000000,
38.04116200468372
],
[
1788627600000,
38.02487922966197
],
[
1788631200000,
38.01249826894891
],
[
1788634800000,
38.008479376183935
],
[
1788638400000,
37.97117984102993
],
[
1788642000000,
37.996183996065994
],
[
1788645600000,
37.862274629559266
],
[
1788649200000,
37.34360231641064
],
[
1788652800000,
37.34923604440181
],
[
1788656400000,
37.34361921064971
],
[
1788660000000,
37.26147973855525
],
[
1788663600000,
37.260474276405915
],
[
1788667200000,
37.05677556381301
],
[
1788670800000,
37.06264148278253
],
[
1788674400000,
37.07432061271084
],
[
1788678000000,
36.42356181385526
],
[
1788681600000,
36.440761159345996
],
[
1788685200000,
36.42054104287318
],
[
1788688800000,
36.41643755891511
],
[
1788692400000,
36.40554285175305
],
[
1788696000000,
36.40858448251462
],
[
1788699600000,
36.39957652159652
],
[
1788703200000,
36.38204816584679
],
[
1788706800000,
36.38599922715135
],
[
1788710400000,
36.38484019515988
],
[
1788714000000,
36.399284438365605
],
[
1788717600000,
36.34531043724365
],
[
1788721200000,
36.34701202847672
],
[
1788724800000,
36.368927169042905
],
[
1788728400000,
36.361783875986184
],
[
1788732000000,
36.358728205612444
],
[
1788735600000,
36.23666065332862
],
[
1788739200000,
36.23120744020156
],
[
1788742800000,
36.273090013075176
],
[
1788746400000,
36.264687293335214
],
[
1788750000000,
36.28173268858742
],
[
1788753600000,
36.27665195999784
],
[
1788757200000,
36.30368643385408
],
[
1788760800000,
36.25447357121739
],
[
1788764400000,
36.08275664288014
],
[
1788768000000,
36.087195163195176
],
[
1788771600000,
36.116470746575025
],
[
1788775200000,
35.97731607070047
]
]

View File

@@ -0,0 +1,242 @@
[
[
1789935450000,
81110.95
],
[
1789935456000,
81110.428333
],
[
1789935462000,
81113.761667
],
[
1789935468000,
81112.3
],
[
1789935474000,
81110.728333
],
[
1789935480000,
81110.993333
],
[
1789935486000,
81111.161667
],
[
1789935492000,
81110.811667
],
[
1789935498000,
81110.6
],
[
1789935504000,
81117.588333
],
[
1789935510000,
81118.403333
],
[
1789935516000,
81118.701667
],
[
1789935522000,
81117.903333
],
[
1789935528000,
81117.62
],
[
1789935534000,
81117.58
],
[
1789935540000,
81111.988333
],
[
1789935546000,
81112.675
],
[
1789935552000,
81131.768333
],
[
1789935558000,
81131.501667
],
[
1789935564000,
81129.551667
],
[
1789935570000,
81118.418333
],
[
1789935576000,
81120.615
],
[
1789935582000,
81120.415
],
[
1789935588000,
81117.451667
],
[
1789935594000,
81115.761667
],
[
1789935600000,
81115.99
],
[
1789935606000,
81118.363333
],
[
1789935612000,
81122.728333
],
[
1789935618000,
81123.318333
],
[
1789935624000,
81121.158333
],
[
1789935630000,
81119.46
],
[
1789935636000,
81119.791667
],
[
1789935642000,
81118.191667
],
[
1789935648000,
81114.616667
],
[
1789935654000,
81111.5
],
[
1789935660000,
81110.628333
],
[
1789935666000,
81109.99
],
[
1789935672000,
81109.06
],
[
1789935678000,
81113.483333
],
[
1789935684000,
81116.048333
],
[
1789935690000,
81117.225
],
[
1789935696000,
81115.436667
],
[
1789935702000,
81108.545
],
[
1789935708000,
81105.001667
],
[
1789935714000,
81104.198333
],
[
1789935720000,
81103.061667
],
[
1789935726000,
81122.996667
],
[
1789935732000,
81124.31
],
[
1789935738000,
81124.393333
],
[
1789935744000,
81130.29
],
[
1789935750000,
81132.178333
],
[
1789935756000,
81132.845
],
[
1789935762000,
81131.555
],
[
1789935768000,
81131.316667
],
[
1789935774000,
81117.736667
],
[
1789935780000,
81111.978333
],
[
1789935786000,
81111.286667
],
[
1789935792000,
81123.215
],
[
1789935798000,
81127.655
],
[
1789935804000,
81130.285
]
]

View File

@@ -0,0 +1,4 @@
{
"estimated_delivery_price": 80814.47,
"index_price": 80814.47
}

View File

@@ -0,0 +1,202 @@
[
"ada_usd",
"ada_usdc",
"ada_usdt",
"algo_usd",
"algo_usdc",
"algo_usdt",
"avax_usd",
"avax_usdc",
"avax_usdt",
"bch_usd",
"bch_usdc",
"bch_usdt",
"bnb_usd",
"bnb_usdc",
"bnb_usdt",
"bonk_usd",
"btc_usd",
"btc_usdc",
"btc_usdt",
"buidl_usd",
"buidl_usdc",
"doge_usd",
"doge_usdc",
"doge_usdt",
"dot_usd",
"dot_usdc",
"dot_usdt",
"eth_usd",
"eth_usdc",
"eth_usdt",
"ethw_usd",
"link_usd",
"link_usdc",
"link_usdt",
"ltc_usd",
"ltc_usdc",
"ltc_usdt",
"matic_usd",
"matic_usdc",
"matic_usdt",
"mog_usd",
"near_usd",
"near_usdc",
"near_usdt",
"paxg_usd",
"paxg_usdc",
"paxg_usdt",
"pepe_usd",
"shib_usd",
"shib_usdc",
"shib_usdt",
"sol_usd",
"sol_usdc",
"sol_usdt",
"sol_eth",
"ton_usd",
"ton_usdc",
"ton_usdt",
"trump_usd",
"trump_usdc",
"trump_usdt",
"trx_usd",
"trx_usdc",
"trx_usdt",
"uni_usd",
"uni_usdc",
"uni_usdt",
"xrp_usd",
"xrp_usdc",
"xrp_usdt",
"usd_usdc",
"usd_usdt",
"usdc_usd",
"usdc_usdt",
"usdt_usdc",
"usde_usd",
"usde_usdc",
"usde_usdt",
"usdt_usd",
"drbfix-btc_usdc",
"drbfix-eth_usdc",
"eth_btc",
"eth_steth",
"steth_eth",
"steth_usd",
"steth_usdc",
"steth_usdt",
"btc_eurr",
"eth_eurr",
"usdc_eur",
"usdt_eur",
"eur_usdc",
"eur_usdt",
"eurr_usd",
"eurr_usdc",
"eurr_usdt",
"eth_matic",
"btc_usyc",
"eth_usyc",
"usyc_usd",
"usyc_usdc",
"paxg_btc",
"eth_paxg",
"btc_usde",
"eth_usde",
"eth_buidl",
"hype_usd",
"hype_usdc",
"hype_usdt",
"aave_usd",
"aave_usdc",
"apt_usd",
"apt_usdc",
"arb_usd",
"arb_usdc",
"ena_usd",
"ena_usdc",
"etc_usd",
"etc_usdc",
"fil_usd",
"fil_usdc",
"hbar_usd",
"hbar_usdc",
"icp_usd",
"icp_usdc",
"inj_usd",
"inj_usdc",
"jup_usd",
"jup_usdc",
"ondo_usd",
"ondo_usdc",
"op_usd",
"op_usdc",
"render_usd",
"render_usdc",
"sei_usd",
"sei_usdc",
"sui_usd",
"sui_usdc",
"tao_usd",
"tao_usdc",
"tia_usd",
"tia_usdc",
"wld_usd",
"wld_usdc",
"xlm_usd",
"xlm_usdc",
"1000bonk_usd",
"1000bonk_usdc",
"1000mog_usd",
"1000mog_usdc",
"1000pepe_usd",
"1000pepe_usdc",
"1000shib_usd",
"1000shib_usdc",
"aero_usd",
"aero_usdc",
"atom_usd",
"atom_usdc",
"axs_usd",
"axs_usdc",
"bera_usd",
"bera_usdc",
"bome_usd",
"bome_usdc",
"brett_usd",
"brett_usdc",
"cfx_usd",
"cfx_usdc",
"crv_usd",
"crv_usdc",
"dydx_usd",
"dydx_usdc",
"eigen_usd",
"eigen_usdc",
"ethfi_usd",
"ethfi_usdc",
"fet_usd",
"fet_usdc",
"jto_usd",
"jto_usdc",
"ldo_usd",
"ldo_usdc",
"morpho_usd",
"morpho_usdc",
"pendle_usd",
"pendle_usdc",
"pengu_usd",
"pengu_usdc",
"pnut_usd",
"pnut_usdc",
"pump_usd",
"pump_usdc",
"pyth_usd",
"pyth_usdc",
"rune_usd",
"rune_usdc",
"s_usd",
"s_usdc",
"strk_usd"
]

View File

@@ -0,0 +1,35 @@
{
"max_liquidation_commission": 0.01,
"taker_commission": 0.00035,
"counter_currency": "USD",
"index_id": 1000033,
"price_index": "btc_usd",
"block_trade_min_trade_amount": 200000,
"contract_size": 10.0,
"state": "open",
"tick_size": 0.5,
"kind": "future",
"creation_timestamp": 1534242287000,
"expiration_timestamp": 32503708800000,
"is_active": true,
"instrument_type": "reversed",
"underlying_type": "crypto",
"tick_size_steps": [],
"min_trade_amount": 10.0,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"instrument_id": 210838,
"product_group": "BTC",
"lot_size": 1,
"quote_currency": "USD",
"future_type": "reversed",
"base_currency": "BTC",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1",
"block_trade_tick_size": 0.01,
"instrument_name": "BTC-PERPETUAL",
"max_leverage": 50,
"settlement_currency": "BTC",
"max_non_default_leverage": 5.0,
"block_trade_commission": 0.00025,
"settlement_period": "perpetual",
"maker_commission": 0.00015
}

View File

@@ -0,0 +1,457 @@
[
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 50,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 32503708800000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 210838,
"creation_timestamp": 1534242287000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 0.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-PERPETUAL",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "perpetual",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1789977600000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 695928,
"creation_timestamp": 1789632015000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-21SEP26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "day",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1790064000000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 696991,
"creation_timestamp": 1789718417000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-22SEP26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "day",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1790150400000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 697962,
"creation_timestamp": 1789804818000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-23SEP26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "day",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1790236800000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 698438,
"creation_timestamp": 1789891217000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-24SEP26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "day",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1790323200000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 522657,
"creation_timestamp": 1758873621000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-25SEP26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1790928000000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 691809,
"creation_timestamp": 1789027214000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-2OCT26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "week",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1791532800000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 695930,
"creation_timestamp": 1789632015000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-9OCT26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "week",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1793347200000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 667489,
"creation_timestamp": 1785398413000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-30OCT26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1795766400000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 682891,
"creation_timestamp": 1787817624000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-27NOV26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1798185600000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 567609,
"creation_timestamp": 1766736024000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-25DEC26",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1806048000000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 607780,
"creation_timestamp": 1774598426000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-26MAR27",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
},
{
"tick_size_steps": [],
"block_trade_tick_size": 0.01,
"quote_currency": "USD",
"price_index": "btc_usd",
"instrument_type": "reversed",
"future_type": "reversed",
"kind": "future",
"max_leverage": 25,
"underlying_type": "crypto",
"product_group": "BTC",
"state": "open",
"expiration_timestamp": 1813910400000,
"contract_size": 10.0,
"min_trade_amount": 10.0,
"instrument_id": 652925,
"creation_timestamp": 1782460823000,
"base_currency_uuid": "5b71fc48-3dd3-540c-809b-f8c94d0e68b5",
"is_active": true,
"lot_size": 1,
"taker_commission": 0.00035,
"tick_size": 2.5,
"block_trade_commission": 0.00025,
"settlement_currency": "BTC",
"instrument_name": "BTC-25JUN27",
"block_trade_min_trade_amount": 200000,
"max_liquidation_commission": 0.01,
"max_non_default_leverage": 5.0,
"index_id": 1000033,
"counter_currency": "USD",
"base_currency": "BTC",
"maker_commission": 0.00015,
"settlement_period": "month",
"quote_currency_uuid": "dc311642-e9fb-4ba9-a312-c75c1aef4ae1"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,452 @@
{
"BTC-21SEP26-68000-C": {
"timestamp": 1789939517455,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 1.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.82154
},
"index_price": 80808.14,
"instrument_name": "BTC-21SEP26-68000-C",
"last_price": null,
"settlement_price": 0.15328061,
"min_price": 0.1285,
"max_price": 0.1865,
"open_interest": 0.0,
"mark_price": 0.1585,
"interest_rate": 0.0,
"estimated_delivery_price": 80808.14,
"best_ask_price": 0.161,
"best_bid_price": 0.1565,
"mark_iv": 61.59,
"bid_iv": 0.0,
"ask_iv": 315.62,
"underlying_price": 80809.22,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 56.0,
"best_bid_amount": 56.0
},
"BTC-21SEP26-68000-P": {
"timestamp": 1789939517455,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 0.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.0
},
"index_price": 80808.14,
"instrument_name": "BTC-21SEP26-68000-P",
"last_price": 0.0001,
"settlement_price": 0.0,
"min_price": 0.0001,
"max_price": 0.015,
"open_interest": 0.5,
"mark_price": 0.0,
"interest_rate": 0.0,
"estimated_delivery_price": 80808.14,
"best_ask_price": 0.0001,
"best_bid_price": 0.0,
"mark_iv": 61.59,
"bid_iv": 0.0,
"ask_iv": 193.39,
"underlying_price": 80809.22,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 20.0,
"best_bid_amount": 0.0
},
"BTC-21SEP26-69000-C": {
"timestamp": 1789939517455,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 1.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.83362
},
"index_price": 80808.14,
"instrument_name": "BTC-21SEP26-69000-C",
"last_price": null,
"settlement_price": 0.14082886,
"min_price": 0.1155,
"max_price": 0.175,
"open_interest": 0.0,
"mark_price": 0.1461,
"interest_rate": 0.0,
"estimated_delivery_price": 80808.14,
"best_ask_price": 0.148,
"best_bid_price": 0.144,
"mark_iv": 61.57,
"bid_iv": 0.0,
"ask_iv": 276.84,
"underlying_price": 80808.56,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 56.0,
"best_bid_amount": 56.0
},
"BTC-21SEP26-69000-P": {
"timestamp": 1789939517455,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 0.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.0
},
"index_price": 80808.14,
"instrument_name": "BTC-21SEP26-69000-P",
"last_price": null,
"settlement_price": 0.0,
"min_price": 0.0001,
"max_price": 0.015,
"open_interest": 0.0,
"mark_price": 0.0,
"interest_rate": 0.0,
"estimated_delivery_price": 80808.14,
"best_ask_price": 0.0001,
"best_bid_price": 0.0,
"mark_iv": 61.57,
"bid_iv": 0.0,
"ask_iv": 178.62,
"underlying_price": 80809.22,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 18.5,
"best_bid_amount": 0.0
},
"BTC-21SEP26-70000-C": {
"timestamp": 1789939517455,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 1.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.8457
},
"index_price": 80808.14,
"instrument_name": "BTC-21SEP26-70000-C",
"last_price": null,
"settlement_price": 0.12837712,
"min_price": 0.1025,
"max_price": 0.163,
"open_interest": 0.0,
"mark_price": 0.1338,
"interest_rate": 0.0,
"estimated_delivery_price": 80808.14,
"best_ask_price": 0.1355,
"best_bid_price": 0.1315,
"mark_iv": 61.57,
"bid_iv": 0.0,
"ask_iv": 252.4,
"underlying_price": 80809.22,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 56.0,
"best_bid_amount": 56.0
},
"BTC-21SEP26-70000-P": {
"timestamp": 1789939518462,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0
},
"greeks": {
"delta": 0.0,
"gamma": 0.0,
"vega": 0.0,
"theta": 0.0,
"rho": 0.0
},
"index_price": 80807.52,
"instrument_name": "BTC-21SEP26-70000-P",
"last_price": 0.0001,
"settlement_price": 2e-08,
"min_price": 0.0001,
"max_price": 0.015,
"open_interest": 22.1,
"mark_price": 0.0,
"interest_rate": 0.0,
"estimated_delivery_price": 80807.52,
"best_ask_price": 0.0001,
"best_bid_price": 0.0,
"mark_iv": 61.57,
"bid_iv": 0.0,
"ask_iv": 163.96,
"underlying_price": 80808.75,
"underlying_index": "BTC-21SEP26",
"best_ask_amount": 27.9,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d25-C": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": 0.99603,
"gamma": 0.19661,
"vega": 1e-05,
"theta": -0.00035,
"rho": 2e-05
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d25-C",
"last_price": null,
"settlement_price": 0.13044156,
"min_price": 0.0655,
"max_price": 0.238,
"open_interest": 0.0,
"mark_price": 0.1491,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.0,
"best_bid_price": 0.0,
"mark_iv": 123.13,
"bid_iv": 0.0,
"ask_iv": 0.0,
"underlying_price": 1.3991,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 0.0,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d25-P": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": -0.00395,
"gamma": 0.19572,
"vega": 1e-05,
"theta": -7e-05,
"rho": 0.0
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d25-P",
"last_price": null,
"settlement_price": 0.00024156,
"min_price": 0.0005,
"max_price": 0.0335,
"open_interest": 0.0,
"mark_price": 0.0001,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.0025,
"best_bid_price": 0.0,
"mark_iv": 123.13,
"bid_iv": 0.0,
"ask_iv": 209.07,
"underlying_price": 1.3992,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 25000.0,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d28-C": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": 0.98222,
"gamma": 0.7315,
"vega": 2e-05,
"theta": -0.00131,
"rho": 2e-05
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d28-C",
"last_price": null,
"settlement_price": 0.10089075,
"min_price": 0.0355,
"max_price": 0.211,
"open_interest": 0.0,
"mark_price": 0.1194,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.0,
"best_bid_price": 0.0,
"mark_iv": 123.13,
"bid_iv": 0.0,
"ask_iv": 0.0,
"underlying_price": 1.3992,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 0.0,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d28-P": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": -0.01778,
"gamma": 0.7315,
"vega": 2e-05,
"theta": -0.00039,
"rho": 0.0
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d28-P",
"last_price": null,
"settlement_price": 0.00069075,
"min_price": 0.0005,
"max_price": 0.045,
"open_interest": 0.0,
"mark_price": 0.0004,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.0025,
"best_bid_price": 0.0,
"mark_iv": 123.13,
"bid_iv": 0.0,
"ask_iv": 173.59,
"underlying_price": 1.3992,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 25000.0,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d3-C": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": 0.96884,
"gamma": 1.25842,
"vega": 3e-05,
"theta": -0.00196,
"rho": 2e-05
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d3-C",
"last_price": null,
"settlement_price": 0.08169251,
"min_price": 0.0155,
"max_price": 0.1915,
"open_interest": 0.0,
"mark_price": 0.0997,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.0,
"best_bid_price": 0.0,
"mark_iv": 114.73,
"bid_iv": 0.0,
"ask_iv": 0.0,
"underlying_price": 1.3992,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 0.0,
"best_bid_amount": 0.0
},
"XRP_USDC-21SEP26-1d3-P": {
"timestamp": 1789939517605,
"state": "open",
"stats": {
"high": null,
"low": null,
"price_change": null,
"volume": 0.0,
"volume_usd": 0.0,
"volume_notional": 0.0
},
"greeks": {
"delta": -0.03116,
"gamma": 1.25842,
"vega": 3e-05,
"theta": -0.00069,
"rho": 0.0
},
"index_price": 1.3987,
"instrument_name": "XRP_USDC-21SEP26-1d3-P",
"last_price": null,
"settlement_price": 0.00149251,
"min_price": 0.0005,
"max_price": 0.05,
"open_interest": 0.0,
"mark_price": 0.0007,
"interest_rate": 0.0,
"estimated_delivery_price": 1.3987,
"best_ask_price": 0.001,
"best_bid_price": 0.0005,
"mark_iv": 114.73,
"bid_iv": 108.74,
"ask_iv": 122.91,
"underlying_price": 1.3992,
"underlying_index": "XRP_USDC-21SEP26",
"best_ask_amount": 25000.0,
"best_bid_amount": 25000.0
}
}

View File

@@ -0,0 +1,73 @@
{
"timestamp": 1789939036574,
"state": "open",
"stats": {
"high": 81497.5,
"low": 80149.5,
"price_change": -0.2493,
"volume": 3099.91787897,
"volume_usd": 250336360.0,
"volume_notional": 250336360.0
},
"change_id": 174959286421,
"index_price": 80815.66,
"instrument_name": "BTC-PERPETUAL",
"bids": [
[
80836.5,
65010.0
],
[
80832.0,
6510.0
],
[
80831.0,
15520.0
],
[
80827.0,
1470.0
],
[
80826.0,
4460.0
]
],
"asks": [
[
80837.0,
10810.0
],
[
80837.5,
10000.0
],
[
80838.0,
3350.0
],
[
80838.5,
6000.0
],
[
80840.0,
6000.0
]
],
"last_price": 80832.5,
"settlement_price": 80317.84,
"min_price": 79618.5,
"max_price": 82044.0,
"open_interest": 787799210,
"mark_price": 80831.35,
"interest_value": 0.024991800794000794,
"current_funding": 0.0,
"estimated_delivery_price": 80815.66,
"funding_8h": 1.583e-05,
"best_ask_price": 80837.0,
"best_bid_price": 80836.5,
"best_ask_amount": 10810.0,
"best_bid_amount": 65010.0
}

View File

@@ -0,0 +1 @@
1789939041264

View File

@@ -0,0 +1,55 @@
{
"continuation": "3JbvTsVzTGQ6HW7KSsh78zQ4sZ8tMD9vrhkwQNR7TJTVHbBswFK6j9yJdsz8vwZTgaYJihMaRyXn9",
"settlements": [
{
"position": 31999610,
"timestamp": 1789891200065,
"type": "delivery",
"index_price": 80321.21,
"instrument_name": "BTC-20SEP26",
"mark_price": 80321.21,
"profit_loss": -3.345250995000001,
"session_profit_loss": 23493.117937861054
},
{
"position": 17.8,
"timestamp": 1789891200065,
"type": "exercise",
"index_price": 80321.21,
"instrument_name": "BTC-20SEP26-68000-P",
"mark_price": 0.0,
"profit_loss": -8.673617379884035e-19,
"session_profit_loss": 492.86261408100006
},
{
"position": 88.8,
"timestamp": 1789891200065,
"type": "exercise",
"index_price": 80321.21,
"instrument_name": "BTC-20SEP26-69000-P",
"mark_price": 0.0,
"profit_loss": 0.009744748999999999,
"session_profit_loss": 1642.87538027
},
{
"position": 65.2,
"timestamp": 1789891200065,
"type": "exercise",
"index_price": 80321.21,
"instrument_name": "BTC-20SEP26-70000-P",
"mark_price": 0.0,
"profit_loss": 0.005033333,
"session_profit_loss": 1642.87538027
},
{
"position": 8.599999999999998,
"timestamp": 1789891200065,
"type": "exercise",
"index_price": 80321.21,
"instrument_name": "BTC-20SEP26-71000-P",
"mark_price": 0.0,
"profit_loss": -0.004930000000000005,
"session_profit_loss": 1807.162918297
}
]
}

View File

@@ -0,0 +1,55 @@
{
"continuation": "23nYDkVbSynLKHS9FLtdNzkdZehx1XPceCZ2K54nzc3PhhBDfRmKbxsg59v47UKDhQL7c",
"settlements": [
{
"position": 778628500,
"timestamp": 1789891200065,
"type": "settlement",
"index_price": 80292.87,
"instrument_name": "BTC-PERPETUAL",
"mark_price": 80317.84,
"profit_loss": -2.882714252999952,
"session_profit_loss": 366525.4973382353
},
{
"position": 778159550,
"timestamp": 1789804800083,
"type": "settlement",
"index_price": 81078.23,
"instrument_name": "BTC-PERPETUAL",
"mark_price": 81109.92,
"profit_loss": -15.834744451000045,
"session_profit_loss": 1564451.0500825022
},
{
"position": 778769380,
"timestamp": 1789718400096,
"type": "settlement",
"index_price": 77738.19,
"instrument_name": "BTC-PERPETUAL",
"mark_price": 77762.07,
"profit_loss": 0.33698070499993793,
"session_profit_loss": 660354.2430167407
},
{
"position": 796354170,
"timestamp": 1789632000061,
"type": "settlement",
"index_price": 76391.01,
"instrument_name": "BTC-PERPETUAL",
"mark_price": 76418.56,
"profit_loss": 1.8330351599999015,
"session_profit_loss": 357003.47944851726
},
{
"position": 794478700,
"timestamp": 1789545600759,
"type": "settlement",
"index_price": 75684.39,
"instrument_name": "BTC-PERPETUAL",
"mark_price": 75709.26,
"profit_loss": -0.049004756999928276,
"session_profit_loss": 676031.4583834042
}
]
}

View File

@@ -0,0 +1,3 @@
{
"locked": "false"
}

View File

@@ -0,0 +1,192 @@
[
"aave_usdc",
"ada_usdc",
"aero_usdc",
"algo_usdc",
"apt_usdc",
"arb_usdc",
"atom_usdc",
"avax_usdc",
"bch_usdc",
"bnb_usdc",
"bonk_usdc",
"btc_eurr",
"btc_paxg",
"btc_usdc",
"btc_usde",
"btc_usdt",
"btc_usyc",
"buidl_usdc",
"cbeth_usdc",
"crv_usdc",
"doge_usdc",
"dot_usdc",
"eth_btc",
"eth_eurr",
"eth_paxg",
"eth_usdc",
"eth_usde",
"eth_usdt",
"eth_usyc",
"eurc_usdc",
"eurr_usdc",
"eurr_usdt",
"fartcoin_usdc",
"fil_usdc",
"hbar_usdc",
"icp_usdc",
"inj_usdc",
"jasmy_usdc",
"link_usdc",
"ltc_usdc",
"matic_usdc",
"near_usdc",
"ondo_usdc",
"op_usdc",
"paxg_btc",
"paxg_eth",
"paxg_usdc",
"pengu_usdc",
"pepe_usdc",
"pump_usdc",
"shib_usdc",
"sol_eth",
"sol_usdc",
"sol_usdt",
"steth_eth",
"steth_usdc",
"steth_usdt",
"sui_usdc",
"tao_usdc",
"uni_usdc",
"usdc_usdt",
"usde_usdc",
"usde_usdt",
"usdt_usdc",
"usyc_usdc",
"vet_usdc",
"xlm_usdc",
"xrp_usdc",
"zora_usdc",
"btc_usd",
"eth_usd",
"1000bonk_usdc",
"1000mog_usdc",
"1000pepe_usdc",
"1000shib_usdc",
"aapl_usdc",
"alt_usdc",
"amd_usdc",
"amzn_usdc",
"anthropic_usdc",
"ape_usdc",
"arm_usdc",
"aster_usdc",
"avnt_usdc",
"axs_usdc",
"based1_usdc",
"be_usdc",
"bera_usdc",
"bill_usdc",
"bome_usdc",
"brentoil_usdc",
"brett_usdc",
"btcdvol_usdc",
"cbrs_usdc",
"cfx_usdc",
"chip_usdc",
"coin50_usdc",
"comp_usdc",
"crcl_usdc",
"dash_usdc",
"dram_usdc",
"dydx_usdc",
"edgex_usdc",
"eigen_usdc",
"ena_usdc",
"etc_usdc",
"ethdvol_usdc",
"ethfi_usdc",
"ewy_usdc",
"fet_usdc",
"gold_usdc",
"googl_usdc",
"hood_usdc",
"hype_usdc",
"intc_usdc",
"inx_usdc",
"jto_usdc",
"jup_usdc",
"ldo_usdc",
"lit_usdc",
"lite_usdc",
"mega_usdc",
"merl_usdc",
"meta_usdc",
"mog_usdc",
"mon_usdc",
"morpho_usdc",
"mrvl_usdc",
"msft_usdc",
"mstr_usdc",
"mu_usdc",
"nbis_usdc",
"night_usdc",
"nvda_usdc",
"openai_usdc",
"opn_usdc",
"pendle_usdc",
"pnut_usdc",
"prl_usdc",
"pyth_usdc",
"qqq_usdc",
"render_usdc",
"resolv_usdc",
"robo_usdc",
"rune_usdc",
"s_usdc",
"saga_usdc",
"sei_usdc",
"silver_usdc",
"skhy_usdc",
"sndk_usdc",
"soxl_usdc",
"spcx_usdc",
"spy_usdc",
"strk_usdc",
"tia_usdc",
"ton_usdc",
"towns_usdc",
"trump_usdc",
"trx_usdc",
"tsla_usdc",
"tsm_usdc",
"virtual_usdc",
"vvv_usdc",
"w_usdc",
"wif_usdc",
"wld_usdc",
"wlfi_usdc",
"wtioil_usdc",
"xpl_usdc",
"zec_usdc",
"ada_usdt",
"algo_usdt",
"avax_usdt",
"bch_usdt",
"bnb_usdt",
"doge_usdt",
"dot_usdt",
"hype_usdt",
"link_usdt",
"ltc_usdt",
"matic_usdt",
"near_usdt",
"paxg_usdt",
"shib_usdt",
"ton_usdt",
"trump_usdt",
"trx_usdt",
"uni_usdt",
"xrp_usdt"
]

View File

@@ -0,0 +1,28 @@
{
"timestamp": 1789939037426,
"state": "open",
"stats": {
"high": 81497.5,
"low": 80149.5,
"price_change": -0.2487,
"volume": 3100.02328212,
"volume_usd": 250344880.0,
"volume_notional": 250344880.0
},
"index_price": 80814.49,
"instrument_name": "BTC-PERPETUAL",
"last_price": 80832.5,
"settlement_price": 80317.84,
"min_price": 79617.5,
"max_price": 82043.0,
"open_interest": 787799210,
"mark_price": 80830.37,
"interest_value": 0.024991800794000794,
"current_funding": 0.0,
"estimated_delivery_price": 80814.49,
"funding_8h": 1.583e-05,
"best_ask_price": 80837.0,
"best_bid_price": 80836.5,
"best_ask_amount": 2520.0,
"best_bid_amount": 65010.0
}

View File

@@ -0,0 +1,313 @@
[
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "AAVE",
"currency_pair": "aave_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "ADA",
"currency_pair": "ada_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "AERO",
"currency_pair": "aero_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "ALGO",
"currency_pair": "algo_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "APT",
"currency_pair": "apt_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "ARB",
"currency_pair": "arb_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "ATOM",
"currency_pair": "atom_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "AVAX",
"currency_pair": "avax_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "BCH",
"currency_pair": "bch_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "BNB",
"currency_pair": "bnb_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "BONK",
"currency_pair": "bonk_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 3598.5,
"calls_volume_30d": 469702.8,
"calls_volume_7d": 126410.1,
"currency": "BTC",
"currency_pair": "btc_usd",
"futures_volume": 3324.25470988,
"futures_volume_30d": 176985.7839283,
"futures_volume_7d": 42302.8948902,
"puts_volume": 2512.5,
"puts_volume_30d": 276401.6,
"puts_volume_7d": 65783.1
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "BUIDL",
"currency_pair": "buidl_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 58799.0,
"spot_volume_7d": 16200.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "CBETH",
"currency_pair": "cbeth_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "CRV",
"currency_pair": "crv_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "DOGE",
"currency_pair": "doge_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "DOT",
"currency_pair": "dot_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 37094.0,
"calls_volume_30d": 2042905.0,
"calls_volume_7d": 697174.0,
"currency": "ETH",
"currency_pair": "eth_usd",
"futures_volume": 46811.902868,
"futures_volume_30d": 1743490.936324,
"futures_volume_7d": 524179.809436,
"puts_volume": 31091.0,
"puts_volume_30d": 1450096.0,
"puts_volume_7d": 399976.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "EURC",
"currency_pair": "eurc_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
},
{
"calls_volume": 0.0,
"calls_volume_30d": 0.0,
"calls_volume_7d": 0.0,
"currency": "EURR",
"currency_pair": "eurr_usd",
"futures_volume": 0.0,
"futures_volume_30d": 0.0,
"futures_volume_7d": 0.0,
"puts_volume": 0.0,
"puts_volume_30d": 0.0,
"puts_volume_7d": 0.0,
"spot_volume": 0.0,
"spot_volume_30d": 0.0,
"spot_volume_7d": 0.0
}
]

View File

@@ -0,0 +1,85 @@
{
"trades": [
{
"timestamp": 1789939017544,
"iv": 31.75,
"price": 0.0011,
"amount": 0.1,
"direction": "sell",
"index_price": 80805.64,
"instrument_name": "BTC-21SEP26-80000-P",
"trade_seq": 134,
"mark_price": 0.00102437,
"tick_direction": 2,
"starbase_match_id": 227538549002481664,
"trade_id": "445682190",
"contracts": 0.1,
"starbase_timestamp": 1789939017544100459
},
{
"timestamp": 1789939015485,
"iv": 34.23,
"price": 0.0265,
"amount": 1.6,
"direction": "buy",
"index_price": 80796.05,
"instrument_name": "BTC-30OCT26-85000-C",
"trade_seq": 660,
"mark_price": 0.02644517,
"tick_direction": 2,
"starbase_match_id": 227538540366409728,
"trade_id": "445682189",
"contracts": 1.6,
"starbase_timestamp": 1789939015485340364
},
{
"timestamp": 1789939009041,
"iv": 56.3,
"price": 0.0001,
"amount": 0.1,
"direction": "buy",
"index_price": 80791.09,
"instrument_name": "BTC-25SEP26-95000-C",
"trade_seq": 2213,
"mark_price": 0.00012879,
"tick_direction": 3,
"starbase_match_id": 227538513338314753,
"trade_id": "445682181",
"contracts": 0.1,
"starbase_timestamp": 1789939009041727430
},
{
"timestamp": 1789938977723,
"iv": 34.72,
"price": 0.006,
"amount": 0.1,
"direction": "buy",
"index_price": 80784.57,
"instrument_name": "BTC-9OCT26-88000-C",
"trade_seq": 103,
"mark_price": 0.0060825,
"tick_direction": 3,
"starbase_match_id": 227538381981102081,
"trade_id": "445682157",
"contracts": 0.1,
"starbase_timestamp": 1789938977723298197
},
{
"timestamp": 1789938977723,
"iv": 34.72,
"price": 0.006,
"amount": 0.1,
"direction": "buy",
"index_price": 80784.57,
"instrument_name": "BTC-9OCT26-88000-C",
"trade_seq": 102,
"mark_price": 0.0060825,
"tick_direction": 3,
"starbase_match_id": 227538381981102080,
"trade_id": "445682156",
"contracts": 0.1,
"starbase_timestamp": 1789938977723298197
}
],
"has_more": true
}

View File

@@ -0,0 +1,80 @@
{
"trades": [
{
"timestamp": 1789939035856,
"price": 80832.5,
"amount": 6000.0,
"direction": "buy",
"index_price": 80815.88,
"instrument_name": "BTC-PERPETUAL",
"trade_seq": 299983429,
"mark_price": 80831.54,
"tick_direction": 1,
"starbase_match_id": 227538625808576513,
"trade_id": "445682210",
"contracts": 600.0,
"starbase_timestamp": 1789939035856973859
},
{
"timestamp": 1789939035856,
"price": 80832.5,
"amount": 2520.0,
"direction": "buy",
"index_price": 80815.88,
"instrument_name": "BTC-PERPETUAL",
"trade_seq": 299983428,
"mark_price": 80831.54,
"tick_direction": 0,
"starbase_match_id": 227538625808576512,
"trade_id": "445682209",
"contracts": 252.0,
"starbase_timestamp": 1789939035856973859
},
{
"timestamp": 1789939032782,
"price": 80832.0,
"amount": 2660.0,
"direction": "sell",
"index_price": 80815.6,
"instrument_name": "BTC-PERPETUAL",
"trade_seq": 299983427,
"mark_price": 80831.17,
"tick_direction": 0,
"starbase_match_id": 227538612915286016,
"trade_id": "445682208",
"contracts": 266.0,
"starbase_timestamp": 1789939032782921844
},
{
"timestamp": 1789939029560,
"price": 80829.0,
"amount": 2520.0,
"direction": "buy",
"index_price": 80813.03,
"instrument_name": "BTC-PERPETUAL",
"trade_seq": 299983426,
"mark_price": 80828.35,
"tick_direction": 0,
"starbase_match_id": 227538599401238528,
"trade_id": "445682207",
"contracts": 252.0,
"starbase_timestamp": 1789939029560823168
},
{
"timestamp": 1789939027344,
"price": 80824.5,
"amount": 2520.0,
"direction": "buy",
"index_price": 80815.27,
"instrument_name": "BTC-PERPETUAL",
"trade_seq": 299983425,
"mark_price": 80830.58,
"tick_direction": 1,
"starbase_match_id": 227538590106660864,
"trade_id": "445682206",
"contracts": 252.0,
"starbase_timestamp": 1789939027344529585
}
],
"has_more": true
}

Some files were not shown because too many files have changed in this diff Show More