README.md

# ccxt_client

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

**Status:** `0.x` — raw endpoint surface is stable; unified translation layer is evolving.

## Installation

```elixir
def deps do
  [
    {:ccxt_client, "~> 0.6"}
  ]
end
```

## Two Contracts

`ccxt_client` exposes two API surfaces with different stability guarantees.

### Raw Endpoints — Stable

Per-exchange generated functions that pass through to the exchange API unchanged. Signing, rate limiting, circuit breakers, and transport are handled; the response body is returned as-is.

```elixir
{:ok, exchange} = CCXT.Exchange.new("bybit", api_key: key, secret: secret)
{:ok, response} = CCXT.Bybit.public_get_v5_market_tickers(exchange, %{category: "spot"})
```

Recommended for agents, automated trading, and any consumer that wants to interpret exchange responses directly.

### Unified API — Evolving

Standardized cross-exchange methods. Today most methods still return the raw HTTP response map (`%{status, body, headers}`); normalized structs arrive with Phase 5 parsers (see [ROADMAP.md](ROADMAP.md) — gated on upstream ccxt_extract Phase 12).

```elixir
{:ok, %{status: 200, body: body}} = CCXT.fetch_time(exchange)
```

Zero-arg public methods (`fetch_time`, `fetch_status`, `fetch_currencies`, `fetch_markets`) are reliable across the priority-tier set. Symbol-bearing methods (`fetch_ticker`, `fetch_order_book`, `fetch_trades`) have per-exchange gaps — some exchanges need market-category params that the unified layer doesn't yet inject. Prefer the raw surface for those until Phase 5 lands.

### WebSocket — Early (T92–T94 landed 2026-04-18/19)

```elixir
{: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)
```

Thin wrapper over [`zen_websocket`](https://hex.pm/packages/zen_websocket) driven by per-exchange subscription + auth patterns. 13 exchanges configured; live tidewave smokes through 0.6.0 (post Bundle A / T101 / T102) confirm stream end-to-end on the full configured set — except `kucoin`, whose WSS host + token come from a REST pre-call that the adapter layer will wire under T97. Callers today pass pre-formatted exchange-native channel strings; spec-driven channel formatting is T97 (upstream-gated). See [ROADMAP.md](ROADMAP.md) for the WebSocket track status.

## Known Caveats

Consumer-facing gotchas not obvious from the API signatures. Full context in [CLAUDE.md](CLAUDE.md) and [ROADMAP.md](ROADMAP.md).

- **`has?/2` is advertising, not a dispatch guarantee.** `CCXT.has?(exchange, "fetchX")` reflects what upstream CCXT declares the exchange *supports in principle* — it does not promise that `ccxt_client` has an endpoint mapping wired for that method today. 116 such gaps exist across the 23 priority-tier exchanges (matches CCXT JS semantics: endpoint map is the dispatch gate, `has` is advertising). When in doubt, cross-check with `CCXT.<Exchange>.__unified_endpoints__/0`; for production workloads prefer the raw per-exchange surface.

- **`:custom` signing — 3 exchanges need a consumer-supplied module.** `derive`, `hyperliquid`, and `lighter` classify as `:custom` signing and do not ship with a bundled signer. Public endpoints work out of the box; **private** endpoints raise `ArgumentError` unless you provide a `custom_module:` implementing `CCXT.Signing.Behaviour`. Example:

  ```elixir
  {:ok, ex} = CCXT.Exchange.new("hyperliquid",
    api_key: "…",
    secret: "…",
    signing: %{pattern: :custom, custom_module: MyApp.HyperliquidSigner}
  )
  ```

  A consumer-side fix is unblocked today; upstream-driven convergence is gated on ccxt_extract Phase 10 exotic.

- **No testnet for 7 exchanges.** `Exchange.new(id, sandbox: true)` returns `{:error, :no_testnet_data}` (and `Exchange.new!` raises `ArgumentError`) for `aster`, `bitfinex`, `htx`, `huobi`, `kraken`, `kucoin`, `kucoinfutures` — those exchanges don't publish a public testnet. Use production URLs with small-size orders and strict risk limits, or pick a different exchange for dry-run integration.

## Discovery

```elixir
CCXT.describe()                      # Library overview
CCXT.describe(CCXT, :fetch_ticker)   # Method signature + params + errors + return shape
CCXT.MCP.tools()                     # MCP tool definitions for agent autodiscovery
CCXT.Registry.exchanges()            # List compiled exchange ids
```

Per-exchange introspection (generated on every exchange module):

```elixir
CCXT.Bybit.__spec__()               # Raw spec map (describe output)
CCXT.Bybit.__endpoints__()          # List of %{path, method, authenticated, weight, ...}
CCXT.Bybit.__signing__()            # %{pattern: :hmac_sha256_headers, config: %{...}}
CCXT.Bybit.__tier__()               # "tier1" | "tier2" | "dex"
CCXT.Bybit.__unified_endpoints__()  # Unified-method → endpoint-config mapping
```

## Documentation

- [ROADMAP.md](ROADMAP.md) — active work, priorities, upstream dependencies
- [CHANGELOG.md](CHANGELOG.md) — completed tasks and known limitations
- [CLAUDE.md](CLAUDE.md) — architecture, design decisions, internal conventions
- [CONTRIBUTING.md](CONTRIBUTING.md) — how to land fixes, including exchange overrides

## Fixing Exchange Bugs Upstream

When an exchange behaves incorrectly (wrong URL, wrong field name, missing auth section, bad error sentinel), fix it in the spec via an **upstream override** rather than patching `ccxt_client`. Overrides live in the `ccxt_extract` repo under `priv/overrides/<exchange>.json` and apply at any JSON Pointer via `OverrideRegistry.apply_all/2`. Every overridden path is tagged `"override"` in the spec's top-level `_provenance` map (schema 2.0.0+), so consumers can see exactly which fields diverge from raw extraction.

See [CONTRIBUTING.md § Exchange Overrides](CONTRIBUTING.md#exchange-overrides) and `ccxt_extract/SCHEMA.md` § Override Contract for the authoritative format and workflow.

## Development

```bash
mix deps.get
mix test.json --quiet
mix dialyzer.json --quiet
mix credo --strict --format json
```