# `CCXT.WS`
[🔗](https://github.com/ZenHive/ccxt_client/blob/main/lib/ccxt/ws.ex#L1)

WebSocket entry point. Thin wrapper around `ZenWebsocket.Client` that binds
a `%CCXT.Exchange{}` to a connection so `subscribe/3` can pick the correct
exchange-native frame builder.

## Layer 1+2 (Task 92)

Pure URL resolution (`CCXT.WS.URLRouting`) + connection lifecycle (this module).
Layer 3 (auth state machine, custom reconnection) is deliberately deferred —
`zen_websocket` covers reconnection, backoff, heartbeat, and subscription
restoration natively.

## Usage

    {:ok, ws} = CCXT.WS.connect(exchange, :public)
    :ok = CCXT.WS.subscribe(ws, ["tickers.BTCUSDT"])
    # Messages arrive at the calling process as {:websocket_message, decoded_map}
    CCXT.WS.close(ws)

For deribit's JSON-RPC subscribe (which carries an `id` field), `subscribe/3`
returns `{:ok, response}` from zen_websocket's request-correlation; for bybit
and okx it returns `:ok` and the subscribe-ack arrives asynchronously as a
`{:websocket_message, _}` message.

## Scope

Three canary exchanges are wired today: `bybit`, `deribit`, `okx`. Other
priority-tier exchanges land in T93 (auth) + T94 (subscription patterns).

# `section`

```elixir
@type section() :: :public | :private
```

# `t`

```elixir
@type t() :: %CCXT.WS{
  exchange: CCXT.Exchange.t(),
  section: section(),
  url: String.t(),
  zen_client: ZenWebsocket.Client.t()
}
```

# `close`

```elixir
@spec close(t()) :: :ok
```

Closes the WebSocket connection.

# `connect`

```elixir
@spec connect(CCXT.Exchange.t(), section(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Connects to the exchange's WebSocket endpoint for the given section
(`:public` or `:private`).

Extra opts are forwarded to `ZenWebsocket.Client.connect/2`. The connection's
heartbeat config is resolved from `CCXT.WS.Config` unless the caller overrides
`heartbeat_config` in opts.

Returns `{:error, :unsupported_exchange}` if the exchange has no WS config,
or `{:error, :no_url_configured}` if the requested section is absent.

# `get_state`

```elixir
@spec get_state(t()) :: :connecting | :connected | :disconnected
```

Returns the current connection state (`:connecting`, `:connected`, or `:disconnected`).

# `get_url`

```elixir
@spec get_url(t()) :: String.t()
```

Returns the resolved WS URL this connection is using.

# `send_message`

```elixir
@spec send_message(t(), String.t() | map()) :: :ok | {:ok, map()} | {:error, term()}
```

Sends a raw (already-encoded or map) payload. Delegates to zen_websocket.

# `subscribe`

```elixir
@spec subscribe(t(), [String.t() | map()], keyword() | map()) ::
  :ok | {:ok, map()} | {:error, term()}
```

Sends an exchange-native subscribe frame for the given channels.

The frame is built by the exchange's registered `subscription_pattern` module
(via `CCXT.WS.Subscription.build_subscribe/3`), encoded as JSON, and sent via
`ZenWebsocket.Client.send_message/2`.

Pattern modules return either a single map (most exchanges) or a list of maps
(`:sub_subscribe`, `:reqtype_sub`, and `:custom` with `array_format` —
HTX/BingX/Upbit emit one frame per channel). List returns are sent
sequentially; the function returns `:ok` only if every frame sent successfully.

For frames with an `id` field (deribit JSON-RPC), `send_message/2` blocks on
the correlated response and returns `{:ok, response}`. For frames without an
id (bybit, okx, etc.), returns `:ok`.

Extra `opts` merge into the exchange's `subscription_config` from
`CCXT.WS.Config` — used for runtime overrides like a fresh JSON-RPC id or an
inline auth token. Keys must be atoms to override the atom-keyed base config;
string-keyed maps coexist rather than override.

TODO(adapter): `:rest_token` (kraken) and `:inline_subscribe` (coinbase) auth
patterns require per-frame auth injection via `CCXT.WS.Auth.build_subscribe_auth/5`,
which this function does not call. Private subscribes on those exchanges ship
unauthenticated until the adapter layer lands (see CHANGELOG T94).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
