# `DpExchange.Core.Types.Order`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/types/order.ex#L1)

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 `t:order_type/0` or reports every `t: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".

# `order_type`

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

# `side`

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

# `status`

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

# `t`

```elixir
@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`

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

# `new`

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

Builds a `t: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 `t:order_type/0` and
`t: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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
