Current section
Files
Jump to
Current section
Files
lib/ccxt/extract/ws_handler_mappings.ex
defmodule CCXT.Extract.WsHandlerMappings do
@moduledoc """
Loads WS handler dispatch mappings extracted from CCXT Pro TypeScript files.
Each exchange's `handleMessage()` method contains a Dict mapping channel names
to handler functions. This module loads those mappings and provides query APIs
that map handler names to W12 payload families.
Data source: `priv/extractor/ccxt_ws_handler_mappings.json`.
## Data Flow
extract-ws-handlers.cjs (static TS analysis)
→ ccxt_ws_handler_mappings.json
→ WsHandlerMappings.load/0
→ query APIs (channel_to_family, families_supported, etc.)
"""
use CCXT.Extract.JsonLoader, file: "ccxt_ws_handler_mappings.json"
# Handler method name → W12 family atom.
# Non-family handlers (auth, pong, subscription management) map to nil.
@handler_to_family %{
# Ticker family
"handleTicker" => :watch_ticker,
"handleTickers" => :watch_ticker,
"handleBidAsk" => :watch_ticker,
"handleBidsAsks" => :watch_ticker,
"handleMarkPrices" => :watch_ticker,
"handleWsTickers" => :watch_ticker,
"handleMarketData" => :watch_ticker,
"handlePricePointUpdates" => :watch_ticker,
# Trades family
"handleTrades" => :watch_trades,
"handleTrade" => :watch_trades,
"handleTradesSnapshot" => :watch_trades,
"handleMyTrade" => :watch_trades,
"handleMyTrades" => :watch_trades,
# Order book family
"handleOrderBook" => :watch_order_book,
"handleOrderBookUpdate" => :watch_order_book,
"handleOrderBookSnapshot" => :watch_order_book,
"handleOrderBookPartialSnapshot" => :watch_order_book,
"handleL2Updates" => :watch_order_book,
"handleChecksum" => :watch_order_book,
# OHLCV family
"handleOHLCV" => :watch_ohlcv,
"handleOHLCV1m" => :watch_ohlcv,
"handleOHLCV24" => :watch_ohlcv,
"handleInitOHLCV" => :watch_ohlcv,
"handleFetchOHLCV" => :watch_ohlcv,
# Orders family (private)
"handleOrders" => :watch_orders,
"handleOrder" => :watch_orders,
"handleOrderUpdate" => :watch_orders,
"handleMyOrder" => :watch_orders,
"handleSingleOrder" => :watch_orders,
"handleMultipleOrders" => :watch_orders,
"handleOrderRequest" => :watch_orders,
# Balance family (private)
"handleBalance" => :watch_balance,
"handleAcountUpdate" => :watch_balance,
"handleBalanceAndPosition" => :watch_balance,
"handleBalanceSnapshot" => :watch_balance,
"handleAccount" => :watch_balance,
"handleAccountUpdate" => :watch_balance,
"handleFetchBalance" => :watch_balance,
# Positions family (private)
"handlePositions" => :watch_positions,
# Non-family handlers → nil (auth, subscription, system, etc.)
"handleAuthenticate" => nil,
"handleAuthenticationMessage" => nil,
"handleAuth" => nil,
"handlePong" => nil,
"handlePing" => nil,
"handleHeartbeat" => nil,
"handleConnected" => nil,
"handleSubscriptionStatus" => nil,
"handleSubscribe" => nil,
"handleSubscribed" => nil,
"handleSubscription" => nil,
"handleSubscriptions" => nil,
"handleSubscriptionResponse" => nil,
"handleUnSubscribe" => nil,
"handleUnsubscribe" => nil,
"handleUnsubscription" => nil,
"handleUnSubscription" => nil,
"handleUnsubscriptionStatus" => nil,
"handleSystemStatus" => nil,
"handleInfo" => nil,
"handleSubject" => nil,
"handleLiquidation" => nil,
"handleFundingRate" => nil,
"handleOrderWs" => nil,
"handlePlaceOrders" => nil,
"handleCancelOrder" => nil,
"handleCancelAllOrders" => nil,
"handleCreateEditOrder" => nil,
"handleEventStreamTerminated" => nil,
"handleTrading" => nil,
"handleTradingFees" => nil,
"handleTransaction" => nil,
"handleDeposits" => nil,
"handleWithdraw" => nil,
"handleWithdraws" => nil,
"handleWsPost" => nil,
"handleMarkets" => nil,
"handleFetchCurrencies" => nil,
"handleErrorMessage" => nil,
"safeValue" => nil,
"apiKey" => nil,
"nonce" => nil,
"resolveData" => nil
}
# Handlers that dispatch to MULTIPLE families (e.g., handleAcountUpdate calls
# both handleBalance and handlePositions). handler_to_family/1 returns only
# the first/primary family; handler_to_families/1 returns all of them.
@multi_family_handlers %{
"handleAcountUpdate" => [:watch_balance, :watch_positions],
"handleBalanceAndPosition" => [:watch_balance, :watch_positions]
}
@known_families [
:watch_ticker,
:watch_trades,
:watch_order_book,
:watch_ohlcv,
:watch_orders,
:watch_balance,
:watch_positions
]
@doc "Returns all exchange IDs with handler mappings."
@spec exchanges() :: [String.t()]
def exchanges do
load()
|> Map.get("exchanges", [])
|> Enum.map(&Map.get(&1, "exchange"))
|> Enum.sort()
end
@doc "Returns the full mapping data for a single exchange."
@spec get(String.t()) :: map() | nil
def get(exchange_id) do
load()
|> Map.get("exchanges", [])
|> Enum.find(fn e -> Map.get(e, "exchange") == exchange_id end)
end
@doc "Returns the envelope pattern config for an exchange."
@spec envelope_pattern(String.t()) :: map() | nil
def envelope_pattern(exchange_id) do
case get(exchange_id) do
nil -> nil
data -> Map.get(data, "envelope")
end
end
@doc ~s{Returns the match_type for an exchange ("exact", "exact_then_substring", "split"), or nil if unknown.}
@spec match_type(String.t()) :: String.t() | nil
def match_type(exchange_id) do
case get(exchange_id) do
nil -> nil
data -> Map.get(data, "match_type")
end
end
@doc "Returns the raw handler entries list for an exchange, or [] if unknown."
@spec handlers(String.t()) :: [map()]
def handlers(exchange_id) do
case get(exchange_id) do
nil -> []
data -> Map.get(data, "handlers", [])
end
end
@doc """
Resolves a channel name to a W12 family for a given exchange.
Returns the family atom (e.g., `:watch_ticker`) or `nil` for non-family
handlers (auth, pong, subscription management, etc.).
"""
@spec channel_to_family(String.t(), String.t()) :: atom() | nil
def channel_to_family(exchange_id, channel_name) do
case get(exchange_id) do
nil ->
nil
data ->
handler = find_handler_for_channel(data, channel_name)
if handler, do: handler_to_family(handler)
end
end
@doc """
Resolves a channel name with tri-state semantics.
Unlike `channel_to_family/2`, this distinguishes between:
- `{:family, atom()}` — Handler found, maps to a known family
- `:system` — Handler found, but non-family (auth, pong, subscription)
- `:not_found` — No handler matches this channel at all
## Examples
iex> CCXT.Extract.WsHandlerMappings.resolve_channel("bybit", "orderbook")
{:family, :watch_order_book}
iex> CCXT.Extract.WsHandlerMappings.resolve_channel("bybit", "pong")
:system
iex> CCXT.Extract.WsHandlerMappings.resolve_channel("bybit", "totally_unknown")
:not_found
"""
@spec resolve_channel(String.t(), String.t()) :: {:family, atom()} | :system | :not_found
def resolve_channel(exchange_id, channel_name) do
case get(exchange_id) do
nil ->
:not_found
data ->
case find_handler_for_channel(data, channel_name) do
nil -> :not_found
handler -> resolve_handler(handler)
end
end
end
@doc false
# Maps a handler name to {:family, atom} or :system
defp resolve_handler(handler_name) do
case handler_to_family(handler_name) do
nil -> :system
family -> {:family, family}
end
end
@doc """
Returns which W12 families an exchange has handlers for.
Only includes families with actual handlers (excludes nil/non-family handlers).
"""
@spec families_supported(String.t()) :: [atom()]
def families_supported(exchange_id) do
case get(exchange_id) do
nil ->
[]
data ->
data
|> Map.get("handlers", [])
|> Enum.flat_map(fn h -> handler_to_families(Map.get(h, "handler")) end)
|> Enum.uniq()
|> Enum.sort()
end
end
@doc """
Returns all channel names that map to a specific family for an exchange.
"""
@spec channels_for_family(String.t(), atom()) :: [String.t()]
def channels_for_family(exchange_id, family) when family in @known_families do
case get(exchange_id) do
nil ->
[]
data ->
data
|> Map.get("handlers", [])
|> Enum.filter(fn h -> family in handler_to_families(Map.get(h, "handler")) end)
|> Enum.map(&Map.get(&1, "channel"))
end
end
@doc "Returns the known handler→family mapping table."
@spec handler_family_map() :: %{String.t() => atom() | nil}
def handler_family_map, do: @handler_to_family
@doc "Returns the list of known W12 families."
@spec known_families() :: [atom()]
def known_families, do: @known_families
@doc """
Maps a handler method name to a W12 family atom.
Returns nil for non-family handlers or unknown handlers.
For handlers that dispatch to multiple families (e.g., `handleAcountUpdate`),
returns only the primary family. Use `handler_to_families/1` for the full list.
"""
@spec handler_to_family(String.t()) :: atom() | nil
def handler_to_family(handler_name) do
Map.get(@handler_to_family, handler_name)
end
@doc """
Maps a handler method name to ALL W12 families it dispatches to.
Returns a list of family atoms. For most handlers this is a single-element list.
For composite handlers like `handleAcountUpdate` (which calls both `handleBalance`
and `handlePositions`), returns all families.
Returns `[]` for non-family handlers (auth, pong, etc.) or unknown handlers.
"""
@spec handler_to_families(String.t()) :: [atom()]
def handler_to_families(handler_name) do
case Map.get(@multi_family_handlers, handler_name) do
nil ->
case handler_to_family(handler_name) do
nil -> []
family -> [family]
end
families ->
families
end
end
# -- Private Helpers -------------------------------------------------------
@doc false
# Finds the handler method name for a channel in an exchange's data.
# Respects the exchange's match_type to avoid false-positive substring matches.
# Also handles per-handler prefix matching (e.g., OKX "candle" matches "candle1m").
defp find_handler_for_channel(data, channel_name) do
handlers = Map.get(data, "handlers", [])
match_type = Map.get(data, "match_type", "exact")
# Try exact match first (all match types start with exact)
exact = Enum.find(handlers, fn h -> Map.get(h, "channel") == channel_name end)
if exact do
Map.get(exact, "handler")
else
# Try exchange-level fallback strategy
result =
case match_type do
"exact_then_substring" ->
find_by_substring(handlers, channel_name)
"split" ->
find_by_split(handlers, channel_name)
_ ->
nil
end
# If exchange-level fallback didn't match, try per-handler prefix entries
result || find_by_prefix(handlers, channel_name)
end
end
@doc false
# Substring fallback: channel_name contains the handler's channel key
defp find_by_substring(handlers, channel_name) do
match =
Enum.find(handlers, fn h ->
channel = Map.get(h, "channel", "")
channel != "" and String.contains?(channel_name, channel)
end)
if match, do: Map.get(match, "handler")
end
@doc false
# Prefix fallback: matches handler entries tagged with match_type "prefix"
# (e.g., OKX "candle" matches "candle1m", "candle5m", etc.)
defp find_by_prefix(handlers, channel_name) do
match =
Enum.find(handlers, fn h ->
Map.get(h, "match_type") == "prefix" and
String.starts_with?(channel_name, Map.get(h, "channel", ""))
end)
if match, do: Map.get(match, "handler")
end
@doc false
# Split fallback: split channel_name on "." and try each part
defp find_by_split(handlers, channel_name) do
parts = String.split(channel_name, ".")
match =
Enum.find_value(parts, fn part ->
Enum.find(handlers, fn h -> Map.get(h, "channel") == part end)
end)
if match, do: Map.get(match, "handler")
end
end