DpExchange.Core.Types.VolumeProfile (DpExchangeCore v0.3.22)

Copy Markdown View Source

Traded volume split by price and by side within one interval — a "footprint" bar.

A candle says where; this says how much, and who lifted it

A Candle gives four prices and a total volume. It cannot say that of 1,000 shares traded, 600 went through at the ask and 400 at the bid, nor that most of the buying happened at one price and the selling at another. That split is the whole content here.

The two are not derivable from one another in either direction. A candle cannot be reconstructed from a profile that carries no open, and a profile cannot be inferred from a candle's single volume number — which is why this is a separate type rather than fields bolted onto Candle.

:delta is signed, and it is not an error when it disagrees with the totals

:delta is the venue's own buy-minus-sell figure. This type does not recompute it from :buy_volume and :sell_volume, and does not correct it when the three disagree: a venue that classifies some prints as neither aggressive buy nor aggressive sell will report totals that do not reconcile, and that gap is information about the venue's classifier rather than a fault to paper over.

:buy_at_price and :sell_at_price are maps keyed on the venue's price string

Kept as the venue sent them — %{"24.20" => Decimal, "24.21" => Decimal} — rather than re-keyed on Decimal. Two price strings that parse to equal decimals are the same level, and merging them here would silently combine two of the venue's rows into one; leaving them alone keeps the venue's own grid visible.

Empty maps mean the venue reported no split, not that nothing traded at any price.

Summary

Functions

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

The price with the most volume across both sides — the point of control.

Types

t()

@type t() :: %DpExchange.Core.Types.VolumeProfile{
  buy_at_price: %{required(String.t()) => Decimal.t()} | nil,
  buy_volume: Decimal.t() | nil,
  delta: Decimal.t() | nil,
  opened_at: DateTime.t(),
  provider: atom() | String.t(),
  sell_at_price: %{required(String.t()) => Decimal.t()} | nil,
  sell_volume: Decimal.t() | nil,
  session: atom() | nil,
  symbol: String.t(),
  timeframe: String.t(),
  total_volume: Decimal.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.

point_of_control(volume_profile)

@spec point_of_control(t()) :: String.t() | nil

The price with the most volume across both sides — the point of control.

Returns the venue's own price string, or nil when neither side reported a split. nil is not a price, and a caller that needs one must treat the interval as un-profiled rather than substituting the close.

Ties return the lower price, chosen so the answer is stable across calls rather than dependent on map ordering. A caller that cares about ties should read the maps.