Bourse.Exchange (bourse v0.7.0)

Copy Markdown View Source

Exchange configuration struct and constructor.

Holds everything needed to make API calls for a specific exchange instance: resolved base URLs, rate limits, credentials, capabilities, and lean spec data.

This is a pure data struct — no process. Rate limiting, HTTP execution, and signing are handled by other modules that receive %Exchange{} as input.

Loaded market metadata lives on the struct as :markets (nil until Bourse.load_markets/1 or put_markets/2). Production caches contain %Bourse.Market{} structs; static replay caches retain raw Bourse maps so their oracle inputs stay byte-faithful. Callers thread the enriched value; there is no hidden global cache.

Examples

# Public-only (no credentials)
{:ok, exchange} = Bourse.Exchange.new("bybit")
exchange.base_urls
#=> %{"public" => "https://api.bybit.com", ...}

# With credentials
{:ok, exchange} = Bourse.Exchange.new("bybit", api_key: "abc", secret: "xyz")
exchange.credentials
#=> %Bourse.Credentials{api_key: "abc", secret: "xyz", ...}

# Sandbox mode (uses testnet URLs)
{:ok, exchange} = Bourse.Exchange.new("bybit", sandbox: true)

# Cache markets for symbol→market_id resolution (loadMarkets equivalent)
{:ok, exchange} = Bourse.load_markets(exchange)

Summary

Functions

Generates introspection functions and endpoint wrappers from a spec ID.

Macro entry point: use Bourse.Exchange, spec: "bybit" generates an exchange module.

Pre-computes a flat list of endpoint configs from the nested API tree.

Builds quoted endpoint wrapper functions from a list of endpoint configs.

Builds @moduledoc text for a generated exchange module from spec metadata.

Builds the quoted module body from prepared generate data.

Builds quoted parse_<slot>/2 wrapper functions from the spec's normalization field maps and provider-support declarations.

Builds unified method mapping from spec data and pre-computed endpoint configs.

Returns the release-pinned capability surface for every runtime venue.

Returns named capability changes between two release surfaces.

Returns the derived config section — deterministic describe() metadata (credentials, limits, status, routing, rate-limit meta, flags).

Returns compile-time currency metadata for code from the spec's markets.currencies catalog (Task 97), or nil when absent.

Returns per-network metadata for currency_code + network_code, or nil.

Returns the documentation URL doc-set (logo, www, doc, fees, api_management) folded from the derived urls section, or %{}.

Returns exact error-code mappings for scope, with scoped entries taking precedence.

Returns the authored exception scope for a request base URL.

Returns the static default fee schedule from the derived fees spec section.

Returns the config.flags map (e.g. dex), or %{}.

Checks if the exchange supports a given capability.

Returns the global default config.limits map (amount/cost/leverage/price), or %{}.

Returns whether Bourse has a complete normalized mapping for a unified method.

Returns the caller-threaded markets cache, or nil when not loaded.

Creates an exchange configuration from an exchange ID and options.

Creates an exchange configuration, raising on error.

Prepares all compile-time data for the generator macro.

Returns a copy of exchange with the markets cache set to markets.

Returns the config.routing map (accountsByType / networks / timeInForce), or %{}.

Reads the explicit signing executor and configuration from an owned runtime spec.

Returns the config.status map (status/eta/url/info/updated), or %{}.

Returns the unified-to-native OHLCV timeframe map from capabilities.timeframes.

Returns the provider-support declaration for a unified method.

Returns the provider-verification state for a unified method.

Returns a copy whose active error maps are selected for scope.

Types

capability_declaration()

@type capability_declaration() :: boolean() | String.t()

capability_surface()

@type capability_surface() :: %{
  required(String.t()) => %{required(String.t()) => capability_declaration()}
}

config()

@type config() :: %{optional(String.t()) => term()}

doc_urls()

@type doc_urls() :: %{optional(String.t()) => String.t() | [String.t()]}

error_body_check()

@type error_body_check() :: %{
  field: String.t() | nil,
  field2: String.t() | nil,
  roles: [error_body_role()],
  sentinel_values: [sentinel_value()]
}

error_body_role()

@type error_body_role() :: :error_code | :status_sentinel

error_handler_check()

@type error_handler_check() :: %{
  status_guard: error_status_guard(),
  body_contains: [String.t()],
  error_type: Bourse.Error.error_type()
}

error_scope()

@type error_scope() :: String.t() | atom() | nil

Authored exception scope selected by the request's market family.

error_status_guard()

@type error_status_guard() :: {:gte, non_neg_integer()} | {:in, [non_neg_integer()]}

fees()

@type fees() :: map() | nil

market_cache()

