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
Types
@type order_type() ::
:market
| :limit
| :stop
| :stop_limit
| :post_only
| :ioc
| :fok
| :trailing_stop
| :trailing_stop_limit
| :market_on_close
| :limit_on_close
@type side() :: :buy | :sell
@type status() ::
:pending
| :open
| :partially_filled
| :filled
| :cancelled
| :rejected
| :expired
@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 }
@type time_in_force() :: :gtc | :ioc | :fok | :gtd | :day | :gfw | :gfm
Functions
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):
:filledor:partially_filledwith a:filled_quantityof zero: nothing was filled, so the order is neither. A venue's status word read as:filledwhere 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,:openor:partially_filledwith a:filled_quantityat or above a positive:quantity: the whole size is filled, so nothing is left to be working or partial.:filledwith a:filled_quantitybelow a positive:quantity::filledmeans the entire quantity traded. A cancel that landed after a part fill is:cancelledwith the figure in:filled_quantity, not:filled: a consumer reading:filledas "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.