lib/tai/venue_adapters/gdax/products.ex

defmodule Tai.VenueAdapters.Gdax.Products do
  @moduledoc """
  Hydrates the available products on the GDAX exchange
  """

  @type venue :: Tai.Venue.id()
  @type product :: Tai.Venues.Product.t()
  @type error_reason ::
          :timeout
          | {:credentials, reason :: term}
          | {:service_unavailable, reason :: String.t()}

  @spec products(venue) :: {:ok, [product]} | {:error, error_reason}
  def products(venue_id) do
    with {:ok, exchange_products} <- ExGdax.list_products() do
      products = Enum.map(exchange_products, &build(&1, venue_id))
      {:ok, products}
    else
      {:error, "Invalid Passphrase" = reason, _status} ->
        {:error, {:credentials, reason}}

      {:error, "Invalid API Key" = reason, _status} ->
        {:error, {:credentials, reason}}

      {:error, reason, 503} ->
        {:error, {:service_unavailable, reason}}

      {:error, "timeout"} ->
        {:error, :timeout}
    end
  end

  defp build(
         %{
           "base_currency" => base_currency,
           "quote_currency" => quote_currency,
           "id" => id,
           "status" => exchange_status,
           "base_min_size" => raw_base_min_size,
           "base_max_size" => raw_base_max_size,
           "quote_increment" => raw_quote_increment
         },
         venue_id
       ) do
    symbol = Tai.Symbol.build(base_currency, quote_currency)
    {:ok, status} = Tai.VenueAdapters.Gdax.ProductStatus.normalize(exchange_status)
    base_min_size = raw_base_min_size |> Tai.Utils.Decimal.cast!(:normalize)
    base_max_size = raw_base_max_size |> Tai.Utils.Decimal.cast!(:normalize)
    quote_increment = raw_quote_increment |> Tai.Utils.Decimal.cast!(:normalize)
    min_notional = Decimal.mult(base_min_size, quote_increment) |> Decimal.normalize()

    %Tai.Venues.Product{
      venue_id: venue_id,
      symbol: symbol,
      venue_symbol: id,
      base: base_currency |> downcase_and_atom(),
      quote: quote_currency |> downcase_and_atom(),
      venue_base: base_currency,
      venue_quote: quote_currency,
      status: status,
      type: :spot,
      collateral: false,
      price_increment: quote_increment,
      size_increment: base_min_size,
      min_price: quote_increment,
      min_size: base_min_size,
      min_notional: min_notional,
      max_size: base_max_size,
      value: Decimal.new(1),
      value_side: :base,
      is_quanto: false,
      is_inverse: false
    }
  end

  defp downcase_and_atom(str), do: str |> String.downcase() |> String.to_atom()
end