@type market_cache() :: [Bourse.Market.t() | raw_market()] | nil

raw_market()

@type raw_market() :: %{optional(String.t()) => term()}

request_contract()

@type request_contract() :: %{
  optional(:method) => atom(),
  optional(:path) => String.t(),
  optional(:path_params) => [String.t()],
  optional(:body_encoding) => String.t(),
  optional(:content_type) => String.t() | nil,
  optional(:timestamp_recipe) => map(),
  optional(:weight) => number(),
  optional(:rate_limit) => map()
}

request_contract_key()

@type request_contract_key() :: {[String.t()], atom(), String.t()}

sentinel_operator()

@type sentinel_operator() :: String.t()

sentinel_value()

@type sentinel_value() :: %{operator: sentinel_operator(), value: String.t()}

t()

@type t() :: %Bourse.Exchange{
  base_urls: map(),
  broad_error_patterns: %{required(String.t()) => Bourse.Error.error_type()},
  common_currencies: %{required(String.t()) => String.t()},
  config: config(),
  credentials: Bourse.Credentials.t() | nil,
  currencies: %{required(String.t()) => map()},
  default_family: String.t() | nil,
  doc_urls: doc_urls(),
  endpoint_selection: %{required(String.t()) => map()},
  error_body_checks: [error_body_check()],
  error_class_ancestors: %{required(String.t()) => [String.t()]},
  error_code_fields: [String.t()],
  error_codes: %{required(String.t()) => Bourse.Error.error_type()},
  error_handler_checks: [error_handler_check()],
  features: %{required(String.t()) => map()} | nil,
  fees: fees(),
  has: %{required(String.t()) => boolean()},
  hostname: String.t() | nil,
  http_exceptions: %{required(String.t()) => Bourse.Error.error_type()},
  id: String.t(),
  markets: market_cache(),
  module: module() | nil,
  name: String.t(),
  network_options: map(),
  options: map(),
  outbound_aliases: %{required(String.t()) => String.t()},
  rate_limit_ms: number(),
  request_contracts: %{required(request_contract_key()) => request_contract()},
  request_defaults: %{required(String.t()) => %{required(String.t()) => term()}},
  request_param_shape: %{
    required(String.t()) => %{required(String.t()) => map()}
  },
  required_credentials: %{required(String.t()) => boolean()},
  retry_classification: %{required(String.t()) => Bourse.Error.retry_class()},
  sandbox: boolean(),
  sandbox_headers: %{optional(String.t()) => String.t()},
  signing_config: map(),
  signing_pattern: Bourse.Signing.pattern() | nil,
  spec: map(),
  status_map: %{required(String.t()) => Bourse.Error.error_type()},
  symbol_patterns: %{required(atom()) => Bourse.Symbol.pattern_config()},
  timeframes: %{required(String.t()) => String.t()}
}

trading_fee_schedule()

@type trading_fee_schedule() :: %{
  maker: number() | nil,
  taker: number() | nil,
  percentage: boolean() | nil,
  tier_based: boolean() | nil,
  fee_side: String.t() | nil,
  tiers: map() | nil,
  fee: Bourse.TradingFee.t(),
  info: map()
}

Functions

__generate__(spec_id)

(macro)

Generates introspection functions and endpoint wrappers from a spec ID.

__using__(opts)

(macro)

Macro entry point: use Bourse.Exchange, spec: "bybit" generates an exchange module.

build_endpoint_configs(api_tree, url_prefixes \\ %{}, authenticated_sections \\ [])

@spec build_endpoint_configs(map(), map(), [String.t()]) :: [map()]

Pre-computes a flat list of endpoint configs from the nested API tree.

Called at compile time by __generate__/1. Recursively traverses the spec's API tree until it finds HTTP method keys (get, post, etc.), then extracts endpoint configs from the level below.

Handles three spec patterns:

  • Standard: %{visibility => %{method => %{path => weight}}}
  • Deep nesting: %{api_type => %{version => %{visibility => %{method => ...}}}}
  • Array endpoints: %{... => %{method => [list_of_paths]}}

All intermediate keys above the HTTP method become the :sections list.

Examples

Bourse.Exchange.build_endpoint_configs(%{
  "public" => %{"get" => %{"v5/market/tickers" => 5}},
  "private" => %{"post" => %{"v5/order/create" => 2.5}}
})
#=> [
#=>   %{name: :public_get_v5_market_tickers, method: :get,
#=>     path: "v5/market/tickers", sections: ["public"], weight: 5},
#=>   %{name: :private_post_v5_order_create, method: :post,
#=>     path: "v5/order/create", sections: ["private"], weight: 2.5}
#=> ]

