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

Copy Markdown View Source

Normalised order, returned by every venue package.

:created_at and :updated_at are the venue's own values where it supplies them and nil where it does not. nil is the honest answer; a substituted local clock would be a plausible value with the wrong meaning.

Not every venue supports every order_type/0 or reports every status/0. The union here is what the contract can express; what a given venue can actually accept is declared by that package's capabilities/0, and asking for one it does not support is an error rather than a silent downgrade to the nearest available type.

Why the enforced keys still admit nil

All seven are enforced, and all but provider allow nil. That is not laxity — it is the same rule as everywhere else here: the venue's word, or nothing.

A venue sending a status this package does not recognise gets nil, never the nearest atom a caller might branch on. Coinbase's close_position/3 returns an order whose side the venue never states — the venue worked the side out from a position this package did not read — and filling in :sell because closing is usually selling is wrong exactly where it matters, on a short.

symbol and id joined them on 2026-09-01. Robinhood acknowledges a cancel request without describing the order it cancelled: there is an id and nothing else, and inventing a symbol to satisfy a type would put a guess where the venue was silent.

The keys stay enforced so a constructor must decide. The types admit nil so the decision can be "the venue did not say".

Summary

Functions

Builds a t/0, failing closed only if :provider is absent or nil.

Types

order_type()

@type order_type() ::
  :market
  | :limit
  | :stop
  | :stop_limit
  | :post_only
  | :ioc
  | :fok
  | :trailing_stop
  | :trailing_stop_limit
  | :market_on_close
  | :limit_on_close

side()

@type side() :: :buy | :sell

status()

@type status() ::
  :pending
  | :open
  | :partially_filled
  | :filled
  | :cancelled
  | :rejected
  | :expired

t()

@type t() :: %DpExchange.Core.Types.Order{
  average_price: Decimal.t() | nil,
  created_at: DateTime.t() | nil,
  fee: Decimal.t() | nil,
  fee_currency: String.t() | nil,
  filled_quantity: Decimal.t() | nil,
  id: String.t() | nil,
  legs: [DpExchange.Core.Types.OrderLeg.t()] | nil,
  order_type: order_type() | nil,
  price: Decimal.t() | nil,
  provider: atom() | String.t(),
  quantity: Decimal.t() | nil,
  side: side() | nil,
  status: status() | nil,
  stop_price: Decimal.t() | nil,
  symbol: String.t() | nil,
  time_in_force: time_in_force() | nil,
  updated_at: DateTime.t() | nil
}

time_in_force()

@type time_in_force() :: :gtc | :ioc | :fok | :gtd | :day | :gfw | :gfm

Functions

new(attrs)

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

Builds a t/0, failing closed only if :provider is absent or nil.

Every other enforced key may legitimately be nil here — see the moduledoc's "Why the enforced keys still admit nil". @enforce_keys still guards their presence; this adds the one field where nil was never intended, using DpExchange.Core.Types.Validate.

A :quantity or :filled_quantity below zero raises: a size is never negative, a negative one is a decode bug (a sign flipped, a delta read as a level), and a consumer summing fills against it would conclude the order is over-filled. Zero stays legal (an order nothing has filled). Prices are not held to a sign here: a venue's 0 for the price of a market order is common, and which fields a venue states is the venue's word.

Every closed vocabulary is refused outside what its type states, with no default. :side is :buy, :sell or nil; :order_type and :status are the atoms of order_type/0 and status/0 or nil; :time_in_force is one of DpExchange.Core.Capabilities.time_in_force/0 or nil (it used to be any atom, so :good_til_cancelled or :GTC built and matched no branch of a consumer's case). nil is "the venue did not say", never a stand-in for an unrecognised word. :legs is nil or a proper list of DpExchange.Core.Types.OrderLeg.

A status is refused beside a :filled_quantity that contradicts it. Only when the venue stated both (a nil size states nothing, so no judgement is made):

  • :filled or :partially_filled with a :filled_quantity of zero: nothing was filled, so the order is neither. A venue's status word read as :filled where the fill figure says otherwise is the decode bug this catches, and a consumer who trusts the status books a position that does not exist.
  • :pending, :open or :partially_filled with a :filled_quantity at or above a positive :quantity: the whole size is filled, so nothing is left to be working or partial.
  • :filled with a :filled_quantity below a positive :quantity: :filled means the entire quantity traded. A cancel that landed after a part fill is :cancelled with the figure in :filled_quantity, not :filled: a consumer reading :filled as "nothing is left" would book a position larger than the one held.

:cancelled, :rejected and :expired carry whatever was filled before they ended, so they are not judged against it. A :filled_quantity above :quantity is not judged either: which of the two a venue rounded is the venue's word.