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