Packages

Elixir client library for cryptocurrency exchanges — generated from CCXT specs via compile-time macros.

Current section

Files

Jump to
ccxt_client lib ccxt ws config.ex
Raw

lib/ccxt/ws/config.ex

defmodule CCXT.WS.Config do
@moduledoc """
Per-exchange WebSocket configuration.
T94 ships WS configuration for the priority-tier universe using the extended
shape that routes WS subscription patterns (`CCXT.WS.Subscription`) and WS
auth patterns (`CCXT.WS.Auth`). Replaces T92's 3-canary
`subscribe_builder: {mod, fun}` with `subscription_pattern` + `auth_pattern`
dispatch atoms.
**Count:** 13 exchanges — every entry here has a matching `CCXT.Registry`
module. T102 (2026-04-19) removed three orphan entries (`bingx`, `bitget`,
`mexc`) whose `Registry` lookups raised because no spec file exists for them
under the current tier1/tier2/dex scope. Add them back via
`mix ccxt_extract.update --exchange <id>` + a re-register in Registry before
reinstating their config here.
WS URLs currently live in this consumer-side map because JSON specs don't
yet surface a clean `urls.api.ws` field. A follow-up ccxt_extract task will
emit `urls.api.ws.{public,private,sandbox_public,sandbox_private}` from the
JS AST; when it lands, this module can be replaced by a one-line read from
`exchange.spec["urls"]["api"]["ws"]`. (Prior to schema 3.0.0 the raw AST was
available under `structure.ws_methods`; T117 dropped that payload to fit the
Hex size cap — the upstream WS-URL task supersedes that path.)
## Entry shape
%{
public_url: String.t() | nil, # may contain `{hostname}` placeholder; nil = requires REST pre-call
public_url_sandbox: String.t() | nil,
private_url: String.t() | nil,
private_url_sandbox: String.t() | nil,
heartbeat: %{type: :ping | :deribit | :custom, interval: pos_integer() | nil},
subscription_pattern: atom(), # routed via CCXT.WS.Subscription
subscription_config: map(),
auth_pattern: atom() | nil, # routed via CCXT.WS.Auth; nil = public-only wiring
auth_config: map()
}
## Known URL limitations (MVP — single URL per public/private slot)
Several exchanges expose per-product URLs (spot vs linear/inverse swap vs
option). The current shape captures one representative URL per
`{section, sandbox}` slot; product routing will land in a follow-up task
once the adapter layer needs it.
- `bybit` — linear-only (`/v5/public/linear`). spot/inverse/option need routing.
- `binance` — spot-only. USDⓈ-M futures (`fstream.binance.com`), COIN-M
(`dstream.binance.com`), and portfolio margin (`/pm/ws`) need routing.
- `htx` / `huobi` — spot-only (`api.huobi.pro/ws`). Contract/swap endpoints
live on `api.hbdm.vn`.
- `aster` — spot-only. Swap on `fstream.asterdex.com`.
- `kucoin` — URLs are fully dynamic (REST `POST /api/v1/bullet-{public,private}`
returns the WSS host + token). Recorded as `nil` until the adapter layer
wires the pre-call.
"""
@configs %{
"binance" => %{
public_url: "wss://stream.binance.com:9443/ws",
public_url_sandbox: "wss://stream.testnet.binance.vision/ws",
private_url: "wss://stream.binance.com:9443/ws",
private_url_sandbox: "wss://stream.testnet.binance.vision/ws",
heartbeat: %{type: :ping, interval: 180_000},
subscription_pattern: :method_subscribe,
subscription_config: %{separator: "@", market_id_format: :lowercase},
auth_pattern: :listen_key,
auth_config: %{
pre_auth: %{
type: :listen_key,
endpoints: %{
spot: "privatePostUserDataStream",
linear: "fapiPrivatePostListenKey",
inverse: "dapiPrivatePostListenKey"
}
}
}
},
"bybit" => %{
public_url: "wss://stream.{hostname}/v5/public/linear",
public_url_sandbox: "wss://stream-testnet.{hostname}/v5/public/linear",
private_url: "wss://stream.{hostname}/v5/private",
private_url_sandbox: "wss://stream-testnet.{hostname}/v5/private",
heartbeat: %{type: :ping, interval: 20_000},
subscription_pattern: :op_subscribe,
subscription_config: %{separator: "."},
auth_pattern: :direct_hmac_expiry,
auth_config: %{
op_field: "op",
op_value: "auth",
encoding: :hex,
expires_offset_ms: 10_000
}
},
"okx" => %{
public_url: "wss://ws.okx.com:8443/ws/v5/public",
public_url_sandbox: "wss://wspap.okx.com:8443/ws/v5/public",
private_url: "wss://ws.okx.com:8443/ws/v5/private",
private_url_sandbox: "wss://wspap.okx.com:8443/ws/v5/private",
heartbeat: %{type: :ping, interval: 25_000},
subscription_pattern: :op_subscribe_objects,
subscription_config: %{},
auth_pattern: :iso_passphrase,
auth_config: %{timestamp_unit: :seconds}
},
"deribit" => %{
public_url: "wss://www.deribit.com/ws/api/v2",
public_url_sandbox: "wss://test.deribit.com/ws/api/v2",
private_url: "wss://www.deribit.com/ws/api/v2",
private_url_sandbox: "wss://test.deribit.com/ws/api/v2",
heartbeat: %{type: :deribit, interval: 30_000},
subscription_pattern: :jsonrpc_subscribe,
subscription_config: %{method: "public/subscribe"},
auth_pattern: :jsonrpc_linebreak,
auth_config: %{}
},
"kraken" => %{
public_url: "wss://ws.kraken.com/v2",
public_url_sandbox: "wss://beta-ws.kraken.com/v2",
private_url: "wss://ws-auth.kraken.com/v2",
private_url_sandbox: "wss://beta-ws-auth.kraken.com/v2",
heartbeat: %{type: :ping, interval: 60_000},
subscription_pattern: :method_params_subscribe,
subscription_config: %{},
auth_pattern: :rest_token,
auth_config: %{
pre_auth: %{
type: :rest_token,
endpoint: "privatePostGetWebSocketsToken"
}
}
},
"kucoin" => %{
# URLs are acquired via REST POST /api/v1/bullet-{public,private}.
# Left nil until the adapter layer wires the pre-call.
public_url: nil,
public_url_sandbox: nil,
private_url: nil,
private_url_sandbox: nil,
heartbeat: %{type: :ping, interval: 18_000},
subscription_pattern: :type_subscribe,
subscription_config: %{op_field: "type", args_field: "topic", args_format: :string},
auth_pattern: :iso_passphrase,
auth_config: %{timestamp_unit: :milliseconds}
},
"gate" => %{
public_url: "wss://api.gateio.ws/ws/v4/",
public_url_sandbox: nil,
private_url: "wss://api.gateio.ws/ws/v4/",
private_url_sandbox: nil,
heartbeat: %{type: :ping, interval: 30_000},
subscription_pattern: :event_subscribe,
subscription_config: %{args_format: :object_list},
auth_pattern: :sha512_newline,
auth_config: %{}
},
"htx" => %{
public_url: "wss://api.huobi.pro/ws",
public_url_sandbox: nil,
private_url: "wss://api.huobi.pro/ws/v2",
private_url_sandbox: nil,
heartbeat: %{type: :custom, interval: 20_000},
subscription_pattern: :sub_subscribe,
subscription_config: %{separator: "."},
auth_pattern: :direct_hmac_expiry,
auth_config: %{
op_field: "op",
op_value: "auth",
encoding: :base64,
expires_offset_ms: 10_000
}
},
"huobi" => %{
public_url: "wss://api.huobi.pro/ws",
public_url_sandbox: nil,
private_url: "wss://api.huobi.pro/ws/v2",
private_url_sandbox: nil,
heartbeat: %{type: :custom, interval: 20_000},
subscription_pattern: :sub_subscribe,
subscription_config: %{separator: "."},
auth_pattern: :direct_hmac_expiry,
auth_config: %{
op_field: "op",
op_value: "auth",
encoding: :base64,
expires_offset_ms: 10_000
}
},
"bitmex" => %{
public_url: "wss://ws.bitmex.com/realtime",
public_url_sandbox: "wss://ws.testnet.bitmex.com/realtime",
private_url: "wss://ws.bitmex.com/realtime",
private_url_sandbox: "wss://ws.testnet.bitmex.com/realtime",
heartbeat: %{type: :ping, interval: 30_000},
subscription_pattern: :op_subscribe,
subscription_config: %{separator: "."},
auth_pattern: :direct_hmac_expiry,
auth_config: %{
op_field: "op",
op_value: "authKeyExpires",
encoding: :base64,
expires_offset_ms: 10_000
}
},
"bitfinex" => %{
public_url: "wss://api-pub.bitfinex.com/ws/2",
public_url_sandbox: nil,
private_url: "wss://api.bitfinex.com/ws/2",
private_url_sandbox: nil,
heartbeat: %{type: :ping, interval: 30_000},
subscription_pattern: :event_subscribe,
subscription_config: %{op_field: "event", args_field: "channel", args_format: :string},
auth_pattern: :sha384_nonce,
auth_config: %{}
},
"coinbaseexchange" => %{
public_url: "wss://ws-feed.exchange.coinbase.com",
public_url_sandbox: "wss://ws-feed-public.sandbox.exchange.coinbase.com",
private_url: "wss://ws-feed.exchange.coinbase.com",
private_url_sandbox: "wss://ws-feed-public.sandbox.exchange.coinbase.com",
heartbeat: %{type: :ping, interval: 30_000},
subscription_pattern: :type_subscribe,
subscription_config: %{
op_field: "type",
args_field: "product_ids",
args_format: :string_list,
channels_field: "channels"
},
auth_pattern: :inline_subscribe,
auth_config: %{}
},
"aster" => %{
public_url: "wss://sstream.asterdex.com/ws",
public_url_sandbox: nil,
private_url: "wss://sstream.asterdex.com/ws",
private_url_sandbox: nil,
heartbeat: %{type: :ping, interval: 180_000},
subscription_pattern: :method_subscribe,
subscription_config: %{separator: "@", market_id_format: :lowercase},
auth_pattern: :listen_key,
auth_config: %{
pre_auth: %{
type: :listen_key,
endpoints: %{spot: "privatePostUserDataStream"}
}
}
}
}
@doc "Returns the WS config map for the given exchange id, or nil if unsupported."
@spec for_exchange(String.t()) :: map() | nil
def for_exchange(id) when is_binary(id), do: Map.get(@configs, id)
@doc "Returns true if this exchange has a WS config entry."
@spec supported?(String.t()) :: boolean()
def supported?(id) when is_binary(id), do: Map.has_key?(@configs, id)
@doc "Returns all supported exchange ids."
@spec supported_exchanges() :: [String.t()]
def supported_exchanges, do: Map.keys(@configs)
end