defmodule Polymarket.Schemas.PriceChangeEvent do @moduledoc """ Price change event. """ use TypedEctoSchema alias Polymarket.Schemas.Coerce alias Polymarket.Schemas.PriceChange alias Polymarket.Schemas.PriceChangeEvent @primary_key false @derive Jason.Encoder typed_embedded_schema do field(:market, :string) # Unix epoch in milliseconds, delivered as a string (e.g. "1779656681214"). field(:timestamp, :integer) field(:event_type, :string) embeds_many(:price_changes, PriceChange) end @required [:market, :timestamp, :event_type] @doc """ Builds a `PriceChangeEvent` 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 %PriceChangeEvent{ market: attrs[:market], timestamp: Coerce.to_int(attrs[:timestamp]), event_type: attrs[:event_type], price_changes: price_changes(attrs[:price_changes]) } |> Coerce.require_fields(@required) end @spec price_changes([map()] | nil) :: [PriceChange.t()] defp price_changes(nil), do: [] defp price_changes(changes) do Enum.map(changes, fn change -> %PriceChange{ asset_id: change[:asset_id], price: Coerce.to_float(change[:price]), size: Coerce.to_float(change[:size]), side: change[:side], hash: change[:hash], best_bid: Coerce.to_float(change[:best_bid]), best_ask: Coerce.to_float(change[:best_ask]) } end) end end