Skip to content
EliteChart

Realtime over WebSocket

Stream live bar updates from a WebSocket into ChartForge — reconnection, multi-symbol fan-out, message shape.

The most common production setup — REST for history, WebSocket for live ticks. ChartForge calls your subscribe(req, onBar) once per (symbol, resolution) pair; you fan that out to a single shared WebSocket.

Quick example

code
import type { Bar, Datafeed, GetBarsRequest, SubscribeRequest } from '@elitechart/elitechart';
import { asPrice, asTimestampMs, asVolume } from '@elitechart/elitechart';

class WsDatafeed implements Datafeed {
  private ws: WebSocket | null = null;
  private subs = new Map<string, (bar: Bar) => void>();

  private ensureWs(): WebSocket {
    if (this.ws !== null && this.ws.readyState === WebSocket.OPEN) return this.ws;
    const ws = new WebSocket('wss://example.com/stream');
    ws.onmessage = (e) => {
      const m = JSON.parse(e.data) as { symbol: string; tf: string; t: number; o: number; h: number; l: number; c: number; v: number };
      const cb = this.subs.get(`${m.symbol}|${m.tf}`);
      if (cb !== undefined) {
        cb({
          time: asTimestampMs(m.t), open: asPrice(m.o), high: asPrice(m.h),
          low: asPrice(m.l), close: asPrice(m.c), volume: asVolume(m.v),
        });
      }
    };
    ws.onclose = () => { this.ws = null; setTimeout(() => this.ensureWs(), 1000); };
    this.ws = ws;
    return ws;
  }

  async getBars(req: GetBarsRequest): Promise<ReadonlyArray<Bar>> {
    const r = await fetch(`/api/bars?symbol=${req.symbol}&tf=${req.resolution}&from=${req.from}&to=${req.to}`);
    return (await r.json()) as ReadonlyArray<Bar>;
  }

  subscribe(req: SubscribeRequest, onBar: (bar: Bar) => void): () => void {
    const key = `${req.symbol}|${req.resolution}`;
    this.subs.set(key, onBar);
    const ws = this.ensureWs();
    const send = () => ws.send(JSON.stringify({ op: 'sub', symbol: req.symbol, tf: req.resolution }));
    if (ws.readyState === WebSocket.OPEN) send();
    else ws.addEventListener('open', send, { once: true });
    return () => {
      this.subs.delete(key);
      this.ws?.send(JSON.stringify({ op: 'unsub', symbol: req.symbol, tf: req.resolution }));
    };
  }
}

export const datafeed = new WsDatafeed();

How it works

A single WebSocket fans out messages to per-(symbol, tf) callbacks via the subs map. On disconnect, the class auto-reconnects and re-sends every active subscription on open.

Bars from your server should be the current bar — not just ticks. The chart merges the incoming bar with the buffer by time. So if the same bar's close keeps moving, you're flooding the chart with (t, o, h, l, c, v) tuples that all share t; the chart updates the in-progress bar.

Variations

Polling fallback if WebSocket is unavailable

See the polling pattern in datafeed-contract.

Backoff on repeated reconnects

code
let attempt = 0;
ws.onclose = () => {
  attempt += 1;
  setTimeout(() => this.ensureWs(), Math.min(1000 * 2 ** attempt, 30_000));
};

API

ConceptFrom
Datafeed.subscribe@elitechart/elitechart
Bar shapedatafeed-contract