build_endpoint_functions(endpoint_configs)

@spec build_endpoint_functions([map()]) :: [Macro.t()]

Builds quoted endpoint wrapper functions from a list of endpoint configs.

Called at compile time by __generate__/1. Each generated function embeds its endpoint config as a literal and delegates to Bourse.Dispatch.call/4.

Example

For a config %{name: :public_get_v5_market_tickers, ...}, generates:

def public_get_v5_market_tickers(exchange, params \\ %{}, opts \\ [])
def public_get_v5_market_tickers(%Bourse.Exchange{} = exchange, params, opts) do
  Bourse.Dispatch.call(exchange, %{...}, params, opts)
end

build_exchange_moduledoc(data)

@spec build_exchange_moduledoc(map()) :: String.t()

Builds @moduledoc text for a generated exchange module from spec metadata.

Used by Bourse.Exchanges at compile time. The returned string is injected into each generated module (e.g. Bourse.Bybit) for ExDoc and IDE discovery.

build_module_body(data, opts \\ [])

@spec build_module_body(
  map(),
  keyword()
) :: Macro.t()

Builds the quoted module body from prepared generate data.

Called by both __generate__/1 (macro path) and Bourse.Exchanges (Module.create path) to ensure a single source of truth.

Options

  • :moduledoc — optional @moduledoc string to inject. The macro path leaves this to the caller; Bourse.Exchanges provides one per exchange.

build_parse_functions(field_maps, venue_support, unified)

@spec build_parse_functions(map(), map(), map()) :: [Macro.t()]

Builds quoted parse_<slot>/2 wrapper functions from the spec's normalization field maps and provider-support declarations.

One function per @parse_slots entry (parse_ticker/2, parse_trade/2, …). Each embeds its slot mapping as a literal and delegates to Bourse.Parser.parse/4, A slot with no provider-offered operation returns {:error, {:unsupported_operation, slot}}. An offered but unmapped slot returns {:error, :no_field_map}; a non-nil _unresolved_reason returns {:error, {:unresolved, reason}}.

build_unified_method_mapping(spec, endpoint_configs)

@spec build_unified_method_mapping(map(), [map()]) :: %{required(atom()) => [map()]}

Builds unified method mapping from spec data and pre-computed endpoint configs.

capability_surface()

@spec capability_surface() :: capability_surface()

Returns the release-pinned capability surface for every runtime venue.

The complete per-capability values are embedded from priv/specs/json/capability_surface.json, which the offline oracle gate keeps equal to the authored runtime specs.

capability_surface_differences(pinned, current)

@spec capability_surface_differences(capability_surface(), capability_surface()) :: [
  String.t()
]

Returns named capability changes between two release surfaces.

config(exchange)

@spec config(t()) :: config()

Returns the derived config section — deterministic describe() metadata (credentials, limits, status, routing, rate-limit meta, flags).

Empty when an authored spec has no matching configuration. Individual sub-sections are also available via limits/1, status/1, routing/1, and flags/1.

currency(exchange, code)

@spec currency(t(), String.t()) :: map() | nil

Returns compile-time currency metadata for code from the spec's markets.currencies catalog (Task 97), or nil when absent.

Records include networks when the exchange surfaces per-network deposit/ withdraw metadata via loadMarkets().

Network coverage is declared by each supported venue's owned runtime spec.

currency_network(exchange, currency_code, network_code)

@spec currency_network(t(), String.t(), String.t()) :: map() | nil

Returns per-network metadata for currency_code + network_code, or nil.

Returns nil when the currency is absent, the exchange spec has empty networks maps (see currency/2), or the requested network code is missing.

doc_urls(exchange)

@spec doc_urls(t()) :: doc_urls()

Returns the documentation URL doc-set (logo, www, doc, fees, api_management) folded from the derived urls section, or %{}.

Call URLs live on base_urls; this surface is metadata for introspection.

error_codes_for(exchange, scope)

@spec error_codes_for(t(), error_scope()) :: %{
  required(String.t()) => Bourse.Error.error_type()
}

Returns exact error-code mappings for scope, with scoped entries taking precedence.

error_scope(exchange, base_url)

@spec error_scope(t(), String.t() | nil) :: String.t() | nil

Returns the authored exception scope for a request base URL.

Scope is never inferred from URL path segments or host labels. Construction projects the venue's authored errors.handle_errors.exception_scopes (API-section → scope) onto production and sandbox base URLs into exchange.spec["error_scopes"]; this lookup is a pure map read against that projection. Venues that declare no scopes return nil for every URL.

fees(exchange)

@spec fees(t()) :: fees()

Returns the static default fee schedule from the derived fees spec section.

