DpExchange.Core.Types.OptionChain (DpExchangeCore v0.1.52)

Copy Markdown View Source

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.

Summary

Functions

The expiries in the chain, earliest first.

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

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

Types

strike_row()

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

t()

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

Functions

expiry_dates(option_chain)

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

The expiries in the chain, earliest first.

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 — see DpExchange.Core.Types.Validate.

strikes(option_chain, expiry)

@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.