Webull, behind the DpExchange facade.
⚠️ EXPERIMENTAL
This package has not run in production. While it is 0.x the API may change without a
major version — pin all three segments. Maturity is declared per endpoint through
capabilities/0; do not read this banner as your check.
This module is the entire public API of this package. Transport, signing, session handling and supervision are internal, and nothing here returns them.
Credentials are required for market data, which is unusual
Every Webull OpenAPI call is signed — including the ones that look public. There is no
anonymous quote endpoint. capabilities/0 declares credential_benefit: :required, the
first venue in this family to do so, and get_price/2 takes credentials that the same
call on other venues does not need.
The host still owns authentication: it holds the App Key and secret and passes them in. This package signs one request with them and keeps nothing.
Two transports, and you see neither
Market data arrives over MQTT 3.1.1 carried on a WebSocket; subscriptions are
HTTP calls; the two are joined by a session identifier this package generates. You
call subscribe/2 with symbols.
That matters beyond tidiness. The venue does not restore subscriptions after a reconnect — this package replays them. A consumer doing that itself would have to notice reconnects, which is exactly what the facade exists to hide.
The UAT environment has REST but no stream
environment: :uat gives authenticated REST against test data. It has no broker —
mqtt-uat.webullbroker.com does not resolve — so subscribe/2 there returns
{:error, {:streaming_unavailable, :uat}} rather than quietly falling back to
production. A consumer testing against UAT that received production prices would be
reading real market data believing it was fake.
No trade volume, anywhere
Webull's crypto OpenAPI reports no volume: not on bars, not on the snapshot, not on the
stream. volume is nil, never 0, and capabilities/0 says
reports_trade_volume: false so volume-dependent work can be routed elsewhere rather
than reading a column of zeroes.
Supervision
children = [{DpExchange.Webull, []}]
{:ok, quote} = DpExchange.Webull.get_price("BTC-USD", credentials: my_credentials())
Summary
Functions
The quote currencies this venue settles in.
Whether this environment carries a live stream.
Functions
@spec quotes() :: [String.t()]
The quote currencies this venue settles in.
Whether this environment carries a live stream.
Exposed because the answer differs by environment on this venue and a consumer should be able to ask before it commits, rather than learning from a subscription that never delivers.