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

Every listed contract on one underlying, addressed the way traders address them.

## A chain is two-dimensional, and flattening it loses the question

A chain is **expiry × strike**, with a call and a put at each intersection. Returning a
flat list of contracts would be lossless in data and useless in shape: the questions asked
of a chain are "what expiries are there", "what strikes at this expiry", and "the call and
the put at this strike" — and a list answers none of them without the caller rebuilding
the grid the venue already had.

Schwab publishes it as `callExpDateMap` and `putExpDateMap`, each keyed by expiry and then
by strike. This keeps that structure and normalises the keys:

    %{~D[2026-03-15] => %{
        Decimal.new("500") => %{call: %OptionContract{}, put: %OptionContract{}}
      }}

**A missing side is `nil`, not an absent key.** A strike listed with only a call is a real
thing, and a caller iterating strikes must see it rather than have it silently skipped.

## `:underlying_price` is carried because the chain is meaningless without it

Moneyness, and every Greek, is relative to where the underlying is. A chain snapshot and
an underlying price fetched separately are two observations at two times, and on a moving
underlying that is how a caller ends up with a "delta-neutral" position that is not.

# `strike_row`

```elixir
@type strike_row() :: %{call: term() | nil, put: term() | nil}
```

# `t`

```elixir
@type t() :: %DpExchange.Core.Types.OptionChain{
  expiries: %{optional(Date.t()) =&gt; %{optional(Decimal.t()) =&gt; strike_row()}},
  provider: atom(),
  underlying: String.t(),
  underlying_price: Decimal.t() | nil,
  venue_time: DateTime.t() | nil
}
```

# `expiry_dates`

```elixir
@spec expiry_dates(t()) :: [Date.t()]
```

The expiries in the chain, earliest first.

# `new`

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

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

`@enforce_keys` guards presence, not `nil` — see `DpExchange.Core.Types.Validate`.

Also raises on `:expiries` that is not a map of `Date` to a map of `Decimal` strike to row
(a string key sorts wrongly or raises in `expiry_dates/1` and `strikes/2`, in the consumer's
process, far from the decode), and on an `:underlying_price` that is not a number.

# `strikes`

```elixir
@spec strikes(t(), Date.t()) :: [Decimal.t()]
```

The strikes listed at `expiry`, ascending. `[]` when the expiry is not in the chain.

`[]` for an absent expiry rather than an error: a caller walking expiries it got from
`expiry_dates/1` cannot ask for one that is missing, and a caller asking for an arbitrary
date is asking whether anything is listed — which is what an empty list says.

---

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