DpExchange.Core.CanonicalPair (DpExchangeCore v0.2.3)

Copy Markdown View Source

The generic, venue-agnostic symbol normaliser.

Canonical form is BASE-QUOTE, uppercase, dash-separated (BTC-USD). This module holds no venue-specific knowledge — it converts given a mapping that each venue package declares inside itself. That split is what keeps Core free of venue facts: the venue supplies its separator, quote list and asset aliases; the string surgery lives here, once.

Mapping

%{
  sep: "-" | "",                 # native separator
  quotes: ["USDC", "USD", ...],  # known quote assets
  asset_aliases: %{"XBT" => "BTC"} # exchange base code → canonical (optional)
}

quotes is sorted longest-first internally, whatever order the caller gives it

A concat-style mapping (sep: "") has to find the SUFFIX among quotes that ends the native symbol, and the wrong quote can end it too: quotes: ["USD", "BUSD"] against "ETHBUSD" matches "USD" first and cuts "ETHB-USD" — wrong, because "BUSD" was the actual quote. This module used to trust the caller to list quotes longest-first, and a venue mapping that got the order wrong mis-split silently: the base/quote invariant below does not catch it, because to_exchange(m, to_canonical(m, p)) concatenates base <> quote either way and round-trips byte-for-byte regardless of where the cut landed. So quotes is sorted by length, descending, before any match is attempted — a caller cannot get the ordering wrong any more, whatever it hands in.

Invariant

to_canonical(m, to_exchange(m, p)) == p for every pair. The conformance suite asserts this against each venue's own mapping, so a venue whose two directions disagree fails before it ships.

Summary

Functions

Exchange-native symbol → canonical BASE-QUOTE, per the connector's mapping. Unparseable / already-dashed input → uppercased input (never dropped).

Canonical BASE-QUOTE → exchange-native, per the connector's mapping.

Types

mapping()

@type mapping() :: %{
  :sep => String.t(),
  :quotes => [String.t()],
  optional(:asset_aliases) => %{optional(String.t()) => String.t()}
}

Functions

to_canonical(mapping, native)

@spec to_canonical(mapping(), String.t()) :: String.t()

Exchange-native symbol → canonical BASE-QUOTE, per the connector's mapping. Unparseable / already-dashed input → uppercased input (never dropped).

to_exchange(mapping, canonical)

@spec to_exchange(mapping(), String.t()) :: String.t()

Canonical BASE-QUOTE → exchange-native, per the connector's mapping.