defmodule CCXT.ResponseTransformer do @moduledoc """ Response transformers for converting raw exchange responses to unified format. Some exchanges return responses in formats that don't match the unified API expectations. For example: - BitMEX returns `[%{ticker}]` instead of `%{ticker}` for fetch_ticker - BitMEX returns flat order list instead of `%{bids: [], asks: []}` for fetch_order_book This module provides transformers that can be configured per-endpoint in specs. ## Usage Add a `response_transformer` field to an endpoint in the spec: %{ name: :fetch_ticker, path: "/instrument", response_transformer: :unwrap_single_element_list } Available transformers: - `:unwrap_single_element_list` - Unwraps `[item]` to `item` - `:order_book_from_flat_list` - Converts flat order list to `%{bids: [], asks: []}` """ require Logger @type transformer :: :unwrap_single_element_list | :order_book_from_flat_list | nil @doc """ Applies a transformer to the response body. Returns the transformed body, or the original body if no transformer is specified. """ @spec transform(term(), transformer()) :: term() def transform(body, nil), do: body def transform(body, :unwrap_single_element_list), do: unwrap_single_element_list(body) def transform(body, :order_book_from_flat_list), do: order_book_from_flat_list(body) def transform(body, unknown) do Logger.warning("[ResponseTransformer] Unknown transformer: #{inspect(unknown)}, returning body unchanged") body end @doc """ Unwraps a single-element list to its element. Used when an API returns `[item]` but the unified API expects `item`. ## Examples iex> CCXT.ResponseTransformer.unwrap_single_element_list([%{"symbol" => "BTC"}]) %{"symbol" => "BTC"} iex> CCXT.ResponseTransformer.unwrap_single_element_list([]) [] iex> CCXT.ResponseTransformer.unwrap_single_element_list([%{a: 1}, %{b: 2}]) [%{a: 1}, %{b: 2}] """ @spec unwrap_single_element_list(term()) :: term() def unwrap_single_element_list([single]) when is_map(single), do: single def unwrap_single_element_list(other), do: other @doc """ Converts a flat order list to structured order book format. BitMEX returns orders as a flat list with "side" field: `[%{"side" => "Sell", "price" => 100, "size" => 10}, %{"side" => "Buy", ...}]` This transforms it to unified format: `%{"bids" => [[price, size], ...], "asks" => [[price, size], ...]}` ## Examples iex> orders = [ ...> %{"side" => "Sell", "price" => 100.5, "size" => 10}, ...> %{"side" => "Buy", "price" => 99.5, "size" => 20} ...> ] iex> CCXT.ResponseTransformer.order_book_from_flat_list(orders) %{"bids" => [[99.5, 20]], "asks" => [[100.5, 10]]} """ @spec order_book_from_flat_list(term()) :: term() def order_book_from_flat_list(orders) when is_list(orders) do {bids, asks} = Enum.reduce(orders, {[], []}, fn order, {bids, asks} -> price = order["price"] size = order["size"] case order["side"] do "Buy" -> {[[price, size] | bids], asks} "Sell" -> {bids, [[price, size] | asks]} # Skip unknown sides _ -> {bids, asks} end end) # Bids sorted descending (highest first), asks sorted ascending (lowest first) %{ "bids" => Enum.sort_by(bids, fn [price, _] -> price end, :desc), "asks" => Enum.sort_by(asks, fn [price, _] -> price end, :asc) } end def order_book_from_flat_list(other), do: other end