DpExchange.Gemini.Private (DpExchangeGemini v0.1.1)

Copy Markdown View Source

Gemini's authenticated endpoints — internal. Balances, orders and trade history.

The boundary, precisely

The host authenticates. This module makes authenticated requests with what the host hands it. Those are different jobs and the difference is the whole design: a caller passes credentials into every one of these functions, they are used to sign that one request, and nothing is kept. This module never obtains a credential, never stores one, never refreshes one, and never decides which authentication scheme applies — see Auth, which refuses to guess.

Which means trading works exactly as the facade intends: the host does auth, calls place_order/3 with credentials, and gets a Core.Types.Order back.

Gemini has no market orders, and this package will not fake one

From the venue's own page:

The API doesn't directly support market orders because they provide you with no price protection. Instead, use the "immediate-or-cancel" order execution option, coupled with an aggressive limit price (i.e. very high for a buy order or very low for a sell order), to achieve the same result.

That advice is not something a package may take on a caller's behalf. "An aggressive limit price" means inventing a number the caller did not supply and sending it to a live exchange as a real order. How aggressive? Ten percent through the book? Fifty? The package cannot know, the caller never said, and the difference is money.

So order_type: :market is {:error, {:unsupported_order_type, :market}} — the venue does not serve it, and the nearest thing requires a price only the caller can choose. A caller who wants that behaviour asks for it explicitly, with their own limit:

%{order_type: :limit, time_in_force: :ioc, price: my_aggressive_price, }

This is the family's named failure mode in its most expensive form. Every other instance in this codebase costs a wrong number in a chart; this one costs a fill at a price nobody chose.

Order execution options are mutually exclusive

If you specify more than one option (or an unsupported option) in the options array, the exchange will reject your order.

So time_in_force maps to exactly one option, or none:

time_in_forceGemini optionMeaning
:gtc (default)nonefills what it can, rests the remainder on the book
:post_only / :maker_or_cancelmaker-or-canceladds liquidity only, cancels if it would take
:iocimmediate-or-canceltakes what it can, cancels the rest
:fokfill-or-killfills entirely or cancels entirely

No option may be combined with a stop-limit order.

A cancelled order is not a failed request

MOC, IOC and FOK orders that do not fill come back 200 with "is_cancelled": true. That is the venue answering successfully; the order simply did not rest. It maps to status: :cancelled on a {:ok, order}, never to an error — a caller that treated it as a failure would retry an order the venue already handled.

Auth failures are refusals, not errors

Measured 2026-08-28 against the demo environment: an unauthenticated POST to any private endpoint returns 401 MissingSecurityHeaders. Gemini's own error table documents MissingApikeyHeader at 400, so the documented codes and the live ones disagree — the fourth documentation divergence found on this venue.

Either way 400, 401 and 403 are permanent for the request as sent: retrying the identical bytes cannot succeed. They are {:refused, reason}. A caller whose token expired refreshes it and calls again with new credentials, which is a different request — not a retry of this one.

Summary

Functions

Cancels one order by the venue's order id.

The account's own record of itself — name, type, and the roles the key carries.

Every currency the account holds, with what is available and what is on hold.

The fee tier this account trades at, from /v1/notionalvolume.

One order's current state.

Every order currently resting on the book for this account.

Past fills for a symbol.

Transfers in and out of the account.

Confirms the credentials reach the venue, using its own heartbeat endpoint.

Functions

cancel_order(credentials, order_id, opts)

@spec cancel_order(map(), String.t(), keyword()) ::
  {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}

Cancels one order by the venue's order id.

get_accounts(credentials, opts)

@spec get_accounts(
  map(),
  keyword()
) :: {:ok, [map()]} | {:error, term()} | {:refused, term()}

The account's own record of itself — name, type, and the roles the key carries.

get_balances(credentials, opts)

@spec get_balances(
  map(),
  keyword()
) ::
  {:ok, [DpExchange.Core.Types.Balance.t()]}
  | {:error, term()}
  | {:refused, term()}

Every currency the account holds, with what is available and what is on hold.

get_fees(credentials, opts)

@spec get_fees(
  map(),
  keyword()
) :: {:ok, map()} | {:error, term()} | {:refused, term()}

The fee tier this account trades at, from /v1/notionalvolume.

Returned as the venue states it — basis points, per maker/taker, alongside the notional volume that determined the tier. Nothing is converted to a rate, because the venue's own units are what a caller will reconcile against.

get_order(credentials, order_id, opts)

@spec get_order(map(), String.t(), keyword()) ::
  {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}

One order's current state.

get_orders(credentials, opts)

@spec get_orders(
  map(),
  keyword()
) ::
  {:ok, [DpExchange.Core.Types.Order.t()]}
  | {:error, term()}
  | {:refused, term()}

Every order currently resting on the book for this account.

get_trade_history(credentials, opts)

@spec get_trade_history(
  map(),
  keyword()
) ::
  {:ok, [DpExchange.Core.Types.Fill.t()]}
  | {:error, term()}
  | {:refused, term()}

Past fills for a symbol.

Gemini requires a symbol here — there is no all-symbols variant — so a caller asking for everything is asking for one request per symbol, and it is theirs to decide whether to.

get_transfers(credentials, opts)

@spec get_transfers(
  map(),
  keyword()
) :: {:ok, [map()]} | {:error, term()} | {:refused, term()}

Transfers in and out of the account.

place_order(credentials, request, opts)

@spec place_order(map(), map(), keyword()) ::
  {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}

Places an order.

request is a map carrying at least :symbol, :side, :quantity and :price. :order_type defaults to :limit; :time_in_force defaults to :gtc. A :client_order_id is passed through when given and is strongly recommended by the venue.

Refuses rather than substituting:

  • order_type: :market — the venue serves none, and the documented workaround needs a limit price only the caller can choose
  • an unknown :time_in_force — the venue rejects an unsupported option outright
  • a missing price on a type that requires one

test_connection(credentials, opts)

@spec test_connection(
  map(),
  keyword()
) :: {:ok, map()} | {:error, term()} | {:refused, term()}

Confirms the credentials reach the venue, using its own heartbeat endpoint.

A real round trip rather than a guess: /v1/heartbeat is authenticated, so a success proves the key, the signature and the nonce mode are all right. It also resets the session's cancel-on-disconnect timer, which is a side effect worth knowing about — on a key provisioned with Requires Heartbeat, calling this keeps open orders alive.