Charles Schwab's Trader API, behind the family's shared facade.
EXPERIMENTAL. Nothing here has run in production, and on this venue that is
structural rather than temporary: every endpoint needs OAuth credentials this repository
must never hold, and Schwab publishes no sandbox — its own documentation says Trader
API sandboxes "will be available later this year", and neither specification declares a
non-production server. There is nowhere to exercise this package that is not somebody's
real money, so maturity is :experimental throughout and moves only when a consumer
trades live (D15).
What is different about this venue
Five things, and each shows up in the contract rather than being smoothed over:
A symbol is one instrument, not a pair. Every other venue in the family addresses
BASE-QUOTE. Here AAPL names a single security and what you pay with is USD because
the venue is a US broker. SymbolFormat.validate/1 therefore refuses pair-shaped input
instead of splitting it — BTC, ETH and SOL are all real listed equity tickers, so a
misrouted crypto pair has a plausible wrong answer waiting for it.
The market closes. market_status/1 is answered from /markets, not assumed. A feed
delivering nothing at 3am is correct, and a consumer that alarms on silence would alarm
every night — making a real outage indistinguishable from a Saturday.
The host authenticates; this package signs and refreshes. The initial grant is
three-legged OAuth through a browser and a person, which no library can do. Everything
after is mechanical: the access token lives 30 minutes and Auth.refresh/2 renews it,
minting a new refresh token each time with a fresh seven days. A host that keeps
refreshing never needs a person again.
There is no order book and no socket. Neither specification describes depth or
streaming, so get_order_book/2 is :unsupported and the feed is a poll. Schwab
publishes a separate Thinkorswim product where a streaming surface would live; it is out
of scope here and named so a reader knows where to look.
The catalogue cannot be enumerated. /instruments has no list-everything projection
— all six of its projections search against a term — so get_symbols/1 requires a
:query and returns {:error, {:query_required, :schwab}} without one. That is
deliberately not :not_supported: the endpoint works, and a caller must be able to
tell "needs a term" from "has no endpoint". Returning some arbitrary search instead would
hand back a short list that looks like a catalogue.
Credentials
Passed per call, never read from a vault and never cached here (§6.0, invariant #2):
credentials = %{
access_token: "…",
refresh_token: "…",
client_id: "…",
client_secret: "…"
}Only :access_token is needed to sign. The rest are needed to refresh, and the result
of a refresh must be persisted by the host — the refresh token is one-time use, so the
one returned is the only way to refresh again.
Summary
Functions
Balances for one account.
Not supported. Schwab publishes no fee-schedule endpoint. previewOrder returns an
estimated commission for one order, which the contract cannot express and which is not
a fee schedule.
Not supported. Schwab publishes no rate-limit status endpoint, and its documented order ceiling is a property of the application's registration rather than something queryable at runtime.
A pull here requires a query, and that is the venue's shape rather than a gap.
Not supported yet. /transactions carries fills, but mapping a Schwab transaction
onto Core.Types.Fill needs a live response to check against and this repository holds
no credential. Declared :experimental and returning :not_supported would be a
declaration disagreeing with itself, so it is neither — see capabilities/0.
Not supported. Money movement is not part of the Trader API.
Canonical candle widths this venue serves.
Refusals reach the subscriber through the same mailbox as quotes, so a caller registering here receives them.
Endpoints the venue does not serve, as distinct from ones not yet written.
Functions
Balances for one account.
Requires :account_hash — Schwab addresses accounts by an encrypted hash, and
get_accounts/2 is the only place to get one. That makes it a prerequisite for the
whole trading surface rather than a convenience.
Not supported. Schwab publishes no fee-schedule endpoint. previewOrder returns an
estimated commission for one order, which the contract cannot express and which is not
a fee schedule.
Not supported. Schwab publishes no rate-limit status endpoint, and its documented order ceiling is a property of the application's registration rather than something queryable at runtime.
A pull here requires a query, and that is the venue's shape rather than a gap.
GET /instruments has no "list everything" projection — every lookup is a search
against a term — so the catalogue cannot be enumerated at all, only queried. Pass
:query; without one this returns {:error, {:query_required, :schwab}}, which is
deliberately not :not_supported. Returning some arbitrary search instead would hand
back a short list that looks like a catalogue.
Not supported yet. /transactions carries fills, but mapping a Schwab transaction
onto Core.Types.Fill needs a live response to check against and this repository holds
no credential. Declared :experimental and returning :not_supported would be a
declaration disagreeing with itself, so it is neither — see capabilities/0.
Not supported. Money movement is not part of the Trader API.
@spec quotes() :: [String.t()]
Canonical candle widths this venue serves.
Refusals reach the subscriber through the same mailbox as quotes, so a caller registering here receives them.
Endpoints the venue does not serve, as distinct from ones not yet written.