Bidirectional symbol normalization between unified and exchange-specific formats.
Bourse uses a unified symbol format: BASE/QUOTE (e.g., "BTC/USDT").
Different exchanges use different formats:
- Binance: "BTCUSDT" (no separator, uppercase)
- Coinbase: "BTC-USD" (dash separator)
- Gate.io: "BTC_USDT" (underscore separator)
- Bitstamp: "btcusd" (lowercase, no separator)
- Derivatives: "BTC/USDT:USDT" (with settle currency)
- Kraken: "XXBTZUSD" (X/Z prefixes for currencies)
- KrakenFutures: "PI_XBTUSD" (contract type prefixes)
Format-Based Conversion
Use with a format map for simple normalization:
format = %{separator: "-", case: :upper}
Bourse.Symbol.normalize("BTC-USD", format)
#=> "BTC/USD"
Bourse.Symbol.denormalize("BTC/USD", format)
#=> "BTC-USD"Parsing and Building
Parse unified symbols into components:
Bourse.Symbol.parse("BTC/USDT:USDT")
#=> {:ok, %{base: "BTC", quote: "USDT", settle: "USDT"}}
Bourse.Symbol.parse_extended("BTC/USDT:USDT-260327")
#=> {:ok, %Bourse.Symbol.ParsedSymbol{base: "BTC", quote: "USDT", settle: "USDT", expiry: "260327", ...}}
Bourse.Symbol.build("BTC", "USDT", "USDT")
#=> "BTC/USDT:USDT"Currency Aliases
Apply exchange-specific currency code mappings:
aliases = %{"XBT" => "BTC", "XXRP" => "XRP"}
Bourse.Symbol.apply_alias("XBT", aliases)
#=> "BTC"
Summary
Types
Extended parse result — alias of Bourse.Symbol.ParsedSymbol.t/0.
Functions
Applies a currency alias mapping to a single currency code.
Builds a unified symbol from components.
Converts dates between derivative symbol formats.
Converts a unified symbol to exchange-specific format.
Converts a unified symbol to WebSocket channel format.
Detects market type from parsed extended symbol components.
Converts an exchange-specific ID to unified symbol format.
Converts an exchange-specific ID to unified symbol, raising on failure.
Returns the list of known quote currencies, optionally extended with exchange-specific currencies.
Converts an exchange-specific symbol to unified format.
Parses a unified symbol into its components.
Parses a unified symbol into its components, raising on error.
Parses a unified symbol into extended components including derivative fields.
Inverts an alias map for reverse lookups (unified → exchange code).
Raises ArgumentError when multiple exchange codes map to the same unified
code, because no map inversion can preserve both authored codes.
Strips known exchange prefixes from a symbol.
Converts a unified symbol to exchange-specific ID.
Converts a unified symbol to exchange-specific ID, raising on failure.
Types
@type parsed_extended() :: Bourse.Symbol.ParsedSymbol.t()
Extended parse result — alias of Bourse.Symbol.ParsedSymbol.t/0.
@type symbol_format() :: %{separator: String.t(), case: :upper | :lower | :mixed}
@type ws_symbol_format() ::
:dash_separated
| :lowercase_no_slash
| :uppercase_no_slash
| :slash
| :unknown
Functions
Applies a currency alias mapping to a single currency code.
Used with commonCurrencies data from exchange specs.
Bourse.Symbol.apply_alias("XBT", %{"XBT" => "BTC"})
#=> "BTC"
Bourse.Symbol.apply_alias("ETH", %{"XBT" => "BTC"})
#=> "ETH"
Builds a unified symbol from components.
Bourse.Symbol.build("BTC", "USDT")
#=> "BTC/USDT"
Bourse.Symbol.build("BTC", "USD", "BTC")
#=> "BTC/USD:BTC"
Converts dates between derivative symbol formats.
Supported formats: :yymmdd, :ddmmmyy, :yyyymmdd.
Raises ArgumentError naming both formats and the input when the pair is
unsupported. Raises ArgumentError naming the input and source format when
the input does not match the declared source format.
Bourse.Symbol.convert_date("260327", :yymmdd, :ddmmmyy)
#=> "27MAR26"
Bourse.Symbol.convert_date("27MAR26", :ddmmmyy, :yymmdd)
#=> "260327"
Bourse.Symbol.convert_date("260327", :yymmdd, :yyyymmdd)
#=> "20260327"
@spec denormalize(String.t(), symbol_format()) :: String.t()
Converts a unified symbol to exchange-specific format.
Strips settle currency (colon suffix) before conversion, then applies separator replacement and case transformation.
Parameters
symbol- The unified symbol (e.g., "BTC/USDT" or "BTC/USDT:USDT")format- Map with:separatorand:casekeys
@spec denormalize_ws(String.t(), ws_symbol_format()) :: String.t()
Converts a unified symbol to WebSocket channel format.
WebSocket channels often use different symbol formats than REST APIs.
@spec detect_market_type(parsed_extended() | map()) :: :spot | :swap | :future | :option
Detects market type from parsed extended symbol components.
Priority: option > future > swap > spot.
Accepts %Bourse.Symbol.ParsedSymbol{} (from parse_extended/1) or a map with
the same keys for call-site convenience.
@spec from_exchange_id(String.t(), Bourse.Exchange.t(), atom()) :: String.t()
Converts an exchange-specific ID to unified symbol format.
Requires market_type since exchange IDs are ambiguous without context.
Contract (identity, unified conversion, or raise)
Under the selected pattern the result is one of:
- Identity — the pattern grammar does not match (e.g. Deribit combo
ids under
:option/:future); the input id is returned unchanged. No intermediate rewrite (upcase,d→D) is emitted. - Unified conversion — a unified symbol containing
/(grammar matched). - Raise — a non-identity rewrite that is still exchange-id-shaped (no
/). That is silent corruption from a partial transform, not a soft fallback.
This is the public-API counterpart of carve C27: combo native ids are not a special-cased branch here; they take the identity path because the single-leg grammar cannot represent them.
No-separator splitting extends get_quote_currencies/1 with the venue's
common_currencies keys and values, so authored aliases are quote candidates.
normalize/3 can pass an explicit :quote_currencies list; this function
does not take that option.
Parameters
exchange_id- The exchange-specific ID (e.g., "BTCUSDT_260327")exchange- A%Bourse.Exchange{}struct withsymbol_patternspopulatedmarket_type- The market type (:spot,:swap,:future,:option)
Examples
Bourse.Symbol.from_exchange_id("BTCUSDT", binance_exchange, :spot)
#=> "BTC/USDT"
Bourse.Symbol.from_exchange_id("BTC-PERPETUAL", deribit_exchange, :swap)
#=> "BTC/USD:BTC"
Bourse.Symbol.from_exchange_id("BTC-12JAN26-84000-C", deribit_exchange, :option)
#=> "BTC/USD:BTC-260112-84000-C"
@spec from_exchange_id!(String.t(), Bourse.Exchange.t(), atom()) :: String.t()
Converts an exchange-specific ID to unified symbol, raising on failure.
Same conversion contract as from_exchange_id/3. Also raises when no pattern
config exists for market_type.
Returns the list of known quote currencies, optionally extended with exchange-specific currencies.
Currencies are sorted by length descending for longest-match-first splitting.
Bourse.Symbol.get_quote_currencies()
#=> ["FDUSD", "USDD", "USDT", "USDC", "BUSD", "TUSD", ...]
Bourse.Symbol.get_quote_currencies(["TRY", "BRL"])
#=> ["FDUSD", "USDD", "USDT", ..., "TRY", "BRL"]
@spec normalize(String.t(), symbol_format(), keyword()) :: String.t()
Converts an exchange-specific symbol to unified format.
Takes a format map with :separator and :case keys. Optionally applies
currency aliases (from commonCurrencies spec data) to map exchange-specific
codes to unified codes.
Parameters
symbol- The exchange-specific symbol (e.g., "BTCUSDT")format- Map with:separatorand:casekeysopts- Keyword options::aliases- Currency alias map applied to each currency after splitting (e.g.,%{"XBT" => "BTC"}). Not a substring rewrite of the raw id.:quote_currencies- Custom list of known quote currencies for no-separator splitting
Returns
The unified symbol (e.g., "BTC/USDT"), or original if cannot parse.
@spec parse(String.t()) :: {:ok, parsed_symbol()} | {:error, :invalid_format}
Parses a unified symbol into its components.
Returns
{:ok, %{base, quote, settle}} or {:error, :invalid_format}.
Bourse.Symbol.parse("BTC/USDT")
#=> {:ok, %{base: "BTC", quote: "USDT", settle: nil}}
Bourse.Symbol.parse("BTC/USDT:USDT")
#=> {:ok, %{base: "BTC", quote: "USDT", settle: "USDT"}}
@spec parse!(String.t()) :: parsed_symbol()
Parses a unified symbol into its components, raising on error.
@spec parse_extended(String.t()) :: {:ok, parsed_extended()} | {:error, :invalid_format}
Parses a unified symbol into extended components including derivative fields.
Returns a %Bourse.Symbol.ParsedSymbol{} with the same field names as the former
ad-hoc map (base, quote, settle, expiry, strike, option_type).
Dot-access and map-pattern matching (%{base: b}) both work on the struct.
Venue-native Bybit dated linear/inverse ids (BASEQUOTE-DDMMMYY) also parse:
position rows carry that form rather than the unified symbol.
Bourse.Symbol.parse_extended("BTC/USDT:USDT-260327")
#=> {:ok, %Bourse.Symbol.ParsedSymbol{base: "BTC", quote: "USDT", settle: "USDT", expiry: "260327", strike: nil, option_type: nil}}
Bourse.Symbol.parse_extended("BTC/USD:BTC-260112-84000-C")
#=> {:ok, %Bourse.Symbol.ParsedSymbol{base: "BTC", quote: "USD", settle: "BTC", expiry: "260112", strike: "84000", option_type: "C"}}
Bourse.Symbol.parse_extended("DOGEUSDT-28AUG26")
#=> {:ok, %Bourse.Symbol.ParsedSymbol{base: "DOGE", quote: "USDT", settle: "USDT", expiry: "260828", strike: nil, option_type: nil}}
Inverts an alias map for reverse lookups (unified → exchange code).
Raises ArgumentError when multiple exchange codes map to the same unified
code, because no map inversion can preserve both authored codes.
Bourse.Symbol.reverse_aliases(%{"XBT" => "BTC", "ZEUR" => "EUR"})
#=> %{"BTC" => "XBT", "EUR" => "ZEUR"}
Strips known exchange prefixes from a symbol.
Handles KrakenFutures contract prefixes (PI, PF, FI, FF, PV_) and Kraken currency prefixes (X for crypto, Z for fiat).
Bourse.Symbol.strip_prefix("PI_XBTUSD")
#=> {"PI_", "XBTUSD"}
Bourse.Symbol.strip_prefix("XXBT")
#=> {"X", "XBT"}
Bourse.Symbol.strip_prefix("ZUSD")
#=> {"Z", "USD"}
Bourse.Symbol.strip_prefix("BTCUSDT")
#=> {nil, "BTCUSDT"}
@spec to_exchange_id(String.t(), Bourse.Exchange.t()) :: String.t()
Converts a unified symbol to exchange-specific ID.
For Binance COIN-M and grammars with an authored quote-settled suffix,
resolves the exact native ID from loaded markets before using the exchange's
symbol_patterns to handle spot, swap, future, and option symbols. Returns
the unified symbol unchanged when no pattern config exists.
Outbound currency aliases come from exchange.outbound_aliases (default
%{}), populated only for exchanges that accept the alias on input
(e.g. Kraken: BTC → XBT). Inbound aliasing (from_exchange_id) uses
common_currencies directly — that direction is universal.
Parameters
unified_symbol- The unified symbol (e.g., "BTC/USDT:USDT-260327")exchange- A%Bourse.Exchange{}struct withsymbol_patternspopulated
Examples
Bourse.Symbol.to_exchange_id("BTC/USDT", binance_exchange)
#=> "BTCUSDT"
Bourse.Symbol.to_exchange_id("BTC/USD:BTC-260112-84000-C", deribit_exchange)
#=> "BTC-12JAN26-84000-C"
@spec to_exchange_id!(String.t(), Bourse.Exchange.t()) :: String.t()
Converts a unified symbol to exchange-specific ID, raising on failure.