Current section
Files
Jump to
Current section
Files
lib/ccxt.ex
defmodule CCXT do
@moduledoc """
Unified cryptocurrency exchange client library.
A CCXT client in Elixir scoped to the priority-tier universe (Tier 1 +
Tier 2 + DEX). Exchange modules are generated from specs via macros — new
exchange = new spec file, zero Elixir code. Tier 3 / unclassified
exchanges are available on demand via `mix ccxt_extract.update --exchange <id>`.
## Architecture
CCXT.fetch_ticker(exchange, "BTC/USDT") # Unified API
→ CCXT.Bybit (generated module) # use CCXT.Exchange, spec: "bybit"
→ CCXT.Dispatch.call/4 # Shared dispatcher
→ CCXT.Signing.sign/4 # 9 patterns
→ CCXT.HTTP.request/4 # Req wrapper
## Quick Start
# List available exchanges
CCXT.Spec.exchanges()
# Create an exchange (public-only)
{:ok, bybit} = CCXT.exchange("bybit")
# Create with credentials
{:ok, bybit} = CCXT.exchange("bybit", api_key: "abc", secret: "xyz")
# TODO: Phase 5 parsers will return typed structs; currently returns raw HTTP response
{:ok, response} = CCXT.fetch_ticker(bybit, "BTC/USDT")
# Create an order (private, requires credentials)
{:ok, response} = CCXT.create_order(bybit, "BTC/USDT", "limit", "buy", 0.001, price: 50000)
## Optional Parameters
All unified functions accept an `opts` keyword list as the last argument.
Optional CCXT parameters (since, limit, price, symbol when optional, etc.)
and exchange-specific parameters are passed via opts:
CCXT.fetch_trades(exchange, "BTC/USDT", since: 1_700_000_000_000, limit: 100)
CCXT.create_order(exchange, "BTC/USDT", "limit", "buy", 0.01, price: 50000)
CCXT.fetch_balance(exchange, type: "spot")
Dispatch-level options (`:endpoint_index`, `:timeout`, `:plug`, `:headers`,
`:base_url`) are separated automatically and do not get passed to the
exchange as parameters.
"""
use Descripex
use Descripex.Discoverable, modules: [CCXT, CCXT.Exchange, CCXT.Symbol, CCXT.HTTP]
alias CCXT.Exchange
alias CCXT.Unified
# ===========================================================================
# Exchange Constructor
# ===========================================================================
api(:exchange, "Create an exchange configuration.",
params: [
exchange_id: [kind: :value, description: "Exchange identifier (e.g., \"bybit\", :binance)"]
],
opts: [
api_key: [kind: :value, description: "API key for authenticated endpoints"],
secret: [kind: :value, description: "API secret"],
password: [kind: :value, description: "API password/passphrase (exchange-specific)"],
sandbox: [kind: :value, description: "Use testnet URLs", default: false]
],
returns: %{
type: :result_tuple,
description:
~s|{:ok, %CCXT.Exchange{} with :tier populated from spec ("tier1"/"tier2"/"dex"/"tier3"/"unclassified"/nil)} or {:error, reason}|
},
errors: [:invalid_exchange, :missing_credentials]
)
@spec exchange(String.t() | atom(), keyword()) :: {:ok, Exchange.t()} | {:error, term()}
defdelegate exchange(exchange_id, opts \\ []), to: Exchange, as: :new
@doc "Bang variant of `exchange/2`. Raises on error."
@spec exchange!(String.t() | atom(), keyword()) :: Exchange.t()
defdelegate exchange!(exchange_id, opts \\ []), to: Exchange, as: :new!
# ===========================================================================
# Generated Unified API
#
# Unified methods generated from CCXT.Unified.method_defs/0.
# Each method gets a standard function (with api() declaration) + bang variant.
#
# Signature: func(exchange, ...required_params, opts \\ [])
# Returns: {:ok, map()} | {:error, CCXT.Error.t()}
# Bang: func!(exchange, ...required_params, opts \\ [])
# Returns: map() | raises CCXT.Error
# ===========================================================================
for {name, js_name, required_params, description} <- Unified.method_defs() do
param_vars = Enum.map(required_params, &Macro.var(&1, __MODULE__))
param_names = required_params
# Descripex declarations via direct attribute emission for compile-time loops.
# TODO(Task 36): Add Descripex.emit_api/3 upstream to support for-comprehension usage
api_opts = [
params:
[exchange: [kind: :value, description: "Exchange configuration struct"]] ++
Enum.map(required_params, fn p ->
{p, [kind: :value, description: p |> Atom.to_string() |> String.replace("_", " ")]}
end),
returns: %{
type: :result_tuple,
description: "{:ok, map()} on success, {:error, CCXT.Error.t()} on failure"
},
errors: [:not_supported, :authentication_error, :rate_limit_exceeded, :network_error]
]
@descripex_api_declarations {name, description, api_opts}
@doc Descripex.generate_doc(description, api_opts)
@doc hints: Descripex.build_hints(description, api_opts)
def unquote(name)(%Exchange{} = exchange, unquote_splicing(param_vars), opts \\ []) do
{dispatch_opts, extra} = Unified.split_opts(opts)
required_values = [unquote_splicing(param_vars)]
params = Unified.build_params(unquote(param_names), required_values, extra)
Unified.call(exchange, unquote(name), unquote(js_name), params, dispatch_opts)
end
# Bang variant — @doc false, convenience wrapper
bang_name = :"#{name}!"
@doc false
def unquote(bang_name)(%Exchange{} = exchange, unquote_splicing(param_vars), opts \\ []) do
case unquote(name)(exchange, unquote_splicing(param_vars), opts) do
{:ok, response} -> response
{:error, error} -> raise error
end
end
end
end