Webull's symbol mapping.
Native form is concatenated, no separator: BTCUSD. Canonical is BASE-QUOTE. So
both directions do real work, and the same overlapping-quote hazard applies as on any
sep: "" venue — the quote list is consulted in order, and a shorter quote that is also
the tail of a longer one wins if checked first.
USDT and USDC therefore precede USD. Without that, BTCUSDT splits as BTCUS/DT
or BTCUSD/T depending on the list, and neither is an asset.
Why both directions run through CanonicalPair
So they cannot drift. The adapter this came from records why that matters: an audit
found build_order_body sending the canonical symbol to the venue un-converted, so
reads worked and writes silently did not. Running every path — read and write — through
one mapping is the fix, and it only works if there is exactly one mapping.
That is also why to_canonical_symbol/1 and to_exchange_symbol/1 read the mapping
through mapping/0 rather than the private @mapping attribute directly: a second,
attribute-shaped copy of "the mapping" sitting beside the function of the same name is
the exact drift this module exists to rule out, even though today the two happen to
hold the same value.
Summary
Functions
@spec mapping() :: DpExchange.Core.CanonicalPair.mapping()
The mapping — the single source both directions and the conformance suite read from.
@spec quotes() :: [String.t()]
The quote currencies this venue settles in.