# `DpExchange.Core.CanonicalPair`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.116/lib/dp_exchange/core/canonical_pair.ex#L1)

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.

# `mapping`

```elixir
@type mapping() :: %{
  :sep =&gt; String.t(),
  :quotes =&gt; [String.t()],
  optional(:asset_aliases) =&gt; %{optional(String.t()) =&gt; String.t()}
}
```

# `to_canonical`

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

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

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

Input with no quote part is not a pair and is never decorated with a trailing separator,
which would be a symbol no venue was ever asked about. It comes back uppercased, with an
`asset_aliases` entry reversed if one applies, and otherwise as it was given. Input with
no base part (`"-USD"`) is not a pair either, and comes back uppercased and otherwise
untouched rather than as the bare quote.

---

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