Packages
dp_exchange_webull
0.1.6
0.4.59
0.4.58
0.4.57
0.4.56
0.4.55
0.4.54
0.4.53
0.4.52
0.4.51
0.4.50
0.4.49
0.4.48
0.4.47
0.4.46
0.4.45
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.31
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.3.3
0.3.2
0.3.1
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
EXPERIMENTAL — Webull venue package for the DpExchange family. Market data, trading and streaming behind the shared DpExchange.Core.Venue facade.
Current section
Files
Jump to
Current section
Files
dp_exchange_webull
usage-rules.md
usage-rules.md
# Using `dp_exchange_webull`
> **EXPERIMENTAL.** Not run in production. Pin three-part. Maturity is per endpoint —
> read `capabilities/0`, not this banner.
Everything general is in
[`dp_exchange_core`'s usage rules](https://hexdocs.pm/dp_exchange_core/usage-rules.html).
This file is only what is **specific to Webull**.
## Credentials are required for market data
There is no anonymous endpoint on this venue. Every OpenAPI call is signed, including the
ones that look public, so `get_price/2` needs credentials that the same call on Coinbase or
Gemini does not:
```elixir
{:ok, quote} = DpExchange.Webull.get_price("BTC-USD", credentials: %{
app_key: "…", app_secret: "…"
})
```
`capabilities/0` declares `credential_benefit: :required` — the only venue in the family
that does. Branch on that rather than assuming market data is free; the alternative is
finding out from a 401.
You keep the credentials. This package signs one request with them and holds nothing.
## Start it, and it brings its own rate limiter
```elixir
children = [{DpExchange.Webull, credentials: my_credentials()}]
```
Passing credentials at start is what lets the package **replay your subscriptions after a
reconnect** — see below. Without them, a reconnect cannot re-subscribe.
## Subscribing is two protocols, and you see neither
Market data arrives over MQTT on a WebSocket. Subscriptions are HTTP calls. They are joined
by a session identifier this package generates and gives to both.
```elixir
:ok = DpExchange.Webull.subscribe(["BTC-USD"], credentials: creds, to: self())
```
**The venue does not restore subscriptions after a reconnect.** This package replays them
for you, which is the whole reason you never have to notice a reconnect. That replay uses
the credentials you supplied — at start, or on the subscribe call.
### Coverage means delivering, not accepted
On this venue there are three different moments: you asked, the HTTP subscribe returned
200, and data is arriving. `coverage/1` reports only the third. A 200 on the subscribe does
not mean the stream is flowing.
## The venue's connection budget shapes what you can ask for
| Constraint | Value |
|---|---|
| Concurrent connections per App Key | **5** |
| Server-side session retention after disconnect | **~1 minute** |
| Push rate per connection | **3 messages/second** |
This package opens **one** connection and never exposes sockets, so you cannot cause a
sixth. The one-minute retention is why reconnect backoff matters: reconnecting immediately
after hitting the limit fails until the venue ages the old sessions out.
If two instances ever shared a session id, the venue would disconnect whichever connected
first — each instance looking healthy in isolation. The id is generated per feed, so this
cannot happen unless you pass one explicitly.
## UAT has REST but no stream
```elixir
{:ok, quote} = DpExchange.Webull.get_price("BTC-USD", environment: :uat, credentials: creds)
```
`environment: :uat` gives authenticated REST against test data. There is **no UAT broker** —
the hostname does not resolve — so:
```elixir
DpExchange.Webull.subscribe(["BTC-USD"], environment: :uat)
#=> {:error, {:streaming_unavailable, :uat}}
```
It refuses rather than falling back to production, because a consumer testing against UAT
that received production prices would be reading real market data believing it was fake.
Ask first if you need to branch:
```elixir
if DpExchange.Webull.streaming?(environment: :uat), do: …
```
Production and UAT can run side by side — supervisor, feed and limiter names all derive
from the environment, so the two neither collide nor share a rate-limit bucket.
**`:production` is the default and a typo raises.** `environment: :uatt` is an
`ArgumentError`, not a quiet fallback: meaning UAT and getting production sends a real
order to a real broker.
## There is no trade volume, anywhere
Not on the bars, not on the snapshot, not on the stream. `volume` is `nil`, never `0` —
zero would look like a real measurement of no trading. `capabilities/0` says
`reports_trade_volume: false`, so route volume-dependent work to another venue rather than
reading a column of nils.
## Eight candle widths, and `1w` is deliberately not one
`1m 5m 15m 30m 1h 2h 4h 1d`.
The venue does serve a weekly bar. It is excluded because a weekly boundary depends on
which weekday the venue starts its week, `Core.Timeframe` models no alignment rule for it,
and a bar whose boundary cannot be verified is a bar that should not be stored.
Asking for a width outside that list is an error, never the nearest one.
## Timestamps come from the venue, or the call fails
A bar or quote the venue did not date returns `{:error, :missing_venue_timestamp}`. The
local clock is never substituted — an undated bar stamped with your own clock is
indistinguishable from a real one, which is how a gap becomes invisible.
## What this package does not do yet
Order placement, balances, accounts, fees, transfers, trade history, the order book and the
market overview are all `:unsupported` in this release. That is a statement about this
package, not about the venue — Webull serves all of them, and they have not been ported.
Read `capabilities/0` rather than assuming.