Current section
Files
Jump to
Current section
Files
lib/ccxt/exchange/classification.ex
defmodule CCXT.Exchange.Classification do
@moduledoc """
Single source of truth for exchange classification based on CCXT metadata.
## Classification System
CCXT uses two properties to classify exchanges:
- `certified: true` - Exchange has paid for official CCXT integration (~22 exchanges)
- `pro: true` - Exchange has WebSocket/Pro API support (~68 exchanges)
We derive three mutually exclusive categories:
| Category | CCXT Criteria | Description |
|----------|---------------|-------------|
| **Certified Pro** | `certified: true` AND `pro: true` | Premium exchanges with full support |
| **Pro** | `pro: true` AND NOT `certified` | WebSocket support but not certified |
| **Supported** | Neither `certified` nor `pro` | Basic REST-only support |
## Testnet Support (Orthogonal Property)
Testnet/sandbox support is independent of classification:
CCXT.Exchange.Classification.has_testnet?("binance") # true
CCXT.Exchange.Classification.has_testnet?("kraken") # false
An exchange in any category may or may not have testnet support.
## Data Source
All data is auto-derived from `priv/extractor/ccxt_exchange_tiers.json`,
which is generated during `mix ccxt.sync` from CCXT's `describe()` metadata.
## Usage
# Get exchanges by classification
CCXT.Exchange.Classification.certified_pro_exchanges()
CCXT.Exchange.Classification.pro_exchanges()
CCXT.Exchange.Classification.supported_exchanges()
# Check individual exchange
CCXT.Exchange.Classification.certified?("binance")
CCXT.Exchange.Classification.pro?("binance")
CCXT.Exchange.Classification.has_testnet?("binance")
# Get exchanges suitable for integration testing
CCXT.Exchange.Classification.certified_pro_with_testnet()
## Expected Counts (from CCXT)
| Category | Count | Examples |
|----------|-------|----------|
| Certified Pro | ~22 | binance, bybit, okx, gate, kucoin, htx |
| Pro (not certified) | ~46 | kraken, deribit, bitstamp, coinbaseexchange |
| Supported | ~39 | bitfinex, bitflyer, etc. |
| Has Testnet | ~45 | binance, bybit, bitmex, deribit |
| **Total** | ~107 | |
"""
# ===========================================================================
# Compile-time data loading
# ===========================================================================
@classification_json_path "priv/extractor/ccxt_exchange_tiers.json"
# Load exchange classification from JSON at compile time
@external_resource @classification_json_path
{all_exchanges, certified_exchanges, pro_exchanges, testnet_exchanges} =
if File.exists?(@classification_json_path) do
# Use stdlib JSON (Elixir 1.18+) at compile time to avoid Jason dependency ordering issues
data = @classification_json_path |> File.read!() |> JSON.decode!()
{
Map.get(data, "all_exchanges", []),
Map.get(data, "certified_exchanges", []),
Map.get(data, "pro_exchanges", []),
Map.get(data, "testnet_exchanges", Map.get(data, "sandbox_exchanges", []))
}
else
# Fallback for fresh checkouts before first `mix ccxt.sync`
{[], [], [], []}
end
@all_exchanges all_exchanges
@certified_exchanges certified_exchanges
@pro_exchanges pro_exchanges
@testnet_exchanges testnet_exchanges
# ===========================================================================
# Derived classifications (computed at compile time)
# ===========================================================================
# Certified Pro: Both certified AND pro
@certified_pro_exchanges Enum.filter(@certified_exchanges, &Enum.member?(@pro_exchanges, &1))
# Pro only: Pro but NOT certified
@pro_only_exchanges @pro_exchanges -- @certified_exchanges
# Supported: Neither certified nor pro
@supported_exchanges @all_exchanges -- @pro_exchanges
# ===========================================================================
# Certified Pro Functions
# ===========================================================================
@doc """
Returns exchanges that are both CCXT certified AND have Pro support.
These are premium exchanges with full official CCXT integration.
## Examples
iex> "binance" in CCXT.Exchange.Classification.certified_pro_exchanges()
true
iex> "kraken" in CCXT.Exchange.Classification.certified_pro_exchanges()
false
"""
@spec certified_pro_exchanges() :: [String.t()]
def certified_pro_exchanges, do: @certified_pro_exchanges
@doc """
Returns the count of Certified Pro exchanges.
"""
@spec certified_pro_count() :: non_neg_integer()
def certified_pro_count, do: length(@certified_pro_exchanges)
@doc """
Checks if an exchange is Certified Pro (both certified and pro).
## Examples
iex> CCXT.Exchange.Classification.certified_pro?("binance")
true
iex> CCXT.Exchange.Classification.certified_pro?("kraken")
false
"""
@spec certified_pro?(String.t()) :: boolean()
def certified_pro?(exchange_id) do
Enum.member?(@certified_pro_exchanges, exchange_id)
end
@doc """
Returns Certified Pro exchanges as atoms.
"""
@spec certified_pro_atoms() :: [atom()]
# sobelow_skip ["DOS.StringToAtom"]
def certified_pro_atoms, do: Enum.map(@certified_pro_exchanges, &String.to_atom/1)
# ===========================================================================
# Pro Functions (pro but not certified)
# ===========================================================================
@doc """
Returns exchanges with Pro support but NOT certified.
These exchanges have WebSocket/streaming APIs but haven't paid for
official CCXT certification.
## Examples
iex> "kraken" in CCXT.Exchange.Classification.pro_exchanges()
true
iex> "binance" in CCXT.Exchange.Classification.pro_exchanges()
false
"""
@spec pro_exchanges() :: [String.t()]
def pro_exchanges, do: @pro_only_exchanges
@doc """
Returns the count of Pro (non-certified) exchanges.
"""
@spec pro_count() :: non_neg_integer()
def pro_count, do: length(@pro_only_exchanges)
@doc """
Checks if an exchange is Pro but not certified.
## Examples
iex> CCXT.Exchange.Classification.pro_only?("kraken")
true
iex> CCXT.Exchange.Classification.pro_only?("binance")
false
"""
@spec pro_only?(String.t()) :: boolean()
def pro_only?(exchange_id) do
Enum.member?(@pro_only_exchanges, exchange_id)
end
@doc """
Returns Pro exchanges as atoms.
"""
@spec pro_atoms() :: [atom()]
# sobelow_skip ["DOS.StringToAtom"]
def pro_atoms, do: Enum.map(@pro_only_exchanges, &String.to_atom/1)
# ===========================================================================
# Supported Functions (neither certified nor pro)
# ===========================================================================
@doc """
Returns exchanges that are neither certified nor pro.
These exchanges have basic REST-only support without WebSocket APIs.
## Examples
iex> "bitfinex" in CCXT.Exchange.Classification.supported_exchanges()
true
iex> "binance" in CCXT.Exchange.Classification.supported_exchanges()
false
"""
@spec supported_exchanges() :: [String.t()]
def supported_exchanges, do: @supported_exchanges
@doc """
Returns the count of Supported exchanges.
"""
@spec supported_count() :: non_neg_integer()
def supported_count, do: length(@supported_exchanges)
@doc """
Checks if an exchange is Supported (neither certified nor pro).
## Examples
iex> CCXT.Exchange.Classification.supported?("bitfinex")
true
iex> CCXT.Exchange.Classification.supported?("binance")
false
"""
@spec supported?(String.t()) :: boolean()
def supported?(exchange_id) do
Enum.member?(@supported_exchanges, exchange_id)
end
@doc """
Returns Supported exchanges as atoms.
"""
@spec supported_atoms() :: [atom()]
# sobelow_skip ["DOS.StringToAtom"]
def supported_atoms, do: Enum.map(@supported_exchanges, &String.to_atom/1)
# ===========================================================================
# General Functions
# ===========================================================================
@doc """
Returns the classification category for an exchange.
## Examples
iex> CCXT.Exchange.Classification.get_classification("binance")
:certified_pro
iex> CCXT.Exchange.Classification.get_classification("kraken")
:pro
iex> CCXT.Exchange.Classification.get_classification("bitfinex")
:supported
iex> CCXT.Exchange.Classification.get_classification("unknown")
:unknown
"""
@spec get_classification(String.t()) :: :certified_pro | :pro | :supported | :unknown
def get_classification(exchange_id) do
cond do
Enum.member?(@certified_pro_exchanges, exchange_id) -> :certified_pro
Enum.member?(@pro_only_exchanges, exchange_id) -> :pro
Enum.member?(@supported_exchanges, exchange_id) -> :supported
true -> :unknown
end
end
@doc """
Returns all exchange IDs from CCXT.
"""
@spec all_exchanges() :: [String.t()]
def all_exchanges, do: @all_exchanges
@doc """
Returns the total count of all exchanges.
"""
@spec all_count() :: non_neg_integer()
def all_count, do: length(@all_exchanges)
# ===========================================================================
# Raw CCXT Property Functions
# ===========================================================================
@doc """
Returns all exchanges with `certified: true` in CCXT.
Note: Most certified exchanges are also pro. Use `certified_pro_exchanges/0`
for the intersection, or this function if you need raw certified status.
"""
@spec certified_exchanges() :: [String.t()]
def certified_exchanges, do: @certified_exchanges
@doc """
Checks if an exchange has `certified: true` in CCXT.
"""
@spec certified?(String.t()) :: boolean()
def certified?(exchange_id) do
Enum.member?(@certified_exchanges, exchange_id)
end
@doc """
Returns all exchanges with `pro: true` in CCXT.
Note: This includes both certified pro and pro-only exchanges.
"""
@spec all_pro_exchanges() :: [String.t()]
def all_pro_exchanges, do: @pro_exchanges
@doc """
Checks if an exchange has `pro: true` in CCXT.
"""
@spec pro?(String.t()) :: boolean()
def pro?(exchange_id) do
Enum.member?(@pro_exchanges, exchange_id)
end
# ===========================================================================
# Testnet Functions (Orthogonal Property)
# ===========================================================================
@doc """
Returns all exchanges with testnet/sandbox support.
This is orthogonal to the classification - exchanges in any category
may or may not have testnet support.
"""
@spec testnet_exchanges() :: [String.t()]
def testnet_exchanges, do: @testnet_exchanges
@doc """
Returns the count of exchanges with testnet support.
"""
@spec testnet_count() :: non_neg_integer()
def testnet_count, do: length(@testnet_exchanges)
@doc """
Checks if an exchange has testnet/sandbox support.
## Examples
iex> CCXT.Exchange.Classification.has_testnet?("binance")
true
iex> CCXT.Exchange.Classification.has_testnet?("kraken")
false
"""
@spec has_testnet?(String.t()) :: boolean()
def has_testnet?(exchange_id) do
Enum.member?(@testnet_exchanges, exchange_id)
end
@doc """
Returns Certified Pro exchanges that have testnet support.
These are the best candidates for integration testing.
## Examples
iex> "binance" in CCXT.Exchange.Classification.certified_pro_with_testnet()
true
"""
@spec certified_pro_with_testnet() :: [String.t()]
def certified_pro_with_testnet do
Enum.filter(@certified_pro_exchanges, &has_testnet?/1)
end
@doc """
Returns Pro exchanges that have testnet support.
"""
@spec pro_with_testnet() :: [String.t()]
def pro_with_testnet do
Enum.filter(@pro_only_exchanges, &has_testnet?/1)
end
# ===========================================================================
# Priority Tier Functions (Orthogonal to CCXT Classification)
# ===========================================================================
#
# Priority tiers reflect trading importance (volume, liquidity, opportunity).
# This is SEPARATE from CCXT classification which reflects API capabilities.
#
# Use cases:
# - CCXT classification: "Does this exchange have WebSocket support?"
# - Priority tiers: "Should I focus development effort on this exchange?"
@priority_tiers_path "priv/priority_tiers.exs"
@external_resource @priority_tiers_path
{tier1_exchanges, tier2_exchanges, tier3_exchanges, dex_exchanges} =
if File.exists?(@priority_tiers_path) do
{data, _bindings} = Code.eval_file(@priority_tiers_path)
{
Map.get(data, :tier1, []),
Map.get(data, :tier2, []),
Map.get(data, :tier3, []),
Map.get(data, :dex, [])
}
else
# Fallback for fresh checkouts
{[], [], [], []}
end
@tier1_exchanges tier1_exchanges
@tier2_exchanges tier2_exchanges
@tier3_exchanges tier3_exchanges
@dex_exchanges dex_exchanges
@doc """
Returns Priority Tier 1 exchanges (must have, 80%+ of trading opportunity).
These are the exchanges that matter most for real trading:
binance, bybit, okx, deribit, coinbaseexchange
## Examples
iex> "binance" in CCXT.Exchange.Classification.tier1_exchanges()
true
iex> "kraken" in CCXT.Exchange.Classification.tier1_exchanges()
false
"""
@spec tier1_exchanges() :: [String.t()]
def tier1_exchanges, do: @tier1_exchanges
@doc """
Returns the count of Tier 1 exchanges.
"""
@spec tier1_count() :: non_neg_integer()
def tier1_count, do: length(@tier1_exchanges)
@doc """
Returns Priority Tier 2 exchanges (valuable, specific use cases).
These exchanges have good volume for specific pairs/markets:
kraken, kucoin, gate, htx, bitmex
## Examples
iex> "kraken" in CCXT.Exchange.Classification.tier2_exchanges()
true
iex> "binance" in CCXT.Exchange.Classification.tier2_exchanges()
false
"""
@spec tier2_exchanges() :: [String.t()]
def tier2_exchanges, do: @tier2_exchanges
@doc """
Returns the count of Tier 2 exchanges.
"""
@spec tier2_count() :: non_neg_integer()
def tier2_count, do: length(@tier2_exchanges)
@doc """
Returns Priority Tier 3 exchanges (low priority, explicitly deprioritized).
These exchanges have issues that make them less suitable for serious trading:
bitget, bingx, bitmart, coinex, cryptocom, mexc, hashkey, woo
## Examples
iex> "bitget" in CCXT.Exchange.Classification.tier3_exchanges()
true
iex> "binance" in CCXT.Exchange.Classification.tier3_exchanges()
false
"""
@spec tier3_exchanges() :: [String.t()]
def tier3_exchanges, do: @tier3_exchanges
@doc """
Returns the count of Tier 3 exchanges.
"""
@spec tier3_count() :: non_neg_integer()
def tier3_count, do: length(@tier3_exchanges)
@doc """
Returns DEX track exchanges (separate from CEX tiers).
DEXes have different infrastructure (WebSocket-first, ECDSA signing)
and are tracked separately from CEX priority tiers:
hyperliquid, aster, dydx, paradex, apex, woofipro, derive, modetrade
## Examples
iex> "hyperliquid" in CCXT.Exchange.Classification.dex_exchanges()
true
iex> "binance" in CCXT.Exchange.Classification.dex_exchanges()
false
"""
@spec dex_exchanges() :: [String.t()]
def dex_exchanges, do: @dex_exchanges
@doc """
Returns the count of DEX exchanges.
"""
@spec dex_count() :: non_neg_integer()
def dex_count, do: length(@dex_exchanges)
@doc """
Returns the priority tier for an exchange.
## Examples
iex> CCXT.Exchange.Classification.get_priority_tier("binance")
:tier1
iex> CCXT.Exchange.Classification.get_priority_tier("kraken")
:tier2
iex> CCXT.Exchange.Classification.get_priority_tier("bitget")
:tier3
iex> CCXT.Exchange.Classification.get_priority_tier("hyperliquid")
:dex
iex> CCXT.Exchange.Classification.get_priority_tier("unknown_exchange")
:unclassified
"""
@spec get_priority_tier(String.t()) :: :tier1 | :tier2 | :tier3 | :dex | :unclassified
def get_priority_tier(exchange_id) do
cond do
exchange_id in @tier1_exchanges -> :tier1
exchange_id in @tier2_exchanges -> :tier2
exchange_id in @tier3_exchanges -> :tier3
exchange_id in @dex_exchanges -> :dex
true -> :unclassified
end
end
@doc """
Checks if an exchange is in Priority Tier 1.
## Examples
iex> CCXT.Exchange.Classification.tier1?("binance")
true
iex> CCXT.Exchange.Classification.tier1?("kraken")
false
"""
@spec tier1?(String.t()) :: boolean()
def tier1?(exchange_id), do: exchange_id in @tier1_exchanges
@doc """
Checks if an exchange is in Priority Tier 2.
## Examples
iex> CCXT.Exchange.Classification.tier2?("kraken")
true
iex> CCXT.Exchange.Classification.tier2?("binance")
false
"""
@spec tier2?(String.t()) :: boolean()
def tier2?(exchange_id), do: exchange_id in @tier2_exchanges
@doc """
Checks if an exchange is in Priority Tier 3.
## Examples
iex> CCXT.Exchange.Classification.tier3?("bitget")
true
iex> CCXT.Exchange.Classification.tier3?("binance")
false
"""
@spec tier3?(String.t()) :: boolean()
def tier3?(exchange_id), do: exchange_id in @tier3_exchanges
@doc """
Checks if an exchange is a DEX.
## Examples
iex> CCXT.Exchange.Classification.dex?("hyperliquid")
true
iex> CCXT.Exchange.Classification.dex?("binance")
false
"""
@spec dex?(String.t()) :: boolean()
def dex?(exchange_id), do: exchange_id in @dex_exchanges
@doc """
Returns Tier 1 exchanges that have testnet support.
These are ideal for integration testing - high priority AND testable.
"""
@spec tier1_with_testnet() :: [String.t()]
def tier1_with_testnet do
Enum.filter(@tier1_exchanges, &has_testnet?/1)
end
@doc """
Returns Tier 1 exchanges that are also Certified Pro in CCXT.
Intersection of priority (trading importance) and quality (API support).
"""
@spec tier1_certified_pro() :: [String.t()]
def tier1_certified_pro do
Enum.filter(@tier1_exchanges, &certified_pro?/1)
end
# ===========================================================================
# Tier Helper Functions (for Mix tasks)
# ===========================================================================
@doc """
Returns exchanges for a given priority tier.
## Examples
iex> "binance" in CCXT.Exchange.Classification.exchanges_for_tier(:tier1)
true
iex> "kraken" in CCXT.Exchange.Classification.exchanges_for_tier(:tier2)
true
"""
@spec exchanges_for_tier(:tier1 | :tier2 | :tier3 | :dex) :: [String.t()]
def exchanges_for_tier(:tier1), do: @tier1_exchanges
def exchanges_for_tier(:tier2), do: @tier2_exchanges
def exchanges_for_tier(:tier3), do: @tier3_exchanges
def exchanges_for_tier(:dex), do: @dex_exchanges
@doc """
Returns the display name for a priority tier (uppercase).
## Examples
iex> CCXT.Exchange.Classification.tier_display_name(:tier1)
"TIER 1"
iex> CCXT.Exchange.Classification.tier_display_name(:dex)
"DEX"
"""
@spec tier_display_name(:tier1 | :tier2 | :tier3 | :dex) :: String.t()
def tier_display_name(:tier1), do: "TIER 1"
def tier_display_name(:tier2), do: "TIER 2"
def tier_display_name(:tier3), do: "TIER 3"
def tier_display_name(:dex), do: "DEX"
@doc """
Collects exchanges from multiple tier flags.
Takes a keyword list of options and returns exchanges for all enabled tiers.
## Examples
iex> opts = [tier1: true, tier2: true]
iex> exchanges = CCXT.Exchange.Classification.collect_tier_exchanges(opts)
iex> "binance" in exchanges
true
iex> "kraken" in exchanges
true
"""
@spec collect_tier_exchanges(keyword()) :: {[String.t()], String.t()}
def collect_tier_exchanges(opts) do
tiers =
Enum.filter([:tier1, :tier2, :tier3, :dex], fn tier -> opts[tier] end)
exchanges =
tiers
|> Enum.flat_map(&exchanges_for_tier/1)
|> Enum.uniq()
|> Enum.sort()
label = build_tier_label(tiers, length(exchanges))
{exchanges, label}
end
@doc """
Checks if any tier flag is set in the options.
## Examples
iex> CCXT.Exchange.Classification.has_tier_flags?(tier1: true)
true
iex> CCXT.Exchange.Classification.has_tier_flags?(all: true)
false
"""
@spec has_tier_flags?(keyword()) :: boolean()
def has_tier_flags?(opts) do
opts[:tier1] || opts[:tier2] || opts[:tier3] || opts[:dex] || false
end
@doc false
@spec build_tier_label([atom()], non_neg_integer()) :: String.t()
def build_tier_label(tiers, count) do
tier_names = Enum.map_join(tiers, " + ", &tier_display_name/1)
"#{tier_names} (#{count})"
end
end