Files
OpenBB/openbb_platform/providers/deribit/tests/test_options_chains.py
Danglewood 869cfed125 [Release] openbb-v5.0.0 (#7741)
* merge branch develop with main

* bump desktop version

* fix the merge

* sync Cargo.lock to desktop 2.0.0

* fix cli conftest.py

---------

Co-authored-by: deeleeramone <>
2026-09-29 10:58:44 -07:00

194 lines
7.0 KiB
Python

"""Tests for the Deribit options chain."""
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_deribit.models.options_chains import DeribitOptionsChainsFetcher
@pytest.fixture
def chain(monkeypatch, responder, load):
"""Answer the instrument listing and the subscription from fixtures."""
def install(root, messages=(), tickers=None):
responder({"get_instruments": load("instruments_option")})
stored = load("option_tickers") if tickers is None else tickers
wanted = {
name: ticker
for name, ticker in stored.items()
if name.startswith(f"{root}-")
}
async def _subscribe(symbols, collected):
collected.update(messages)
return {name: wanted[name] for name in symbols if name in wanted}
monkeypatch.setattr(
"openbb_deribit.utils.websocket.subscribe_tickers", _subscribe
)
return wanted
return install
class TestChain:
"""A chain carries one row per contract, with greeks."""
@pytest.mark.asyncio
async def test_rows(self, chain):
"""Each contract quoted lands on the chain, sorted."""
quoted = chain("XRP_USDC")
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
rows = result.model_dump()
assert len(rows) == len(quoted)
assert [
(row["expiration"], row["strike"], row["option_type"]) for row in rows
] == sorted(
(row["expiration"], row["strike"], row["option_type"]) for row in rows
)
assert all(row["delta"] is not None for row in rows)
@pytest.mark.asyncio
async def test_percentages_stay_in_percent_units(self, chain):
"""A volatility is kept as the exchange publishes it."""
chain("XRP_USDC")
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
quoted = [
row["implied_volatility"]
for row in result.model_dump()
if row["implied_volatility"]
]
assert quoted
assert all(value > 1 for value in quoted)
@pytest.mark.asyncio
async def test_linear_prices_are_left_alone(self, chain):
"""A USDC-quoted contract keeps the price the exchange published."""
quoted = chain("XRP_USDC")
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
rows = {row["contract_symbol"]: row for row in result.model_dump()}
name = next(iter(quoted))
assert rows[name]["mark"] == quoted[name]["mark_price"]
@pytest.mark.asyncio
async def test_inverse_prices_are_carried_to_usd(self, chain):
"""A BTC-quoted contract's premium is carried at the index level."""
quoted = chain("BTC")
result = await DeribitOptionsChainsFetcher.fetch_data({"symbol": "BTC"}, {})
rows = {row["contract_symbol"]: row for row in result.model_dump()}
name = next(iter(quoted))
published = quoted[name]
assert rows[name]["mark"] == (
published["mark_price"] * published["index_price"]
)
@pytest.mark.asyncio
async def test_no_rounding(self, chain):
"""The carried premium keeps every digit the arithmetic produced."""
quoted = chain("BTC")
result = await DeribitOptionsChainsFetcher.fetch_data({"symbol": "BTC"}, {})
rows = {row["contract_symbol"]: row for row in result.model_dump()}
carried = [
rows[name]["mark"] for name in quoted if rows[name]["mark"] is not None
]
assert any(round(value, 2) != value for value in carried)
@pytest.mark.asyncio
async def test_decimal_strikes(self, chain):
"""A strike the exchange spells with a ``d`` reads as a decimal."""
chain("XRP_USDC")
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
assert any(row["strike"] % 1 for row in result.model_dump())
@pytest.mark.asyncio
async def test_warns_but_returns(self, chain):
"""A partial subscription warns and returns what did arrive."""
chain("XRP_USDC", messages=("half the chain timed out",))
with pytest.warns(UserWarning, match="timed out"):
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
assert result.model_dump()
@pytest.mark.asyncio
async def test_raises_when_everything_failed(self, chain):
"""A subscription that returned nothing at all raises its reason."""
chain("XRP_USDC", messages=("the subscription failed",), tickers={})
with pytest.raises(OpenBBError, match="subscription failed"):
await DeribitOptionsChainsFetcher.fetch_data({"symbol": "XRP_USDC"}, {})
@pytest.mark.asyncio
async def test_empty_without_a_reason(self, chain):
"""A silent empty subscription is an empty-data error."""
chain("XRP_USDC", tickers={})
with pytest.raises(EmptyDataError):
await DeribitOptionsChainsFetcher.fetch_data({"symbol": "XRP_USDC"}, {})
@pytest.mark.asyncio
async def test_a_failed_expiration_is_skipped(self, monkeypatch, responder, load):
"""An expiration whose subscription raised is left out, not fatal."""
responder({"get_instruments": load("instruments_option")})
stored = load("option_tickers")
calls: list = []
async def _subscribe(symbols, collected):
calls.append(symbols)
if not any(name in stored for name in symbols):
raise RuntimeError("the socket went away")
return {name: stored[name] for name in symbols if name in stored}
monkeypatch.setattr(
"openbb_deribit.utils.websocket.subscribe_tickers", _subscribe
)
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
assert result.model_dump()
@pytest.mark.asyncio
async def test_unlisted_underlying(self, responder, load):
"""An underlying with no options names the ones that have them."""
responder({"get_instruments": load("instruments_option")})
with pytest.raises(OpenBBError, match="XRP_USDC"):
await DeribitOptionsChainsFetcher.fetch_data({"symbol": "DOGE"}, {})
@pytest.mark.asyncio
async def test_index_underlying_is_named(self, chain, load):
"""A contract priced off the index names the index, not a placeholder."""
stored = load("option_tickers")
for ticker in stored.values():
ticker["underlying_index"] = "index_price"
chain("XRP_USDC", tickers=stored)
result = await DeribitOptionsChainsFetcher.fetch_data(
{"symbol": "XRP_USDC"}, {}
)
assert {row["underlying_symbol"] for row in result.model_dump()} == {"XRP-USDC"}