defmodule CCXT.Unified do @moduledoc false # Internal: method definitions and dispatch for unified API functions. # # The @method_defs list is the single source of truth for unified methods. # Each entry: {elixir_name, js_capability_name, required_params, description}. # CCXT module generates functions + bang variants + api() declarations from # this list at compile time. # # Optional params (since, limit, price, tag, symbols, etc.) go in opts. alias CCXT.Dispatch alias CCXT.Error alias CCXT.Exchange alias CCXT.Registry alias CCXT.Symbol # Dispatch-level opts separated from exchange params @dispatch_opts [:endpoint_index, :timeout, :plug, :headers, :base_url] # =========================================================================== # Method Definitions # # {elixir_name, js_capability_name, required_params, description} # # Signature convention: # - Required positional args only (exchange is implicit, opts is appended) # - Optional CCXT params (since, limit, price, symbol when optional) → opts # - Generated function: func(exchange, ...required, opts \\ []) # =========================================================================== # Curated descriptions for the most-used unified methods. # Remaining methods get auto-generated descriptions via description_for/1. @curated_descriptions %{ fetch_ticker: "Fetch latest ticker (price, volume, bid/ask) for a trading pair.", fetch_tickers: "Fetch tickers for all or specified trading pairs.", fetch_order_book: "Fetch the order book (bids and asks) for a symbol.", fetch_trades: "Fetch recent public trades for a symbol.", fetch_ohlcv: "Fetch OHLCV candlestick data for a symbol and timeframe.", fetch_markets: "Fetch all available markets and trading pairs.", fetch_currencies: "Fetch all available currencies and their details.", fetch_time: "Fetch the exchange server time.", fetch_status: "Fetch the exchange operational status.", create_order: "Create a new order on the exchange.", create_orders: "Create multiple orders in a single request.", cancel_order: "Cancel an existing order by ID.", cancel_all_orders: "Cancel all open orders, optionally filtered by symbol.", edit_order: "Edit an existing order (modify price, amount, etc.).", fetch_order: "Fetch details of a specific order by ID.", fetch_orders: "Fetch a list of orders, optionally filtered by symbol.", fetch_open_orders: "Fetch all currently open orders.", fetch_closed_orders: "Fetch completed (filled) orders.", fetch_balance: "Fetch account balance across all currencies.", fetch_my_trades: "Fetch the authenticated user's trade history.", fetch_positions: "Fetch all open derivative positions.", fetch_position: "Fetch a specific derivative position for a symbol.", set_leverage: "Set leverage for a symbol on the exchange.", set_margin_mode: "Set margin mode (cross/isolated) for a symbol.", fetch_funding_rate: "Fetch the current funding rate for a perpetual swap.", fetch_funding_rates: "Fetch funding rates for all perpetual swaps.", fetch_deposit_address: "Fetch a deposit address for a currency.", withdraw: "Withdraw funds to an external address.", fetch_deposits: "Fetch deposit history.", fetch_withdrawals: "Fetch withdrawal history.", transfer: "Transfer funds between exchange accounts (e.g., spot to futures).", fetch_trading_fee: "Fetch the trading fee for a specific symbol.", fetch_trading_fees: "Fetch trading fees for all symbols.", fetch_ledger: "Fetch the account ledger (transaction history).", close_position: "Close a derivative position for a symbol.", fetch_open_interest: "Fetch open interest for a perpetual or futures symbol.", fetch_leverage: "Fetch current leverage setting for a symbol.", fetch_mark_price: "Fetch the mark price for a derivative symbol.", fetch_liquidations: "Fetch recent liquidation events for a symbol.", fetch_option_chain: "Fetch the full options chain for an underlying asset.", fetch_greeks: "Fetch option greeks (delta, gamma, theta, vega) for a symbol." } @method_defs [ # ----------------------------------------------------------------------- # Market Data (public) # ----------------------------------------------------------------------- {:fetch_ticker, "fetchTicker", [:symbol]}, {:fetch_tickers, "fetchTickers", []}, {:fetch_order_book, "fetchOrderBook", [:symbol]}, {:fetch_order_books, "fetchOrderBooks", []}, {:fetch_l2_order_book, "fetchL2OrderBook", [:symbol]}, {:fetch_l3_order_book, "fetchL3OrderBook", [:symbol]}, {:fetch_trades, "fetchTrades", [:symbol]}, {:fetch_ohlcv, "fetchOHLCV", [:symbol, :timeframe]}, {:fetch_markets, "fetchMarkets", []}, {:fetch_markets_by_type, "fetchMarketsByType", [:type]}, {:fetch_markets_by_type_and_sub_type, "fetchMarketsByTypeAndSubType", [:type, :sub_type]}, {:fetch_spot_markets, "fetchSpotMarkets", []}, {:fetch_swap_markets, "fetchSwapMarkets", []}, {:fetch_future_markets, "fetchFutureMarkets", []}, {:fetch_option_markets, "fetchOptionMarkets", []}, {:fetch_contract_markets, "fetchContractMarkets", []}, {:fetch_swap_and_future_markets, "fetchSwapAndFutureMarkets", []}, {:fetch_inverse_swap_markets, "fetchInverseSwapMarkets", []}, {:fetch_uta_markets, "fetchUTAMarkets", []}, {:fetch_swap_balance, "fetchSwapBalance", []}, {:fetch_usdt_markets, "fetchUSDTMarkets", []}, {:fetch_currencies, "fetchCurrencies", []}, {:fetch_currency, "fetchCurrency", [:code]}, {:fetch_currency_by_id, "fetchCurrencyById", [:id]}, {:fetch_time, "fetchTime", []}, {:fetch_status, "fetchStatus", []}, {:fetch_bids_asks, "fetchBidsAsks", []}, {:fetch_last_prices, "fetchLastPrices", []}, {:fetch_market, "fetchMarket", [:symbol]}, {:fetch_market_by_id, "fetchMarketById", [:id]}, {:fetch_mark_price, "fetchMarkPrice", [:symbol]}, {:fetch_mark_prices, "fetchMarkPrices", []}, {:fetch_market_leverage_tiers, "fetchMarketLeverageTiers", [:symbol]}, {:fetch_derivatives_market_leverage_tiers, "fetchDerivativesMarketLeverageTiers", [:symbol]}, # Funding rates {:fetch_funding_rate, "fetchFundingRate", [:symbol]}, {:fetch_funding_rates, "fetchFundingRates", []}, {:fetch_funding_rate_history, "fetchFundingRateHistory", [:symbol]}, {:fetch_funding_history, "fetchFundingHistory", [:symbol]}, {:fetch_funding_interval, "fetchFundingInterval", [:symbol]}, {:fetch_funding_intervals, "fetchFundingIntervals", []}, {:fetch_funding_limits, "fetchFundingLimits", []}, # Open interest {:fetch_open_interest, "fetchOpenInterest", [:symbol]}, {:fetch_open_interest_history, "fetchOpenInterestHistory", [:symbol]}, {:fetch_open_interests, "fetchOpenInterests", []}, {:fetch_derivatives_open_interest_history, "fetchDerivativesOpenInterestHistory", [:symbol]}, # Analytics {:fetch_long_short_ratio_history, "fetchLongShortRatioHistory", [:symbol]}, {:fetch_liquidations, "fetchLiquidations", [:symbol]}, {:fetch_volatility_history, "fetchVolatilityHistory", [:symbol]}, # Options {:fetch_option, "fetchOption", [:symbol]}, {:fetch_option_chain, "fetchOptionChain", [:symbol]}, {:fetch_greeks, "fetchGreeks", [:symbol]}, {:fetch_all_greeks, "fetchAllGreeks", []}, {:fetch_underlying_assets, "fetchUnderlyingAssets", []}, {:fetch_option_underlyings, "fetchOptionUnderlyings", []}, {:fetch_option_positions, "fetchOptionPositions", []}, {:fetch_option_ohlcv, "fetchOptionOHLCV", [:symbol, :timeframe]}, # Exchange-specific OHLCV variants {:fetch_contract_ohlcv, "fetchContractOHLCV", [:symbol, :timeframe]}, {:fetch_spot_ohlcv, "fetchSpotOHLCV", [:symbol, :timeframe]}, {:fetch_uta_ohlcv, "fetchUTAOHLCV", [:symbol, :timeframe]}, # Exchange-specific tickers {:fetch_contract_tickers, "fetchContractTickers", []}, # ----------------------------------------------------------------------- # Trading — Order Creation # ----------------------------------------------------------------------- {:create_order, "createOrder", [:symbol, :type, :side, :amount]}, {:create_orders, "createOrders", [:orders]}, {:create_spot_order, "createSpotOrder", [:symbol, :type, :side, :amount]}, {:create_spot_orders, "createSpotOrders", [:orders]}, {:create_contract_order, "createContractOrder", [:symbol, :type, :side, :amount]}, {:create_contract_orders, "createContractOrders", [:orders]}, {:create_swap_order, "createSwapOrder", [:symbol, :type, :side, :amount]}, {:create_uta_order, "createUtaOrder", [:symbol, :type, :side, :amount]}, {:create_uta_orders, "createUtaOrders", [:orders]}, {:create_order_with_take_profit_and_stop_loss, "createOrderWithTakeProfitAndStopLoss", [:symbol, :type, :side, :amount]}, {:create_trailing_amount_order, "createTrailingAmountOrder", [:symbol, :type, :side, :amount]}, {:create_trailing_percent_order, "createTrailingPercentOrder", [:symbol, :type, :side, :amount]}, {:create_twap_order, "createTwapOrder", [:symbol, :type, :side, :amount]}, {:create_market_buy_order_with_cost, "createMarketBuyOrderWithCost", [:symbol, :cost]}, {:create_market_sell_order_with_cost, "createMarketSellOrderWithCost", [:symbol, :cost]}, {:create_market_order_with_cost, "createMarketOrderWithCost", [:symbol, :side, :cost]}, # ----------------------------------------------------------------------- # Trading — Order Cancellation # ----------------------------------------------------------------------- {:cancel_order, "cancelOrder", [:id]}, {:cancel_orders, "cancelOrders", [:ids]}, {:cancel_all_orders, "cancelAllOrders", []}, {:cancel_all_orders_after, "cancelAllOrdersAfter", [:timeout]}, {:cancel_orders_for_symbols, "cancelOrdersForSymbols", [:orders]}, {:cancel_spot_order, "cancelSpotOrder", [:id]}, {:cancel_contract_order, "cancelContractOrder", [:id]}, {:cancel_unified_order, "cancelUnifiedOrder", [:id]}, {:cancel_uta_order, "cancelUtaOrder", [:id]}, {:cancel_uta_orders, "cancelUtaOrders", [:ids]}, {:cancel_twap_order, "cancelTwapOrder", [:id]}, {:cancel_all_spot_orders, "cancelAllSpotOrders", []}, {:cancel_all_contract_orders, "cancelAllContractOrders", []}, {:cancel_all_uta_orders, "cancelAllUtaOrders", []}, # ----------------------------------------------------------------------- # Trading — Order Editing # ----------------------------------------------------------------------- {:edit_order, "editOrder", [:id, :symbol, :type, :side]}, {:edit_orders, "editOrders", [:orders]}, {:edit_contract_order, "editContractOrder", [:id, :symbol, :type, :side]}, {:edit_spot_order, "editSpotOrder", [:id, :symbol, :type, :side]}, # ----------------------------------------------------------------------- # Trading — Order Fetching # ----------------------------------------------------------------------- {:fetch_order, "fetchOrder", [:id]}, {:fetch_order_classic, "fetchOrderClassic", [:id]}, {:fetch_orders, "fetchOrders", []}, {:fetch_orders_classic, "fetchOrdersClassic", []}, {:fetch_open_order, "fetchOpenOrder", [:id]}, {:fetch_open_orders, "fetchOpenOrders", []}, {:fetch_closed_order, "fetchClosedOrder", [:id]}, {:fetch_closed_orders, "fetchClosedOrders", []}, {:fetch_canceled_orders, "fetchCanceledOrders", []}, {:fetch_canceled_and_closed_orders, "fetchCanceledAndClosedOrders", []}, {:fetch_order_trades, "fetchOrderTrades", [:id]}, {:fetch_order_status, "fetchOrderStatus", [:id]}, {:fetch_orders_by_ids, "fetchOrdersByIds", [:ids]}, {:fetch_orders_by_state, "fetchOrdersByState", [:state]}, {:fetch_orders_by_status, "fetchOrdersByStatus", [:status]}, {:fetch_orders_by_type, "fetchOrdersByType", [:type]}, # Exchange-specific order variants {:fetch_spot_order, "fetchSpotOrder", [:id]}, {:fetch_spot_orders, "fetchSpotOrders", []}, {:fetch_spot_order_trades, "fetchSpotOrderTrades", [:id]}, {:fetch_spot_orders_by_states, "fetchSpotOrdersByStates", []}, {:fetch_spot_orders_by_status, "fetchSpotOrdersByStatus", []}, {:fetch_open_spot_orders, "fetchOpenSpotOrders", []}, {:fetch_open_swap_orders, "fetchOpenSwapOrders", []}, {:fetch_closed_spot_orders, "fetchClosedSpotOrders", []}, {:fetch_closed_contract_orders, "fetchClosedContractOrders", []}, {:fetch_canceled_and_closed_spot_orders, "fetchCanceledAndClosedSpotOrders", []}, {:fetch_canceled_and_closed_swap_orders, "fetchCanceledAndClosedSwapOrders", []}, {:fetch_contract_order, "fetchContractOrder", [:id]}, {:fetch_contract_orders, "fetchContractOrders", []}, {:fetch_contract_orders_by_status, "fetchContractOrdersByStatus", []}, {:fetch_uta_order, "fetchUtaOrder", [:id]}, {:fetch_uta_orders_by_status, "fetchUtaOrdersByStatus", []}, {:fetch_uta_canceled_and_closed_orders, "fetchUtaCanceledAndClosedOrders", []}, {:fetch_adl_rank, "fetchADLRank", []}, # ----------------------------------------------------------------------- # Account — Balance & Info # ----------------------------------------------------------------------- {:fetch_balance, "fetchBalance", []}, {:fetch_spot_balance, "fetchSpotBalance", []}, {:fetch_contract_balance, "fetchContractBalance", []}, {:fetch_margin_balance, "fetchMarginBalance", []}, {:fetch_financial_balance, "fetchFinancialBalance", []}, {:fetch_uta_balance, "fetchUtaBalance", []}, {:fetch_account, "fetchAccount", []}, {:fetch_accounts, "fetchAccounts", []}, {:fetch_account_positions, "fetchAccountPositions", []}, {:create_account, "createAccount", []}, {:create_sub_account, "createSubAccount", []}, # ----------------------------------------------------------------------- # Account — Trades # ----------------------------------------------------------------------- {:fetch_my_trades, "fetchMyTrades", []}, {:fetch_my_spot_trades, "fetchMySpotTrades", []}, {:fetch_my_contract_trades, "fetchMyContractTrades", []}, {:fetch_my_uta_trades, "fetchMyUtaTrades", []}, {:fetch_my_buys, "fetchMyBuys", []}, {:fetch_my_sells, "fetchMySells", []}, {:fetch_my_dust_trades, "fetchMyDustTrades", []}, {:fetch_my_liquidations, "fetchMyLiquidations", []}, {:fetch_my_settlement_history, "fetchMySettlementHistory", []}, # ----------------------------------------------------------------------- # Account — Positions # ----------------------------------------------------------------------- {:fetch_position, "fetchPosition", [:symbol]}, {:fetch_positions, "fetchPositions", []}, {:fetch_positions_for_symbol, "fetchPositionsForSymbol", [:symbol]}, {:fetch_positions_history, "fetchPositionsHistory", []}, {:fetch_position_history, "fetchPositionHistory", []}, {:fetch_positions_risk, "fetchPositionsRisk", []}, {:fetch_positions_adl_rank, "fetchPositionsADLRank", []}, {:close_position, "closePosition", [:symbol]}, {:close_all_positions, "closeAllPositions", []}, {:fetch_position_mode, "fetchPositionMode", []}, {:set_position_mode, "setPositionMode", [:hedge_mode]}, # ----------------------------------------------------------------------- # Account — Leverage & Margin # ----------------------------------------------------------------------- {:fetch_leverage, "fetchLeverage", [:symbol]}, {:fetch_leverages, "fetchLeverages", []}, {:fetch_leverage_tiers, "fetchLeverageTiers", []}, {:set_leverage, "setLeverage", [:leverage, :symbol]}, {:fetch_margin_mode, "fetchMarginMode", [:symbol]}, {:fetch_margin_modes, "fetchMarginModes", []}, {:set_margin_mode, "setMarginMode", [:margin_mode, :symbol]}, {:set_margin, "setMargin", [:amount, :symbol]}, {:add_margin, "addMargin", [:amount, :symbol]}, {:reduce_margin, "reduceMargin", [:amount, :symbol]}, {:fetch_margin_adjustment_history, "fetchMarginAdjustmentHistory", []}, # ----------------------------------------------------------------------- # Account — Fees & Trading Limits # ----------------------------------------------------------------------- {:fetch_trading_fee, "fetchTradingFee", [:symbol]}, {:fetch_trading_fees, "fetchTradingFees", []}, {:fetch_trading_limits, "fetchTradingLimits", []}, {:fetch_trading_limits_by_id, "fetchTradingLimitsById", [:id]}, {:fetch_private_trading_fee, "fetchPrivateTradingFee", [:symbol]}, {:fetch_private_trading_fees, "fetchPrivateTradingFees", []}, {:fetch_public_trading_fee, "fetchPublicTradingFee", [:symbol]}, {:fetch_public_trading_fees, "fetchPublicTradingFees", []}, {:fetch_transaction_fee, "fetchTransactionFee", [:code]}, {:fetch_transaction_fees, "fetchTransactionFees", []}, {:fetch_private_transaction_fees, "fetchPrivateTransactionFees", []}, {:fetch_public_transaction_fees, "fetchPublicTransactionFees", []}, # ----------------------------------------------------------------------- # Account — Ledger # ----------------------------------------------------------------------- {:fetch_ledger, "fetchLedger", []}, {:fetch_ledger_entry, "fetchLedgerEntry", [:id]}, {:fetch_ledger_by_entries, "fetchLedgerByEntries", []}, {:fetch_ledger_entries_by_ids, "fetchLedgerEntriesByIds", [:ids]}, # ----------------------------------------------------------------------- # Funding — Deposits # ----------------------------------------------------------------------- {:fetch_deposit, "fetchDeposit", [:id]}, {:fetch_deposits, "fetchDeposits", []}, {:fetch_deposit_address, "fetchDepositAddress", [:code]}, {:fetch_deposit_addresses, "fetchDepositAddresses", []}, {:fetch_deposit_addresses_by_network, "fetchDepositAddressesByNetwork", [:code]}, {:fetch_network_deposit_address, "fetchNetworkDepositAddress", [:code]}, {:fetch_contract_deposit_address, "fetchContractDepositAddress", [:code]}, {:create_deposit_address, "createDepositAddress", [:code]}, {:fetch_deposit_method_id, "fetchDepositMethodId", [:code]}, {:fetch_deposit_method_ids, "fetchDepositMethodIds", []}, {:fetch_deposit_methods, "fetchDepositMethods", [:code]}, {:fetch_payment_methods, "fetchPaymentMethods", []}, # ----------------------------------------------------------------------- # Funding — Withdrawals # ----------------------------------------------------------------------- {:withdraw, "withdraw", [:code, :amount, :address]}, {:fetch_withdrawal, "fetchWithdrawal", [:id]}, {:fetch_withdrawals, "fetchWithdrawals", []}, {:fetch_withdraw_addresses, "fetchWithdrawAddresses", []}, {:fetch_contract_withdrawals, "fetchContractWithdrawals", []}, {:fetch_contract_deposits, "fetchContractDeposits", []}, # ----------------------------------------------------------------------- # Funding — Deposit/Withdraw Fees # ----------------------------------------------------------------------- {:fetch_deposit_withdraw_fee, "fetchDepositWithdrawFee", [:code]}, {:fetch_deposit_withdraw_fees, "fetchDepositWithdrawFees", []}, {:fetch_private_deposit_withdraw_fees, "fetchPrivateDepositWithdrawFees", []}, {:fetch_public_deposit_withdraw_fees, "fetchPublicDepositWithdrawFees", []}, # ----------------------------------------------------------------------- # Funding — Deposits + Withdrawals combined # ----------------------------------------------------------------------- {:fetch_deposits_withdrawals, "fetchDepositsWithdrawals", []}, {:fetch_transactions, "fetchTransactions", []}, {:fetch_transactions_by_type, "fetchTransactionsByType", [:type]}, # ----------------------------------------------------------------------- # Funding — Transfers # ----------------------------------------------------------------------- {:transfer, "transfer", [:code, :amount, :from_account, :to_account]}, {:fetch_transfer, "fetchTransfer", [:id]}, {:fetch_transfers, "fetchTransfers", []}, {:transfer_between_main_and_sub_account, "transferBetweenMainAndSubAccount", [:code, :amount, :from_account, :to_account]}, {:transfer_between_sub_accounts, "transferBetweenSubAccounts", [:code, :amount, :from_account, :to_account]}, {:transfer_classic, "transferClassic", [:code, :amount, :from_account, :to_account]}, {:transfer_out, "transferOut", [:code, :amount, :address]}, {:transfer_uta, "transferUta", [:code, :amount, :from_account, :to_account]}, # ----------------------------------------------------------------------- # Margin — Borrowing # ----------------------------------------------------------------------- {:borrow_cross_margin, "borrowCrossMargin", [:code, :amount]}, {:borrow_isolated_margin, "borrowIsolatedMargin", [:symbol, :code, :amount]}, {:repay_cross_margin, "repayCrossMargin", [:code, :amount]}, {:repay_isolated_margin, "repayIsolatedMargin", [:symbol, :code, :amount]}, {:repay_margin, "repayMargin", [:code, :amount]}, {:fetch_borrow_interest, "fetchBorrowInterest", []}, {:fetch_borrow_rate_history, "fetchBorrowRateHistory", [:code]}, {:fetch_borrow_rate_histories, "fetchBorrowRateHistories", []}, {:fetch_cross_borrow_rate, "fetchCrossBorrowRate", [:code]}, {:fetch_cross_borrow_rates, "fetchCrossBorrowRates", []}, {:fetch_isolated_borrow_rate, "fetchIsolatedBorrowRate", [:symbol]}, {:fetch_isolated_borrow_rates, "fetchIsolatedBorrowRates", []}, # ----------------------------------------------------------------------- # Convert # ----------------------------------------------------------------------- {:create_convert_trade, "createConvertTrade", [:from_code, :to_code, :amount]}, {:fetch_convert_currencies, "fetchConvertCurrencies", []}, {:fetch_convert_quote, "fetchConvertQuote", [:from_code, :to_code, :amount]}, {:fetch_convert_trade, "fetchConvertTrade", [:id]}, {:fetch_convert_trade_history, "fetchConvertTradeHistory", []}, # ----------------------------------------------------------------------- # Settlement # ----------------------------------------------------------------------- {:fetch_settlement_history, "fetchSettlementHistory", []}, # ----------------------------------------------------------------------- # Portfolios & Vaults # ----------------------------------------------------------------------- {:fetch_portfolios, "fetchPortfolios", []}, {:fetch_portfolio_details, "fetchPortfolioDetails", [:portfolio_id]}, {:create_vault, "createVault", [:code, :amount]}, # ----------------------------------------------------------------------- # Gift Codes # ----------------------------------------------------------------------- {:create_gift_code, "createGiftCode", [:code, :amount]} ] @doc "Returns all unified method definitions as `{elixir_name, js_name, required_params, description}` tuples." @spec method_defs() :: [{atom(), String.t(), [atom()], String.t()}] def method_defs do Enum.map(@method_defs, fn {name, js_name, params} -> {name, js_name, params, description_for(name)} end) end @doc "Returns the description for a unified method, curated or auto-generated." @spec description_for(atom()) :: String.t() def description_for(name) do Map.get_lazy(@curated_descriptions, name, fn -> auto_description(name) end) end # Auto-generates a description from the method name. # "fetch_funding_rate" → "Fetch funding rate." defp auto_description(name) do name |> Atom.to_string() |> String.replace("_", " ") |> then(fn s -> String.upcase(String.first(s)) <> String.slice(s, 1..-1//1) end) |> Kernel.<>(".") end # =========================================================================== # Dispatch # =========================================================================== @doc "Dispatches a unified method call through module resolution and endpoint selection." @spec call(Exchange.t(), atom(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t() | term()} def call(%Exchange{} = exchange, method_atom, capability_name, params, opts) do with {:ok, module} <- require_module(exchange), {:ok, config} <- resolve_endpoint(module, method_atom, exchange.id, capability_name, opts) do # Strip :endpoint_index — it's our opt, not Dispatch/Req's dispatch_opts = Keyword.delete(opts, :endpoint_index) final_params = params |> maybe_denormalize_symbol(exchange) |> maybe_merge_request_defaults(exchange, capability_name) Dispatch.call(exchange, config, final_params, dispatch_opts) end end @doc """ Denormalize a unified `"symbol"` param (e.g. `"BTC/USDT"`) into the exchange's native form (e.g. `"BTCUSDT"` on Binance, `"BTC-PERPETUAL"` on Deribit swap) before the params reach Dispatch. No-op when: * `params` has no `"symbol"` key * `params["symbol"]` is not a binary * `exchange.symbol_patterns` has no entry for the detected market type (`CCXT.Symbol.to_exchange_id/2` returns the input unchanged) Raw generated endpoint callers (`CCXT.Bybit.public_get_v5_market_tickers/3`) bypass this — they're expected to pass exchange-native symbols already. """ @spec maybe_denormalize_symbol(map(), Exchange.t()) :: map() def maybe_denormalize_symbol(%{"symbol" => unified} = params, %Exchange{} = exchange) when is_binary(unified) do Map.put(params, "symbol", Symbol.to_exchange_id(unified, exchange)) end def maybe_denormalize_symbol(params, _exchange), do: params @doc """ Merge per-method default request-body params into the caller's params. Reads `structure.request_defaults` literals materialized onto the Exchange struct at construction time (schema 2.0.1+, upstream ccxt_extract Task 73c). Caller-supplied params win over defaults (`Map.put_new` semantics) — matches CCXT JS's `this.extend(request, params)` precedence. Unblocks exchanges like hyperliquid whose unified methods route through a single endpoint discriminated by a literal body field (e.g. `fetchTime` → `POST /info` with `{ "type": "exchangeStatus" }`). No-op when: * `exchange.request_defaults` has no entry for the method * the entries map is empty (all entries were `kind: "unresolved"` and filtered out at Exchange construction time) """ @spec maybe_merge_request_defaults(map(), Exchange.t(), String.t()) :: map() def maybe_merge_request_defaults(params, %Exchange{request_defaults: defaults}, js_name) when is_map(params) and is_binary(js_name) do case Map.get(defaults, js_name) do literals when is_map(literals) and map_size(literals) > 0 -> Enum.reduce(literals, params, fn {k, v}, acc -> Map.put_new(acc, k, v) end) _ -> params end end @doc "Separates dispatch-level opts from exchange params." @spec split_opts(keyword()) :: {keyword(), keyword()} def split_opts(opts), do: Keyword.split(opts, @dispatch_opts) @doc "Builds a string-keyed params map from required param names/values and extra opts." @spec build_params([atom()], [term()], keyword()) :: map() def build_params(required_names, required_values, extra) do params = required_names |> Enum.zip(required_values) |> Map.new(fn {k, v} -> {to_string(k), v} end) Enum.reduce(extra, params, fn {k, v}, acc -> Map.put_new(acc, to_string(k), v) end) end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- # Resolves the exchange module from struct field or registry fallback defp require_module(%Exchange{module: module}) when not is_nil(module), do: {:ok, module} defp require_module(%Exchange{id: id}) do case Registry.module_for(id) do nil -> {:error, Error.not_supported( exchange: id, message: "No compiled module found for exchange #{id}" )} module -> {:ok, module} end end # Looks up unified endpoint configs and selects one. # exchange_id and capability_name are used for descriptive error messages. defp resolve_endpoint(module, method_atom, exchange_id, capability_name, opts) do case module.__unified_endpoint__(method_atom) do [] -> {:error, Error.not_supported( exchange: exchange_id, message: "#{exchange_id} does not support #{capability_name}" )} configs -> {:ok, select_endpoint(configs, opts)} end end # Selects endpoint config: override via :endpoint_index, else first in list defp select_endpoint(configs, opts) do case Keyword.get(opts, :endpoint_index) do nil -> hd(configs) idx -> Enum.at(configs, idx) || hd(configs) end end end