DpExchange.Core.Types.OrderBook (DpExchangeCore v0.3.120)

Copy Markdown View Source

Normalised order book snapshot, returned by every venue package.

bids and asks are sorted by price, best price first:

  • bids descending — highest bid first
  • asks ascending — lowest ask first

Each level is a {price, quantity} tuple. The ordering is part of the contract, not a convenience: a caller reading hd(bids) as the best bid is reading it correctly, and a venue package that returns venue-order without re-sorting has broken the contract even though every value in it is true.

:venue_time is the venue's own, used as-is, and nil where the venue publishes none — a nil there says the venue did not date this book, which is information rather than an omission. :observed_at is when this package read it, and is always present.

These replaced a single :timestamp in 0.2.0. See DpExchange.Core.Types.Quote's own moduledoc for the full reasoning, and docs/design/2026-09-09_venue-time-and-observed-time.md for the decision: the short version is that a venue publishing no time for a frame — which Gemini's partial-depth snapshot genuinely does not — had to either lie in a field documented as the venue's or drop real data, and one field could not tell a consumer which had happened.

dp_exchange_schwab's Streamer book is the model for :venue_time: it reads the venue's snapshot_time and fails closed when absent, rather than substituting.

Sizes and prices

In a snapshot a level whose size is 0 is dropped by the package's decoder: it is no liquidity and does not belong in the book, so new/1 refuses one that reaches it rather than keep it. A negative size, or a price <= 0, refuses the whole book, and so does a level that cannot be read, or a side that is present but not a list. An unreadable level is never skipped, because a book missing a level is a different book. (A delta, Types.OrderBookDelta, is different: size 0 there means "remove this price".) The depth a caller asks for is the :depth option, in levels per side, on every venue.

Sequence

:sequence is the venue's book sequence number where it publishes one, for callers reconciling snapshots against a delta stream, and nil where it does not.

Summary

Functions

Builds a t/0, failing closed if a required field is absent or nil.

Types

level()

@type level() :: {Decimal.t(), Decimal.t()}

t()

@type t() :: %DpExchange.Core.Types.OrderBook{
  asks: [level()],
  bids: [level()],
  observed_at: DateTime.t(),
  provider: atom() | String.t(),
  sequence: integer() | nil,
  symbol: String.t(),
  venue_time: DateTime.t() | nil
}

Functions

new(attrs)

@spec new(keyword() | map()) :: t()

Builds a t/0, failing closed if a required field is absent or nil.

@enforce_keys guards presence, not nil — see DpExchange.Core.Types.Validate.

Also raises ArgumentError when bids or asks is not a list of {price, quantity} pairs, or when the moduledoc's ordering is broken: bids must not rise and asks must not fall from one level to the next, so hd/1 is the best price. A venue-order book that was never re-sorted is true in every value and wrong in meaning. A level whose price is <= 0 or whose size is <= 0 raises too (see "Sizes and prices" above), and so does a :sequence that is neither an integer nor nil: a "7" compared with > against the integer a consumer last held sorts above it by Erlang term order, and a stale book would read as the newest.