defmodule Polymarket.Schemas.BookEvent do @moduledoc """ Book event. A full order book snapshot for an asset. """ use TypedEctoSchema alias Polymarket.Schemas.Ask alias Polymarket.Schemas.Bid alias Polymarket.Schemas.BookEvent alias Polymarket.Schemas.Coerce @primary_key false @derive Jason.Encoder typed_embedded_schema do field(:market, :string) field(:asset_id, :string) field(:hash, :string) # Unix epoch in milliseconds, delivered as a string (e.g. "1779658820355"). field(:timestamp, :integer) field(:event_type, :string) field(:last_trade_price, :float) field(:tick_size, :float) embeds_many(:bids, Bid) embeds_many(:asks, Ask) end @required [:market, :asset_id, :hash, :timestamp, :event_type] @doc """ Builds a `BookEvent` from a decoded (atom-keyed) websocket message. Constructs the struct directly — no `Ecto.Changeset` — for the websocket hot path. Numeric fields (delivered as JSON strings) are coerced via `Polymarket.Schemas.Coerce`. Returns `{:error, {:missing_fields, fields}}` when a required field is absent. """ @spec from_attrs(map()) :: {:ok, t()} | {:error, {:missing_fields, [atom()]}} def from_attrs(attrs) do %BookEvent{ market: attrs[:market], asset_id: attrs[:asset_id], hash: attrs[:hash], timestamp: Coerce.to_int(attrs[:timestamp]), event_type: attrs[:event_type], last_trade_price: Coerce.to_float(attrs[:last_trade_price]), tick_size: Coerce.to_float(attrs[:tick_size]), bids: bids(attrs[:bids]), asks: asks(attrs[:asks]) } |> Coerce.require_fields(@required) end @spec bids([map()] | nil) :: [Bid.t()] defp bids(nil), do: [] defp bids(levels) do Enum.map(levels, fn level -> %Bid{price: Coerce.to_float(level[:price]), size: Coerce.to_float(level[:size])} end) end @spec asks([map()] | nil) :: [Ask.t()] defp asks(nil), do: [] defp asks(levels) do Enum.map(levels, fn level -> %Ask{price: Coerce.to_float(level[:price]), size: Coerce.to_float(level[:size])} end) end end