Canonical symbol to Schwab symbol, and back — internal.
A symbol here is one instrument, not a pair
This is the first venue in the family where that is true, and it is the whole of this
module's difficulty. Every other package normalises BTC-USD to whatever the venue
spells it as, because a crypto symbol names two things: what you are buying and what
you are paying with. AAPL names one thing. What you pay with is not in the symbol; it
is USD because the venue is a US broker.
So there is nothing to split, nothing to join, and no separator to translate. The canonical form and the native form are the same string, and the work is entirely in refusing the ones that are not equity symbols rather than in transforming anything.
Which is why this module is mostly a rejection
A pair-shaped symbol reaching this venue is a routing bug — a host that meant to send
BTC-USD to Gemini and sent it here. Passing it through would produce a 404 from
Schwab, or worse a match: BTC is a real listed equity symbol, and so are ETH and
SOL, all of them ETFs holding nothing like the coin. A crypto pair silently
resolving to an equity ticker is the exact failure this family exists to prevent —
every value stays plausible and only the meaning is wrong.
Translating and judging are two functions, and that split is forced
The obvious design is one function returning {:ok, native} | {:error, reason}. It does
not survive contact with the contract: Core.SymbolNormalizer requires
to_exchange_symbol/1 to be total, and Core's conformance suite asserts the round
trip over arbitrary input — including input this venue would refuse. A consumer
normalising a string for display must not have it raise or hand back a tuple.
But a caller about to spend a request has to refuse first, for the reason above.
So:
to_exchange_symbol/1andto_canonical_symbol/1translate, and are total. They normalise case and whitespace and return a string for anything. They judge nothing.validate/1judges, returning{:ok, native} | {:error, reason}.Rest,OrdersandFakecall this one, always, before any request leaves.
A transformation that cannot fail may return a string; a validation may not. Keeping both under one name would have meant breaking one of the two contracts.
Options are a different shape and are not handled here
Schwab's option symbols are fixed-width and positional — six characters of underlying
padded with spaces, six of expiry, one of call/put, eight of strike, as in
XYZ 210115C00050000. They round-trip through nothing in Core, whose symbol
vocabulary is a string that names an instrument. Option symbols are passed through
untouched when they already look like one, and constructing them is not this package's
job — a caller that has an option symbol got it from /chains.
Summary
Functions
Whether symbol is a Schwab option symbol by its fixed 21-character shape.
Native to canonical — total, and the identity on anything to_exchange_symbol/1
produced.
Canonical to native — total, per Core.SymbolNormalizer.
Whether symbol is something this venue can be asked about, and the native form if so.
Types
Functions
Whether symbol is a Schwab option symbol by its fixed 21-character shape.
Native to canonical — total, and the identity on anything to_exchange_symbol/1
produced.
It exists so the round trip is expressible and testable, not because it transforms anything. If Schwab ever returns a spelling that differs from what it accepts, this is where that is absorbed rather than at every call site.
Canonical to native — total, per Core.SymbolNormalizer.
Translation only. On this venue a canonical symbol and a native symbol are the same string, so this normalises case and whitespace and returns what it was given otherwise. It does not judge, because the contract requires a total function and a translation that raises on bad input is not one.
Judging is validate/1's job, and callers that are about to spend a request use
that instead. The split is deliberate: Rest and Orders must refuse a pair-shaped
symbol before sending, while a consumer normalising a string for display must not have
that blow up in its hands.
Whether symbol is something this venue can be asked about, and the native form if so.
Returns {:error, {:not_an_equity_symbol, symbol}} for anything pair-shaped rather than
letting it through. See the module doc: BTC is a listed ETF, so a crypto pair arriving
here has a plausible wrong answer available, and a 404 would be the lucky outcome.
This is what Rest and Orders call. to_exchange_symbol/1 is the contract's total
translation and does not refuse anything.