WebSocket frames into the contract's value types.
Pure functions; no socket. Three rules here are not obvious from the field names, and each produces a wrong-but-plausible answer if missed.
1. m is "whether the buyer is the maker" — the opposite of the taker's side
This venue reports the trade side two different ways on two transports:
REST /v1/trades `type` = the TAKER's side. "buy" means an ask was lifted.
WS @trade `m` = whether the BUYER was the MAKER.So m: true means the buyer was resting and the seller was the aggressor —
aggressor: :sell. Carrying m straight through as a buy would invert every trade on the
socket while agreeing with the REST field name, which is exactly how such a bug survives
review.
2. Timestamps are nanoseconds, not milliseconds
E is documented as a nanosecond Unix timestamp, and the vendor notes the values exceed
JavaScript's safe integer range. Reading one as milliseconds puts the event roughly fifty
thousand years into the future; reading it as seconds is worse, because the result still
looks like a date.
3. A depth frame is a diff, and the sequence range is how you know it is safe
depth and depthFast carry U..u — the range of update ids the frame covers. The
vendor: "if a frame's U skips ahead of the last applied u, discard the book and
resubscribe to resync." A package applying diffs without checking that gap builds a book
that is silently wrong from the first dropped frame onward, and every price in it stays
real. depth_gap?/2 is that check.
A quantity of zero in a diff removes the level rather than setting it to zero — the vendor says so, and a package storing the zero would keep a level nobody is quoting.
Summary
Functions
The bid and ask changes in a differential depth frame, as {price, quantity} levels.
Whether applying frame to a book last updated at last_applied would skip updates.
An OrderBook from a partial-depth snapshot (@depth5, @depth10, @depth20).
An OrderBookDelta from a {symbol}@depth / @depthFast differential frame.
A TopOfBook from a {symbol}@bookTicker frame.
A Trade from a {symbol}@trade frame.
Functions
The bid and ask changes in a differential depth frame, as {price, quantity} levels.
A quantity of zero removes the level — the vendor says so — and is returned as-is rather than filtered, because the caller applying the diff is the one that must delete rather than store it. Filtering here would drop the deletion and leave a level nobody quotes standing forever.
Whether applying frame to a book last updated at last_applied would skip updates.
The vendor's rule: a frame's U must not skip ahead of the last applied u. true
means discard the book and resubscribe — not "retry", because the missing updates are
gone and the local book is already wrong.
nil for last_applied means nothing has been applied yet, which is never a gap.
@spec to_order_book(map(), String.t(), DateTime.t()) :: {:ok, DpExchange.Core.Types.OrderBook.t()}
An OrderBook from a partial-depth snapshot (@depth5, @depth10, @depth20).
These frames carry lastUpdateId and absolute levels. lastUpdateId becomes the book's
sequence, which is what lets a caller tell one snapshot from a later one — a snapshot
with no sequence cannot be ordered against anything.
The frame carries no timestamp of its own, so observed_at is passed in and used;
that is when the snapshot was seen, and the type has nowhere to claim otherwise.
@spec to_order_book_delta(map(), String.t()) :: {:ok, DpExchange.Core.Types.OrderBookDelta.t()} | {:error, term()}
An OrderBookDelta from a {symbol}@depth / @depthFast differential frame.
Built on depth_changes/1, side-tagging each level the way OrderBookDelta.level/0
requires — bids first, then asks. The venue does not interleave the two sides by time
within one frame, so concatenating them in that order loses no ordering information the
frame itself carried.
sequence is the frame's u — the update id this diff advances the book to, the same
value depth_gap?/2 compares the next frame's U against. A quantity of zero is
carried through unresolved, exactly as depth_changes/1 and OrderBookDelta's own
moduledoc both require: it means the level ceased to exist, and deciding that is the
consumer's job, not this package's.
@spec to_top_of_book(map(), String.t(), DateTime.t()) :: {:ok, DpExchange.Core.Types.TopOfBook.t()} | {:error, term()}
A TopOfBook from a {symbol}@bookTicker frame.
b/B are the bid and its size, a/A the ask and its size. E is the venue's own
nanosecond timestamp, so unlike some venues in this family venue_time is real here.
@spec to_trade(map(), String.t()) :: {:ok, DpExchange.Core.Types.Trade.t()} | {:error, term()}
A Trade from a {symbol}@trade frame.
aggressor is inverted from m: see the moduledoc. m: true (buyer is maker) means
the seller lifted, so the aggressor is :sell.