Unusual Whales API - WebSocket streaming

Layer mapping: L1 LOAD-BEARING. Fourteen channels under SocketController. The WebSocket side eliminates polling latency for time-sensitive flows; almost every “RTH every minute” pattern in the REST pages becomes “subscribe once, react push-by-push” here.

This is the IMPULSE-engine substrate path forward. Per 2026-05-15-mk3-data-foundation-constraint, MK2 never recorded raw UW stream because no live capture pipeline existed. Subscribing to these channels + writing to local Parquet from day-zero of MK3 paper rollout fills that gap.

1. Connection contract

PropertyValue
URLwss://api.unusualwhales.com/socket?token={UW_TOKEN}
AuthToken in URL query parameter (not header)
Message formatJSON
ThroughputHundreds to thousands of msg/sec
Server backpressureServer drops messages if consumer can’t keep up

1.1 Subscribe frame (join a channel)

Send after connection open:

{ "channel": "<channel-name>", "msg_type": "join" }

To leave a channel:

{ "channel": "<channel-name>", "msg_type": "leave" }

Multiple channels = multiple frames. No batch-subscribe.

1.2 Channel naming convention

Two patterns based on the channel:

PatternExampleDescription
<channel>flow-alerts, market-tideGlobal channels (one stream for all tickers)
<channel>:<TICKER>option_trades:SPY, gex:SPYPer-ticker channels (one stream per symbol)

Per-ticker channels require joining once per ticker (gex:SPY, gex:QQQ, gex:IWM for the index three).

1.3 Incoming message envelope

Each message is a JSON array of [channel, payload]:

data = json.loads(message)
channel, payload = data
# channel = "gex:SPY"
# payload = { ...channel-specific fields... }

1.4 Reconnection pattern (from official examples)

ConstantValuePurpose
TIMEOUT_LENGTH30sReceive timeout
MAX_RECONNECT_ATTEMPTS5Reconnect attempt cap
RECONNECT_DELAY5sBase reconnect delay
RECONNECT_DELAY_MAX60sCap on exponential backoff

After reconnect: re-send all subscribe frames (UW does not persist subscriptions across socket lifetimes).

1.5 Keepalive

After TIMEOUT_LENGTH seconds without messages, send ws.ping() and wait_for(pong, timeout=10). If pong fails, treat connection as dead and reconnect.

2. Channel catalog (14 channels)

#ChannelNamingREST equivalentLayer
1channels(meta - list available)n/areference
2contract-screenerglobal, with query paramsuw-api-screener /screener/option-contractsL1
3custom-alertsglobal, user-config-bounduw-api-alerts /api/alertsL1
4flow-alertsglobaluw-api-option-trade /flow-alertsL1 load-bearing
5gex:{TICKER}per-tickeruw-api-gex-greeks /greek-exposure*L1 load-bearing
6interval_flow:{TICKER}per-tickeruw-api-ticker-flowL1 load-bearing
7lit_trades:{TICKER}per-tickeruw-api-lit-flow /lit-flow/{ticker}L1 load-bearing
8market-tideglobaluw-api-tide /market-tideL1 load-bearing
9net_flow:{TICKER}per-tickeruw-api-net-flow /expiryL1
10newsglobaluw-api-news /news/headlinesL1 (circuit-breaker)
11off_lit_trades:{TICKER}per-ticker (dark pool)uw-api-darkpool /darkpool/{ticker}L1 load-bearing
12option_trades:{TICKER}per-tickeruw-api-option-trade /full-tapeL1 load-bearing
13price:{TICKER}per-tickeruw-api-ticker-meta /last-stock-stateL1 reference
14trading-haltsglobaln/a (REST not documented)L1 (circuit-breaker)

Note: the exact channel name strings (hyphen vs underscore, case) need to be confirmed on first connect. The official example uses option_trades:TSLA, gex:SPY, flow-alerts - inconsistent naming (some hyphen, some underscore). The names above are the endpoint-doc-derived guesses; verify against the live channels directory channel before going live.

3. Per-channel notes for SPY hunter

3.1 flow-alerts (global)

Replaces 30s polling of /api/option-trades/flow-alerts. Same 9-rule classification per uw-api-option-trade. Each push is one cluster record.

3.2 gex:SPY + gex:QQQ + gex:IWM

Replaces 5-min polling of uw-api-tide /market-tide. Per-ticker real-time GEX updates. The official examples sample uses all three indices.

3.3 option_trades:SPY (HIGH VOLUME)

The full SPY options tape. High volume warning: subscribing to SPY option_trades without server-side filtering is the highest throughput single subscription on this account. Have batching + queue backpressure handling before joining.

3.4 lit_trades:SPY + off_lit_trades:SPY (HIGH VOLUME)

Equity tape, lit + dark. Same volume warning as above. The combined stream is the equity-aggressor input the SPY hunter needs for uw-api-darkpool / uw-api-lit-flow derived features in real-time.

3.5 market-tide (global)

User-flagged-important from uw-api-tide. Replaces 5-min polling. Push-cadence likely 1-min (matches the REST interval_5m=false mode).

3.6 trading-halts (global, circuit-breaker)

