DpExchange.Schwab (DpExchangeSchwab v0.1.2)

Copy Markdown View Source

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

get_balances(credentials, opts \\ [])

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.

get_fees(credentials, opts \\ [])

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.

get_rate_limit_status(credentials, opts \\ [])

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.

get_symbols(opts \\ [])

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.

get_trade_history(credentials, opts \\ [])

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.

get_transfers(credentials, opts \\ [])

Not supported. Money movement is not part of the Trader API.

quotes()

@spec quotes() :: [String.t()]

Canonical candle widths this venue serves.

subscribe_notices(opts \\ [])

Refusals reach the subscriber through the same mailbox as quotes, so a caller registering here receives them.

venue_does_not_serve()

@spec venue_does_not_serve() :: [{atom(), arity()}]

Endpoints the venue does not serve, as distinct from ones not yet written.