This venue's feed — a REST poll, and nothing outside this module needs to know that.
Why this is a poll, and why that is not a statement about the venue
Schwab has a WebSocket Streamer. This module does not speak it yet. Those are different facts, and this moduledoc used to conflate them: it said neither Trader API specification describes a streaming surface, which is true, and left the reader to conclude the venue has none, which is false.
The Streamer carries 15 services — LEVELONE_* quotes, NYSE_BOOK, NASDAQ_BOOK and
OPTIONS_BOOK for depth, CHART_* for candles, and ACCT_ACTIVITY for order and fill
events. It is documented in the prose beside the specifications, committed at
docs/reference/schwab/documentation/market-data-production.txt, and its bootstrap is
GET /userPreference, which returns streamerInfo.streamerSocketUrl.
The error was reading the OpenAPI documents and stopping there. Both are honest about their own scope; neither claims to describe the whole venue. Nothing warned that the prose beside them held a second transport.
Until the Streamer is implemented, this is a REST poll served by Core.PollingFeed, and
behind the feed that does not matter: the same Core.Types.Quote reaches the same
subscriber as from a socket venue, no consumer branches on transport, and coverage/1
reports what is actually arriving rather than what was subscribed (D12, §6.1.8). When the
Streamer lands, streamable gains :order_book, :orders and :fills, and no consumer
should have to change.
The market closes, and silence is usually correct
This is the first venue in the family where delivering nothing is the normal overnight
state rather than a fault. A consumer that alarms on silence would alarm every night and
all weekend, which makes a real outage indistinguishable from a Saturday — so
market_status/1 exists, is answered from /markets, and is the thing to check before
concluding a quiet feed is broken.
The feed does not stop itself when the market closes. That is deliberate: pausing would make "closed" and "crashed" look the same from outside, and pre-market and post-market sessions are real trading windows this package must not decide are uninteresting.
One request per symbol, and the cost is real
/quotes accepts several symbols at once, but the throttle that matters here applies to
order writes, not reads — reads are documented as unthrottled. Even so, each poll is a
signed request against a token with a 30-minute life, so Core.PollingFeed spreads
symbols across the interval rather than sweeping them in a burst.
Summary
Functions
Child spec, so a consumer supervises this the same way it supervises any venue.
What is actually arriving, per symbol.
Default poll interval in milliseconds.
Start the poller. :credentials are the host's, and are passed to every fetch.
Whether the poller is delivering, and what it last failed on.
Replace the polled set.
Functions
@spec child_spec(keyword()) :: Supervisor.child_spec()
Child spec, so a consumer supervises this the same way it supervises any venue.
What is actually arriving, per symbol.
Observed, never intended: a symbol asked for and never answered is absent rather than reported as covered, because reporting it would assert a delivery that never happened. On this venue that distinction does double duty — overnight, nothing is arriving and nothing is wrong.
@spec interval_ms() :: pos_integer()
Default poll interval in milliseconds.
@spec start_link(keyword()) :: GenServer.on_start()
Start the poller. :credentials are the host's, and are passed to every fetch.
Whether the poller is delivering, and what it last failed on.
Replace the polled set.