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
| Property | Value |
|---|---|
| URL | wss://api.unusualwhales.com/socket?token={UW_TOKEN} |
| Auth | Token in URL query parameter (not header) |
| Message format | JSON |
| Throughput | Hundreds to thousands of msg/sec |
| Server backpressure | Server 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:
| Pattern | Example | Description |
|---|---|---|
<channel> | flow-alerts, market-tide | Global channels (one stream for all tickers) |
<channel>:<TICKER> | option_trades:SPY, gex:SPY | Per-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)
| Constant | Value | Purpose |
|---|---|---|
TIMEOUT_LENGTH | 30s | Receive timeout |
MAX_RECONNECT_ATTEMPTS | 5 | Reconnect attempt cap |
RECONNECT_DELAY | 5s | Base reconnect delay |
RECONNECT_DELAY_MAX | 60s | Cap 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)
| # | Channel | Naming | REST equivalent | Layer |
|---|---|---|---|---|
| 1 | channels | (meta - list available) | n/a | reference |
| 2 | contract-screener | global, with query params | uw-api-screener /screener/option-contracts | L1 |
| 3 | custom-alerts | global, user-config-bound | uw-api-alerts /api/alerts | L1 |
| 4 | flow-alerts | global | uw-api-option-trade /flow-alerts | L1 load-bearing |
| 5 | gex:{TICKER} | per-ticker | uw-api-gex-greeks /greek-exposure* | L1 load-bearing |
| 6 | interval_flow:{TICKER} | per-ticker | uw-api-ticker-flow | L1 load-bearing |
| 7 | lit_trades:{TICKER} | per-ticker | uw-api-lit-flow /lit-flow/{ticker} | L1 load-bearing |
| 8 | market-tide | global | uw-api-tide /market-tide | L1 load-bearing |
| 9 | net_flow:{TICKER} | per-ticker | uw-api-net-flow /expiry | L1 |
| 10 | news | global | uw-api-news /news/headlines | L1 (circuit-breaker) |
| 11 | off_lit_trades:{TICKER} | per-ticker (dark pool) | uw-api-darkpool /darkpool/{ticker} | L1 load-bearing |
| 12 | option_trades:{TICKER} | per-ticker | uw-api-option-trade /full-tape | L1 load-bearing |
| 13 | price:{TICKER} | per-ticker | uw-api-ticker-meta /last-stock-state | L1 reference |
| 14 | trading-halts | global | n/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:
| Channel | Why |
|---|---|
option_trades:SPY | Per-trade options tape with Greeks |
option_trades:QQQ | Cross-index context |
lit_trades:SPY | Equity lit prints |
off_lit_trades:SPY | Equity dark prints |
flow-alerts | UW pre-classified clusters |
market-tide | Market-wide regime |
gex:SPY | Per-ticker GEX evolution |
news (major_only=true filter via custom-alerts) | Event labels |
trading-halts | Regulatory 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.
5. Recommended client architecture (from UW skills page)
| Concern | Recommendation |
|---|---|
| Token | From env (UW_TOKEN); never hardcoded |
| JSON parsing | orjson not stdlib json |
| Queue depth | ~50,000 (throughput × acceptable lag seconds) |
| Batching trigger | Both size-based AND time-based (1s flush + N-message flush) |
| Receive loop | Minimal work; offload processing to consumer task |
| Logging | Queue depth + drop counter |
| Reconnect | Exponential 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
UWWsMessageenvelope + per-channel payload types. Re-use the custom-data classes already sketched in the REST per-channel pages. - Phase 2: data client.
UWLiveDataClientextends Nautilus’sLiveDataClient. 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):
- Reference nautilus-dev-adapters (Phase 1-7).
- Reference this page (channel catalog + connection contract).
- Reference the official Python example for the connection skeleton:
https://github.com/unusual-whales/api-examples/tree/main/examples/ws-multi-channel-multi-output - 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
channelsdirectory on first connect. - Subscribe parameters per channel (e.g.
gex:{TICKER}is clear; but doescontract-screeneraccept query-style filter parameters in the join frame?). Need first-connect exploration. custom-alertsconfig 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.channelshttps://unusualwhales.com/skills/websocket.mdhttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.contract_screenerhttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.custom_alertshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.flow_alertshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.gexhttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.interval_flowhttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.lit_tradeshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.market_tidehttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.net_flowhttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.newshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.off_lit_tradeshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.option_tradeshttps://api.unusualwhales.com/docs/operations/PublicApi.SocketController.pricehttps://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