LULD halts, regulatory halts. Critical L1 circuit-breaker: if a SPY-driving mega-cap halts (or SPY itself halts), suspend new entries immediately.

3.7 news (global, circuit-breaker)

Realtime version of uw-api-news /news/headlines?major_only=true. Same circuit-breaker pattern: 5-min entry suspension after major macro / mega-cap headlines.

4. The IMPULSE-substrate recording strategy

For the data-foundation gap from 2026-05-15-mk3-data-foundation-constraint:

Subscribe to the following at MK3 paper rollout, write each push to Parquet locally:

ChannelWhy
option_trades:SPYPer-trade options tape with Greeks
option_trades:QQQCross-index context
lit_trades:SPYEquity lit prints
off_lit_trades:SPYEquity dark prints
flow-alertsUW pre-classified clusters
market-tideMarket-wide regime
gex:SPYPer-ticker GEX evolution
news (major_only=true filter via custom-alerts)Event labels
trading-haltsRegulatory events

Local Parquet partitioned by date and channel. Within months this becomes the OOS-validation corpus MK2 never had. At ~1 year, MK3 has its own raw stream archive matching the 1Y UW Parquet bundle but forward-looking.

ConcernRecommendation
TokenFrom env (UW_TOKEN); never hardcoded
JSON parsingorjson not stdlib json
Queue depth~50,000 (throughput × acceptable lag seconds)
Batching triggerBoth size-based AND time-based (1s flush + N-message flush)
Receive loopMinimal work; offload processing to consumer task
LoggingQueue depth + drop counter
ReconnectExponential backoff + resubscribe

6. Nautilus integration shape

This is the right place for a custom Nautilus adapter, per nautilus-dev-adapters Phase 1-7 sequence:

  • Phase 1: schema. Define UWWsMessage envelope + per-channel payload types. Re-use the custom-data classes already sketched in the REST per-channel pages.
  • Phase 2: data client. UWLiveDataClient extends Nautilus’s LiveDataClient. One websocket connection per process; subscribes to all configured channels on startup.
  • Phase 3: message routing. Push each incoming message to the MessageBus as the matching typed data.
  • Phase 4: tests. Mock the websocket, replay recorded fixtures (which we’ll have from the recording strategy in section 4).
  • Phase 5-7: production hardening per the adapter authoring bible.

get_runtime().spawn() from nautilus-dev-rust is the canonical escape hatch for async I/O on the Rust side - but for MK3 (Python- only), this is a Python asyncio task that posts to the kernel via message_bus.publish() callbacks. Determinism: ts_event from payload timestamps where present; ts_init from receive time.

7. Codex handoff implications

When MK3 Codex implements the WebSocket adapter (Codex-first per CLAUDE.md):

  1. Reference nautilus-dev-adapters (Phase 1-7).
  2. Reference this page (channel catalog + connection contract).
  3. Reference the official Python example for the connection skeleton: https://github.com/unusual-whales/api-examples/tree/main/examples/ws-multi-channel-multi-output
  4. Reference uw-api-option-trade and other REST pages for the per-channel payload shapes (WS pushes use the same record shapes as REST responses, minus the array wrapper).

Don’t bundle this adapter with feature work - it’s its own scoped session per the exact-pin-Nautilus + scope-discipline rules in CLAUDE.md.

8. Known gaps / footguns

  • Channel name exact casing is inconsistent in the public examples (hyphen vs underscore). Verify against the live channels directory on first connect.
  • Subscribe parameters per channel (e.g. gex:{TICKER} is clear; but does contract-screener accept query-style filter parameters in the join frame?). Need first-connect exploration.
  • custom-alerts config binding - is the channel keyed to all custom alerts on the account, or do you specify which configs to stream? First-connect check.
  • Drop behavior - server drops messages on backpressure but the drop isn’t signalled to the client. Track ws receive rate vs expected to detect drops indirectly.
  • No documented heartbeat from server - client-initiated ping/pong is the only documented liveness check.
  • No documented rate limit on subscribes - probably safe to join 10+ channels per connection but verify.

9. Source URLs

  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.channels
  • https://unusualwhales.com/skills/websocket.md
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.contract_screener
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.custom_alerts
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.flow_alerts
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.gex
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.interval_flow
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.lit_trades
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.market_tide
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.net_flow
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.news
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.off_lit_trades
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.option_trades
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.price
  • https://api.unusualwhales.com/docs/operations/PublicApi.SocketController.trading_halts
  • Examples repo: https://github.com/unusual-whales/api-examples
  • Multi-channel Python example: https://github.com/unusual-whales/api-examples/tree/main/examples/ws-multi-channel-multi-output
  • Multi-channel Node.js example: https://github.com/unusual-whales/api-examples/tree/main/examples/ws-multi-channel-multi-output-nodejs

cortana-north-star uw-api-option-trade uw-api-gex-greeks uw-api-tide uw-api-darkpool uw-api-lit-flow uw-api-ticker-flow uw-api-news 2026-05-15-mk3-data-foundation-constraint 2026-05-22-uw-historical-findings 2026-05-15-mk3-setup-hunter-architecture nautilus-dev-adapters nautilus-custom-data