DpExchange.Schwab.Feed (DpExchangeSchwab v0.1.2)

Copy Markdown View Source

This venue's feed — a REST poll, and nothing outside this module needs to know that.

Why a venue with no socket still has a feed

Neither of Schwab's Trader API specifications describes a streaming surface. Behind a feed that does not matter: the same Core.Types.Quote reaches the same subscriber as from a WebSocket venue, no consumer branches on transport, and coverage/1 reports what is actually arriving rather than what was subscribed (D12, §6.1.8).

Schwab does publish a separate Thinkorswim API product, which is where a streaming quote surface would live if it exists. Nothing here has been checked against it, and it is out of scope for this package — recorded so a reader looking for a Schwab socket knows where to look next rather than concluding there is none.

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

child_spec(opts)

@spec child_spec(keyword()) :: Supervisor.child_spec()

Child spec, so a consumer supervises this the same way it supervises any venue.

coverage(feed)

@spec coverage(pid() | atom()) :: %{required(String.t()) => :internal_poll}

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.

interval_ms()

@spec interval_ms() :: pos_integer()

Default poll interval in milliseconds.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Start the poller. :credentials are the host's, and are passed to every fetch.

status(feed)

@spec status(pid() | atom()) :: map()

Whether the poller is delivering, and what it last failed on.

update_symbols(feed, symbols)

@spec update_symbols(pid() | atom(), [String.t()]) :: :ok

Replace the polled set.