Skip to content

WebSocket Streaming

market-feed/ws opens a persistent WebSocket connection to Polygon.io, Finnhub, Alpaca, or Interactive Brokers TWS and yields individual trade executions in real time. For providers without native WebSocket support (Yahoo, Alpha Vantage), it falls back to HTTP polling automatically.

Basic usage

ts
import { connect } from "market-feed/ws";
import { FinnhubProvider } from "market-feed";

const provider = new FinnhubProvider({ apiKey: process.env.FINNHUB_KEY! });
const controller = new AbortController();

for await (const event of connect(provider, ["AAPL", "MSFT", "TSLA"], {
  signal: controller.signal,
})) {
  switch (event.type) {
    case "trade":
      console.log(`${event.trade.symbol}: $${event.trade.price} × ${event.trade.size}`);
      break;
    case "connected":
      console.log(`Connected to ${event.provider}`);
      break;
    case "disconnected":
      console.log(`Disconnected (attempt ${event.attempt})`);
      break;
  }
}

controller.abort();

Event types

trade

ts
{
  type: "trade";
  trade: {
    symbol: string;
    price: number;
    size: number;
    timestamp: Date;
    conditions?: string[];
  };
}

connected / disconnected

ts
{ type: "connected";    provider: string; }
{ type: "disconnected"; provider: string; attempt: number; reconnecting: boolean; }

error

ts
{ type: "error"; error: Error; recoverable: boolean; }

Provider support

ProviderWebSocketNotes
PolygonProviderNative WSSubscribes to T.* trade channel; auth via JSON handshake
FinnhubProviderNative WSToken in URL; per-symbol subscribe messages
AlpacaProviderNative WSFree IEX or paid SIP feed; auth via key/secret JSON message
IbTwsProviderNative WSConnects to local TWS/IB Gateway; streams level I market data
YahooProviderPolling fallbackPolls quote() every 5 s
AlphaVantageProviderPolling fallbackSame as Yahoo
TwelveDataProviderPolling fallbackSame as Yahoo
TiingoProviderPolling fallbackSame as Yahoo

Alpaca

Free Alpaca account required. Sign up at alpaca.markets

ts
import { AlpacaProvider } from "market-feed";
import { connect } from "market-feed/ws";

const provider = new AlpacaProvider({
  keyId:     process.env.ALPACA_KEY_ID!,
  secretKey: process.env.ALPACA_SECRET_KEY!,
  feed: "iex",  // "iex" (free) or "sip" (paid, full SIP feed)
});

for await (const event of connect(provider, ["AAPL", "MSFT"])) {
  if (event.type === "trade") {
    console.log(`${event.trade.symbol}: $${event.trade.price}`);
  }
}

Interactive Brokers TWS

Connects to a locally running IB TWS or IB Gateway with the Client Portal API enabled. The session must be authenticated via the browser before connecting.

ts
import { IbTwsProvider } from "market-feed";
import { connect } from "market-feed/ws";

// conidMap maps symbols to IB contract IDs.
// Look up conids at https://localhost:5000 → Contract Lookup,
// or via GET /v1/api/iserver/secdef/search?symbol=AAPL
const provider = new IbTwsProvider({
  conidMap: {
    AAPL:  265598,
    MSFT:  272093,
    TSLA:  76792991,
    GOOGL: 208813719,
  },
  // host: "localhost",  // default
  // port: 5000,         // TWS default (5001 for IB Gateway)
});

for await (const event of connect(provider, ["AAPL", "MSFT"])) {
  if (event.type === "trade") {
    console.log(`${event.trade.symbol}: $${event.trade.price}`);
  }
}

Options

ts
interface WsOptions {
  /** Custom WebSocket constructor — required on Node 18–20. */
  wsImpl?: typeof WebSocket;

  /** Max reconnect attempts before giving up. Default: 10 */
  maxReconnectAttempts?: number;

  /** Base reconnect delay in ms. Doubles per attempt, capped at 30 s. Default: 1 000 */
  reconnectDelayMs?: number;

  /** AbortSignal to stop the stream. */
  signal?: AbortSignal;
}

Node 18–20

Node 21+, Bun, Deno, and Cloudflare Workers expose WebSocket globally. For Node 18–20, install the ws package and inject it:

bash
npm install ws
npm install --save-dev @types/ws
ts
import WebSocket from "ws";
import { connect } from "market-feed/ws";
import { PolygonProvider } from "market-feed";

const provider = new PolygonProvider({ apiKey: process.env.POLYGON_KEY! });

for await (const event of connect(provider, ["AAPL"], {
  wsImpl: WebSocket as unknown as typeof globalThis.WebSocket,
})) {
  // ...
}

Reconnection

The stream reconnects automatically on disconnect with exponential backoff:

attempt 1: wait 1 s
attempt 2: wait 2 s
attempt 3: wait 4 s
...
attempt 10: wait 30 s (max)

After maxReconnectAttempts, the generator terminates. Each reconnect resets the attempt counter on successful auth.

Level II order book

getOrderBook() yields top-of-book (level I) updates — best bid and ask — for a single symbol.

ts
import { getOrderBook } from "market-feed/ws";
import { PolygonProvider } from "market-feed";

const provider = new PolygonProvider({ apiKey: process.env.POLYGON_KEY! });
const controller = new AbortController();

for await (const update of getOrderBook(provider, "AAPL", { signal: controller.signal })) {
  const bid = update.bids[0];
  const ask = update.asks[0];
  console.log(`AAPL  bid ${bid?.price} × ${bid?.size}  ask ${ask?.price} × ${ask?.size}`);
}

Provider support

ProviderSourceData
PolygonProviderQ.* NBBO quote channelReal bid/ask price + size
AlpacaProviderquotes subscriptionReal bid/ask price + size (IEX or SIP)
IbTwsProviderFields 84/86 (bid/ask)Real bid/ask price from TWS
All othersHTTP pollingSynthetic 1-level book from last trade price

OrderBookEvent

ts
interface OrderBookEvent {
  symbol: string;
  /** Bids sorted price descending — index 0 is best bid */
  bids: OrderBookLevel[];
  /** Asks sorted price ascending — index 0 is best ask */
  asks: OrderBookLevel[];
  timestamp: Date;
}

interface OrderBookLevel {
  price: number;
  size: number;  // 0 when size is unavailable (polling fallback)
}

OrderBookOptions

Extends WsOptions with one additional field:

OptionDefaultDescription
pollIntervalMs2000Poll interval for non-WS providers (ms)

vs. market-feed/stream

See the HTTP Polling Stream comparison table.

Released under the MIT License.