DpExchange.Schwab.Capabilities (DpExchangeSchwab v0.1.2)

Copy Markdown View Source

What Schwab's Trader API can do, declared before anything was written against it.

Every value here is read out of the two OpenAPI documents committed under docs/reference/schwab/openapi/, or out of the portal's Documentation tab beside them. Nothing is recalled and nothing is carried over from another venue. The distinction earns its keep: Gemini's own published candle-width list names three widths its API rejects, and only measurement caught that.

docs/reference/schwab/spec-facts.md names the schema or parameter behind each value. When something here changes, that file is what it must be checked against.

Nothing here is measured against the live API

Reading a specification is tier-1 evidence (D7). It supports :experimental and it can never support :proven, because a specification describes what a venue is documented to do rather than what it does. Every endpoint below is therefore :experimental or :unsupported, and the one live observation on record is an unauthenticated 401 from /marketdata/v1/quotes, which confirms the base URL and nothing else.

This venue is not shaped like the others

Five differences matter enough to state before the values:

A symbol is one instrument, not a pair. Every other venue in the family addresses a pair and returns one quote shape. Schwab has seven quote shapes across AssetMainType and a symbol names a single security.

The catalogue cannot be enumerated. /instruments has no list-everything projection, so get_symbols/1 is declared active but requires a :query. It is not :unsupported, and the difference is the point: "needs a search term" and "has no endpoint" are different facts, and a caller has to be able to act on each.

There is no order book and no socket. No endpoint in either document returns depth, and neither document describes a streaming surface. The feed is a REST poll served by Core.PollingFeed.

The market closes. /markets answers isOpen directly. This is the venue market_status/1 was added to the contract for — a feed that alarms on silence would alarm every night, making a real outage indistinguishable from Saturday.

There is no sandbox. Schwab writes that Trader API sandboxes "will be available later this year" in a document published 2025-10-30, and neither spec declares a non-production server. Unlike Gemini, this venue cannot be exercised end to end without touching real money.

Summary

Types

A {name, arity} pair naming one Core.Venue callback.

Functions

The declaration.

The eight candle widths GET /pricehistory serves.

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

Types

endpoint()

@type endpoint() :: {atom(), arity()}

A {name, arity} pair naming one Core.Venue callback.

Functions

declaration()

@spec declaration() :: DpExchange.Core.Capabilities.t()

The declaration.

Built through Capabilities.new/1 so Core validates it: an endpoint naming a callback that does not exist, an order type outside the contract's vocabulary, or a malformed ceiling all raise here rather than being discovered by a consumer.

timeframes()

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

The eight candle widths GET /pricehistory serves.

A width is not one parameter here. It is a (periodType, frequencyType, frequency) triple, and the legal combinations are constrained in both directions — see spec-facts.md §1 for the full table.

The minute widths are only reachable through periodType=day, which caps the lookback at ten days. Ten days of one-minute candles is the most this venue will serve, and no combination of the other parameters extends it. A consumer asking for a year of one-minute data cannot be served and must be told so — handing back a coarser series instead is the substitution this family exists to refuse.

venue_does_not_serve()

@spec venue_does_not_serve() :: [endpoint()]

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

Everything named here is :unsupported in declaration/0, and the test suite asserts that rather than trusting it.