# 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
```