DpExchange.Core.Types.Candle (DpExchangeCore v0.3.8)

Copy Markdown View Source

One OHLC bar over an interval.

Why this type exists

get_historical_prices/4 declared [Types.Quote.t()] and the venues returned bare maps with their own keys. Both are wrong in the same direction: a bar is not a quote. A quote is one price at an instant; a bar is four prices and a volume over a span. Flattening a bar into a quote either discards the open, high and low, or keeps the close and calls it "the price" — a plausible number with the wrong meaning, which is this family's recurring defect.

The untyped map was the more immediate problem: with no type, each venue package chose its own keys and nothing compared them. A consumer switching venues got a different map and no error.

:opened_at, not :timestamp

Venues disagree about whether a bar is stamped at its open or its close, and the difference is one whole interval. A daily series stamped at the close and joined to one stamped at the open is misaligned by a day, and every value in both is correct — which is why nothing catches it.

So the field is named for what it holds. A venue that publishes close-stamped bars subtracts the interval on the way in; a venue whose convention is unclear is a venue whose candles this package should not be shipping.

:volume is nil when the venue publishes none

Never 0. One venue in this family reports no crypto volume anywhere, and a 0 there would claim a genuinely flat interval — a much stronger statement than "not reported", and one a consumer might act on.

Summary

Functions

Whether the bar's own values are internally consistent — high is the highest, low the lowest.

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

Types

t()

@type t() :: %DpExchange.Core.Types.Candle{
  close: Decimal.t(),
  high: Decimal.t(),
  low: Decimal.t(),
  open: Decimal.t(),
  opened_at: DateTime.t(),
  provider: atom(),
  symbol: String.t(),
  timeframe: String.t(),
  volume: Decimal.t() | nil
}

Functions

coherent?(candle)

@spec coherent?(t()) :: boolean()

Whether the bar's own values are internally consistent — high is the highest, low the lowest.

A bar failing this is malformed at the venue, and worth catching at the boundary rather than discovering downstream: a high below the close will silently corrupt any range, breakout or volatility calculation built on the series, and none of those will error.

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 — a %__MODULE__{open: nil, ...} literal builds fine despite open's typespec forbidding it, and blows up later inside Decimal instead of at the boundary. This is the exact defect that motivated DpExchange.Core.Types.Validate, written up there in full.