# `Bourse.Exchange`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/exchange.ex#L1)

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)

# `capability_declaration`

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

# `capability_surface`

```elixir
@type capability_surface() :: %{
  required(String.t()) =&gt; %{required(String.t()) =&gt; capability_declaration()}
}
```

# `config`

```elixir
@type config() :: %{optional(String.t()) =&gt; term()}
```

# `doc_urls`

```elixir
@type doc_urls() :: %{optional(String.t()) =&gt; String.t() | [String.t()]}
```

# `error_body_check`

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

# `error_body_role`

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

# `error_handler_check`

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

# `error_scope`

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

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

# `error_status_guard`

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

# `fees`

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

# `market_cache`

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

# `raw_market`

```elixir
@type raw_market() :: %{optional(String.t()) =&gt; term()}
```

# `request_contract`

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

# `request_contract_key`

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

# `sentinel_operator`

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

# `sentinel_value`

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

# `t`

```elixir
@type t() :: %Bourse.Exchange{
  base_urls: map(),
  broad_error_patterns: %{required(String.t()) =&gt; Bourse.Error.error_type()},
  common_currencies: %{required(String.t()) =&gt; String.t()},
  config: config(),
  credentials: Bourse.Credentials.t() | nil,
  currencies: %{required(String.t()) =&gt; map()},
  default_family: String.t() | nil,
  doc_urls: doc_urls(),
  endpoint_selection: %{required(String.t()) =&gt; map()},
  error_body_checks: [error_body_check()],
  error_class_ancestors: %{required(String.t()) =&gt; [String.t()]},
  error_code_fields: [String.t()],
  error_codes: %{required(String.t()) =&gt; Bourse.Error.error_type()},
  error_handler_checks: [error_handler_check()],
  features: %{required(String.t()) =&gt; map()} | nil,
  fees: fees(),
  has: %{required(String.t()) =&gt; boolean()},
  hostname: String.t() | nil,
  http_exceptions: %{required(String.t()) =&gt; 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()) =&gt; String.t()},
  rate_limit_ms: number(),
  request_contracts: %{required(request_contract_key()) =&gt; request_contract()},
  request_defaults: %{required(String.t()) =&gt; %{required(String.t()) =&gt; term()}},
  request_param_shape: %{
    required(String.t()) =&gt; %{required(String.t()) =&gt; map()}
  },
  required_credentials: %{required(String.t()) =&gt; boolean()},
  retry_classification: %{required(String.t()) =&gt; Bourse.Error.retry_class()},
  sandbox: boolean(),
  sandbox_headers: %{optional(String.t()) =&gt; String.t()},
  signing_config: map(),
  signing_pattern: Bourse.Signing.pattern() | nil,
  spec: map(),
  status_map: %{required(String.t()) =&gt; Bourse.Error.error_type()},
  symbol_patterns: %{required(atom()) =&gt; Bourse.Symbol.pattern_config()},
  timeframes: %{required(String.t()) =&gt; String.t()}
}
```

# `trading_fee_schedule`

```elixir
@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()
}
```

# `__generate__`
*macro* 

Generates introspection functions and endpoint wrappers from a spec ID.

# `__using__`
*macro* 

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

# `build_endpoint_configs`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec build_unified_method_mapping(map(), [map()]) :: %{required(atom()) =&gt; [map()]}
```

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

# `capability_surface`

```elixir
@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`

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

Returns named capability changes between two release surfaces.

# `config`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec error_codes_for(t(), error_scope()) :: %{
  required(String.t()) =&gt; Bourse.Error.error_type()
}
```

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

# `error_scope`

```elixir
@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`

```elixir
@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`

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

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

# `has?`

```elixir
@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`

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

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

# `mapping_complete?`

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

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

# `markets`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

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

Prepares all compile-time data for the generator macro.

# `put_markets`

```elixir
@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`

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

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

# `signing_from_spec`

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

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

# `status`

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

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

# `timeframes`

```elixir
@spec timeframes(t()) :: %{required(String.t()) =&gt; 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`

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

Returns the provider-support declaration for a unified method.

# `verification_state`

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

Returns the provider-verification state for a unified method.

# `with_error_scope`

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
