Bourse.Symbol (bourse v0.8.0)

Copy Markdown View Source

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

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

parsed_extended()

@type parsed_extended() :: Bourse.Symbol.ParsedSymbol.t()

Extended parse result — alias of Bourse.Symbol.ParsedSymbol.t/0.

parsed_symbol()

@type parsed_symbol() :: %{
  base: String.t(),
  quote: String.t(),
  settle: String.t() | nil
}

pattern_config()

@type pattern_config() :: %{
  optional(:quote_settled_suffix) => String.t(),
  pattern: atom(),
  separator: String.t(),
  case: :upper | :lower | :mixed,
  date_format: :yymmdd | :ddmmmyy | :yyyymmdd | nil,
  suffix: String.t() | nil,
  prefix: String.t() | nil
}

symbol_format()

@type symbol_format() :: %{separator: String.t(), case: :upper | :lower | :mixed}

ws_symbol_format()

@type ws_symbol_format() ::
  :dash_separated
  | :lowercase_no_slash
  | :uppercase_no_slash
  | :slash
  | :unknown

Functions

apply_alias(currency, aliases)

@spec apply_alias(String.t(), map()) :: String.t()

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"

build(base, quote_currency, settle \\ nil)

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

Builds a unified symbol from components.

Bourse.Symbol.build("BTC", "USDT")
#=> "BTC/USDT"

Bourse.Symbol.build("BTC", "USD", "BTC")
#=> "BTC/USD:BTC"

convert_date(date_str, format, format)

@spec convert_date(String.t(), atom(), atom()) :: String.t()

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"

denormalize(symbol, map)

@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 :separator and :case keys

denormalize_ws(symbol, arg2)

@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.

detect_market_type(parsed)

@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.

from_exchange_id(exchange_id, exchange, market_type)

@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:

  1. 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, dD) is emitted.
  2. Unified conversion — a unified symbol containing / (grammar matched).
  3. 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 with symbol_patterns populated
  • market_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"

from_exchange_id!(exchange_id, exchange, market_type)

@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.

get_quote_currencies(extra \\ nil)

@spec get_quote_currencies([String.t()] | nil) :: [String.t()]

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"]

normalize(symbol, format, opts \\ [])

@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 :separator and :case keys
  • opts - 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.

parse(symbol)

@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"}}

parse!(symbol)

@spec parse!(String.t()) :: parsed_symbol()

Parses a unified symbol into its components, raising on error.

parse_extended(symbol)

@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}}

reverse_aliases(aliases)

@spec reverse_aliases(map()) :: map()

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"}

strip_prefix(symbol)

@spec strip_prefix(String.t()) :: {String.t() | nil, String.t()}

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"}

to_exchange_id(unified_symbol, exchange)

@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: BTCXBT). 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 with symbol_patterns populated

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"

to_exchange_id!(unified_symbol, exchange)

@spec to_exchange_id!(String.t(), Bourse.Exchange.t()) :: String.t()

Converts a unified symbol to exchange-specific ID, raising on failure.