DpExchange.Webull.Rest (DpExchangeWebull v0.1.1)

Copy Markdown View Source

Webull's OpenAPI REST surface — internal.

Every call is signed, including the public-looking ones

There is no anonymous path here. /openapi/market-data/crypto/snapshot needs the same App Key and signature as an order. That is why this venue declares credential_benefit: :required — the first in the family to do so — and why get_price/2 takes credentials that other venues' get_price/2 does not need.

A consumer branching on capabilities/0 learns this before it calls; a consumer that assumed market data is free learns it from a 401.

Bars nest one level down, and assuming otherwise returns nothing

The crypto-bars response is a list of groups, each carrying its rows under "result":

[%{"instrument_id" => , "symbol" => , "result" => [%{"open" => }, ]}]

The adapter this came from once mapped its row decoder over the group objects. Groups have no "open", "close" or "time", so every field resolved to nil — the call returned a list of all-nil bars, and the backfill logged an empty result for every crypto pair. It looked like the venue had no data.

Both shapes are handled: a group with "result" is flattened, and a flat bar decodes directly, in case the equities path or a future change sends one.

No volume, anywhere

Webull's crypto OpenAPI exposes no trade volume — not on the bars, not on the snapshot, not on the MQTT stream. volume is nil rather than 0, because zero is a volume and this venue is not reporting one. capabilities/0 declares reports_trade_volume: false so a consumer can route volume-dependent work elsewhere rather than discovering a column of zeroes.

Summary

Functions

OHLC bars for a symbol and canonical timeframe.

Last price for one symbol, from the crypto snapshot endpoint.

Every crypto symbol the venue lists, canonical.

Canonical timeframes this venue serves, shortest first.

Functions

get_historical_prices(symbol, timeframe, range, credentials, opts)

@spec get_historical_prices(String.t(), String.t(), keyword(), map(), keyword()) ::
  {:ok, [map()]} | {:error, term()} | {:refused, term()}

OHLC bars for a symbol and canonical timeframe.

Bars carry no volume — see the module doc. A bar without a venue timestamp is an error, not a bar stamped with the local clock.

get_price(symbol, credentials, opts)

@spec get_price(String.t(), map(), keyword()) ::
  {:ok, DpExchange.Core.Types.Quote.t()} | {:error, term()} | {:refused, term()}

Last price for one symbol, from the crypto snapshot endpoint.

get_symbols(credentials, opts)

@spec get_symbols(
  map(),
  keyword()
) :: {:ok, [String.t()]} | {:error, term()} | {:refused, term()}

Every crypto symbol the venue lists, canonical.

Measured 2026-08-05 against /openapi/instrument/crypto/list: 342 symbols, every one quoted in USD.

timeframes()

@spec timeframes() :: [String.t()]

Canonical timeframes this venue serves, shortest first.