DpExchange.Schwab.SymbolFormat (DpExchangeSchwab v0.1.3)

Copy Markdown View Source

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/1 and to_canonical_symbol/1 translate, and are total. They normalise case and whitespace and return a string for anything. They judge nothing.
  • validate/1 judges, returning {:ok, native} | {:error, reason}. Rest, Orders and Fake call 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

Types

A symbol as this package's callers spell it.

A symbol as Schwab spells it.

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

canonical()

@type canonical() :: String.t()

A symbol as this package's callers spell it.

native()

@type native() :: String.t()

A symbol as Schwab spells it.

Functions

option?(symbol)

@spec option?(String.t()) :: boolean()

Whether symbol is a Schwab option symbol by its fixed 21-character shape.

to_canonical_symbol(symbol)

@spec to_canonical_symbol(native()) :: canonical()

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.

to_exchange_symbol(symbol)

@spec to_exchange_symbol(canonical()) :: native()

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.

validate(symbol)

@spec validate(canonical()) :: {:ok, native()} | {:error, term()}

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.