London Data

Nexus explainer · companion to Stack by Example

Three Ways to Stream

Lightstreamer, WebSockets and Server-Sent Events all push data to a client without polling. They sit at different layers, and Nexus uses all three for different jobs.

In short

Nexus already uses WebSockets in three places. IG's Lightstreamer price feed runs over one, the arbitrage engine's exchange connectors use them through ccxt.pro, and the RFQ simulator serves two of its own. The trader's browser console uses Server-Sent Events instead.

Lightstreamer sits one layer above WebSockets. It is a streaming server with its own subscription protocol, and that protocol normally travels over a WebSocket. The live capture reports its connection as CONNECTED:WS-STREAMING. If a WebSocket can't be opened, Lightstreamer falls back to HTTP streaming or polling.

Where each one sits

Read each stack from the bottom up. SSE is one long HTTP response. A WebSocket starts as HTTP and then switches to its own frames. Lightstreamer adds sessions and subscriptions on top of a WebSocket.

Server-Sent Events SSE

TCP + TLS
HTTP GET, kept openContent-Type: text/event-stream
Named text eventsevent: quotes
data: {…}
(optional id: and retry:)

Server → client only. The client sends anything else as ordinary HTTP requests.

WebSocket WS

TCP + TLS
HTTP GET with Upgrade: websocketserver answers 101 Switching Protocols
Frames in both directionstext or binary, plus ping/pong
Your own message formatsubscriptions, ids and replay are yours to design

Full duplex. One connection carries messages both ways (RFC 6455).

Lightstreamer LS

TCP + TLS
WebSocket, by defaultfalls back to HTTP streaming, then polling
TLCP sessioncreate, subscribe, update, control, recover
Items × fieldsmodes MERGE, DISTINCT, COMMAND, RAW

Server pushes updates; the client sends control requests (subscribe, unsubscribe) on the same session.

Where each runs in Nexus

Three separate paths. Colour marks the transport on each hop; unmarked arrows are ordinary in-process or database writes.

IG prices to the trader console

IG streaming serverLightstreamer
LS over WSMERGE subscriptions, ≤ 20 items each
Capture daemonnexus-trader capture
1 s and 1 min bars · latest quote
Postgres + Redisbars in TimescaleDB · quote cache, 180 s TTL
each push reads Redis, then SQL 1 min bars
Trader APIGET /trader/quotes/stream
SSEevent: quotes, every 1 s
Trader pageEventSource in the browser

The two ends use different transports. The market-facing feed needs subscriptions and throttling; the browser only needs a one-way snapshot. Orders already go through authenticated REST calls, and EventSource reconnects on its own.

Exchange books to the arbitrage engine

Six crypto exchangeseach with its own WS API
WSccxt.pro watch_order_book
Connectorsone asyncio task per symbol
XADD, approximate MAXLEN
Redis Streamsread by the signal engine

ccxt.pro hides each exchange's WebSocket dialect behind one method. Nexus adds backoff with jitter and a separate delay for rate limits.

RFQ simulator to its dashboard

RFQ engineFastAPI app state
WS/v1/ws/prices · /v1/ws/book
JSON snapshot every 1 s
Dashboardlive quotes with TTL countdown
REST: POST /rfq, then /execute
RFQ enginequote check, credit, limits

The WebSocket only pushes. Trades go over REST so each one is a normal request with a status code.

Side by side

Lightstreamer LSWebSocket WSServer-Sent Events SSE
What it isA streaming server and client libraries with their own protocol (TLCP)A browser and network standard for a two-way channelA browser standard for a one-way event stream over HTTP
DirectionUpdates from server; control requests from clientBoth ways on one connectionServer to client only
Runs overWebSocket, or HTTP streaming or polling when blockedIts own frames after an HTTP UpgradeA plain HTTP response that stays open
Message modelSubscribe to items and fields; the mode decides deliveryWhatever you defineNamed text events with optional ids
ReconnectBuilt in, with session recoveryYou write itBuilt into EventSource; resumes with Last-Event-ID
Slow clientsServer throttles per client; MERGE sends only the latest valueYou handle backpressureYou handle it; sending full snapshots makes it simple
Binary dataNo, text protocolYesNo, UTF-8 text
ProxiesFalls back automatically when WebSockets are blockedSome proxies and firewalls block the UpgradePasses through, but proxy buffering must be turned off
BrowserVendor JavaScript clientNative WebSocket APINative EventSource; it can't set custom headers
Cost to runA dedicated, licensed serverA stateful connection per clientAn open HTTP response per client
SuitsFanning market data out to many clients with entitlements and throttlingInteractive two-way apps: order entry, chat, collaborationDashboards, notifications, job progress
In NexusConsumed: IG's price feed into the capture daemonConsumed from six exchanges; served by the RFQ simulatorServed: the trader's live quotes panel

In the code

Lightstreamer client

nexus-trader/src/nexus_trader/capture/stream.py::MarketStreamer

  • lightstreamer-client-lib 2.2.2. No transport is forced, so the client negotiates and lands on WS-STREAMING.
  • MERGE subscriptions in chunks of at most 20 items. IG once refused a 35-item subscription while the connection still reported healthy.
  • Error 21 triggers a one-time switch between the PRICE and MARKET subscription schemes.
  • Ticks are stamped with our own UTC receipt time; IG's update time has no date.
  • A second watchdog checks bars are being written, after the stream showed CONNECTED for about 11 hours while writing nothing.

nexus-trader/requirements-lock.txt

  • The client runs on aiohttp. A parser CVE there mattered because the feed parses IG traffic all day, so the patched version is pinned ahead of the default.

WebSocket client and server

nexus-arb/connectors/base_connector.py::BaseConnector._watch_symbol

  • await exchange.watch_order_book(symbol) in a loop, one task per symbol, ccxt 4.5.
  • Exponential backoff with jitter on errors, and a separate base delay for HTTP 429 rate limits.
  • Latency is receipt time minus exchange time, recorded per connector.

nexus-rfq-sim/app/routers/stream.py::ws_prices, ws_book

  • FastAPI @router.websocket: accept, then send_json a snapshot every second.
  • Exits cleanly on WebSocketDisconnect and closes on cancellation.
  • Push only; execution stays on the REST path.

SSE server and browser

nexus-trader/src/nexus_trader/app/routers/trader.py::quotes_stream

  • StreamingResponse with text/event-stream. Sends event: quotes, or event: error while keeping the stream open.
  • Interval is a query parameter, 250 ms to 10 s, default 1 s.
  • X-Accel-Buffering: no and Cache-Control: no-cache, no-transform, so nginx passes events through as they are written.

nexus-data-web/src/pages/Trader.tsx::LiveQuotesPanel

  • First paint from a snapshot GET, then new EventSource(url).
  • Flashes rows whose mid changed; closes the stream on unmount.
  • Every event is a full snapshot, so a reconnect needs no replay.

What I would change

Sources: each repository's main branch on 2026-09-29; the capture status strings in nexus-trader tests and day ledger; RFC 6455 for WebSockets, the HTML standard for EventSource, and Lightstreamer's documentation for TLCP, subscription modes and transport fallback.