This is the exchange-level describe().fees default, not live per-market loadMarkets() fees and not dynamic fetch_trading_fees endpoint data.

flags(exchange)

@spec flags(t()) :: map()

Returns the config.flags map (e.g. dex), or %{}.

has?(exchange, capability)

@spec has?(t(), String.t()) :: boolean()

Checks if the exchange supports a given capability.

Returns the derived callable surface, not the provider-support declaration. Provider-native capabilities require an authored route; provider-emulated capabilities require either an authored raw route or an implemented Bourse emulation. Verification state never changes this result.

Capability names use camelCase strings matching the Bourse spec (e.g., "fetchTicker", "createOrder").

Examples

Bourse.Exchange.has?(exchange, "fetchTicker")
#=> true

Bourse.Exchange.has?(exchange, "fetchFundingRateHistory")
#=> false

limits(exchange)

@spec limits(t()) :: map()

Returns the global default config.limits map (amount/cost/leverage/price), or %{}.

mapping_complete?(exchange, capability)

@spec mapping_complete?(t(), String.t()) :: boolean()

Returns whether Bourse has a complete normalized mapping for a unified method.

markets(exchange)

@spec markets(t()) :: market_cache()

Returns the caller-threaded markets cache, or nil when not loaded.

Bourse.load_markets/1 and put_markets/2 store %Bourse.Market{} structs. Static response replay stores raw Bourse string-keyed maps to preserve its oracle input. Pure data — no process or global store. Reload by calling Bourse.load_markets/1 again and threading the returned struct.

new(exchange_id, opts \\ [])

@spec new(
  String.t() | atom(),
  keyword()
) :: {:ok, t()} | {:error, term()}

Creates an exchange configuration from an exchange ID and options.

Loads the spec, resolves base URLs (with hostname interpolation and sandbox/testnet switching), and optionally builds credentials.

Options

  • :api_key - API key string (builds credentials automatically)
  • :secret - API secret string (builds credentials automatically)
  • :password - API password (OKX, KuCoin)
  • :uid - User ID
  • :credentials - Pre-built %Bourse.Credentials{} (overrides key/secret opts)
  • :sandbox - Use testnet URLs (default: false; OKX defaults to www.okx.com)
  • :hostname - Override the default hostname
  • :options - Exchange-specific options map

Examples

{:ok, exchange} = Bourse.Exchange.new("bybit")
{:ok, exchange} = Bourse.Exchange.new("okx", api_key: "k", secret: "s", password: "p")
{:error, :missing_secret} = Bourse.Exchange.new("bybit", api_key: "k")

new!(exchange_id, opts \\ [])

@spec new!(
  String.t() | atom(),
  keyword()
) :: t()

Creates an exchange configuration, raising on error.

Examples

exchange = Bourse.Exchange.new!("bybit")
exchange = Bourse.Exchange.new!("bybit", api_key: "abc", secret: "xyz")

prepare_generate_data(spec_id)

@spec prepare_generate_data(String.t()) :: map()

Prepares all compile-time data for the generator macro.

put_markets(exchange, markets)

@spec put_markets(t(), [Bourse.Market.t()]) :: t()

Returns a copy of exchange with the markets cache set to markets.

Use when you already hold a fetch_markets result and want subsequent market-metadata consumers (e.g. symbol→market_id resolution) to reuse it without another network round-trip. Prefer Bourse.load_markets/1 for the usual fetch-and-attach path.

routing(exchange)

@spec routing(t()) :: map()

Returns the config.routing map (accountsByType / networks / timeInForce), or %{}.

signing_from_spec(spec)

@spec signing_from_spec(map()) :: {Bourse.Signing.pattern() | nil, map()}

Reads the explicit signing executor and configuration from an owned runtime spec.

status(exchange)

@spec status(t()) :: map()

Returns the config.status map (status/eta/url/info/updated), or %{}.

timeframes(exchange)

@spec timeframes(t()) :: %{required(String.t()) => String.t()}

Returns the unified-to-native OHLCV timeframe map from capabilities.timeframes.

Keys are Bourse unified labels (e.g. "1h", "15m"); values are exchange-native labels (e.g. "60" on Bybit, "1h" on Binance). Empty when upstream omitted the map.

venue_support(exchange, capability)

@spec venue_support(t(), String.t()) :: true | false | String.t() | nil

Returns the provider-support declaration for a unified method.

verification_state(exchange, capability)

@spec verification_state(t(), String.t()) :: :verified | :unverified

Returns the provider-verification state for a unified method.

with_error_scope(exchange, scope)

@spec with_error_scope(t(), error_scope()) :: t()

Returns a copy whose active error maps are selected for scope.