sifting/io
Dashboards & Frontend
6 min readSiftingIO Team

US stock market data API: build a live price chart with historical bars and real-time ticks

Build a live US stock price and chart page: backfill adjusted OHLCV history over REST, stream real-time ticks over WebSocket, and handle market sessions.

Building a symbol price page: live quotes and charts for XAUUSD, BTCUSD, and EURUSD

A US stock market data API gets its hardest workout on the most ordinary page in fintech: one ticker at the top, a live price beside it, and a candlestick chart underneath. Portfolio trackers need that page for every holding, research dashboards need it for every watchlist row, and the layout is never the hard part. The hard part is that the page joins two different kinds of data: historical OHLCV bars fetched over REST to draw the chart, and live ticks pushed over WebSocket to keep the newest candle moving. This post builds that data layer for US stocks, using AAPL, MSFT, and NVDA as neutral examples. The tickers illustrate the API, never a view on the companies.

The build has four steps: backfill history, render it, stream live updates into the current bar, and let session state decide what the page shows when nothing is trading.

Backfill history from the US stock market data API#

The chart needs two series. Daily bars cover the long view, one-minute bars cover today.

curl --compressed -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/hist/stocks/AAPL/bars?interval=1d"

curl --compressed -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/hist/stocks/AAPL/bars?interval=1m"

The --compressed flag matters: bar endpoints require gzip and return 406 gzip_required without it. The free tier covers development comfortably, with 10,000 REST calls a month and one month of bar history, no credit card.

The backfill must use the split- and dividend-adjusted series. NVDA split 10-for-1 in June 2024, and an unadjusted daily chart draws a 90 percent single-day cliff at the ex-date. Nothing close to that happened to anyone holding the stock; the cliff is bookkeeping. Adjusted bars rescale everything before the ex-date so the series reads as continuous. SiftingIO serves US stock bars in both forms and exposes the underlying adjustment factors (see /docs for the bar parameters), and the full argument for when each series is correct is in adjusted vs unadjusted stock prices. For a chart, adjusted is the right answer.

The live stream is the opposite. A tick is the price that just traded, and no feed rescales the present, so the stream is unadjusted by definition. That sounds like a seam problem between the two series, and mostly it isn't, because adjustment rebases the past and leaves the most recent bar untouched. Factors change only when a corporate action takes effect at its ex-date, which happens between sessions, never mid-session. The stitch rule falls out of that: refetch the backfill on page load and at each session start, build only the current bar from ticks, and the join is exact. Cache the backfill across an ex-date and the seam becomes very visible; that failure appears in the pitfalls below.

Render the candles with Lightweight Charts#

Lightweight Charts wants { time, open, high, low, close } with time in seconds, so the mapping from the bar response is short. Prices arrive as strings; cast before arithmetic.

import { createChart, CandlestickSeries } from "lightweight-charts";

const chart = createChart(document.getElementById("chart"), { height: 320 });
const candles = chart.addSeries(CandlestickSeries);

const res = await fetch(
  "https://api.sifting.io/v1/hist/stocks/NVDA/bars?interval=1m",
  { headers: { "X-API-Key": SIFTING_KEY } }
);
const { data } = await res.json();

candles.setData(data.map(b => ({
  time: b.t / 1000,
  open: Number(b.o), high: Number(b.h),
  low: Number(b.l), close: Number(b.c),
})));

Browser fetch negotiates gzip on its own, which is why no explicit header appears here.

Update the chart in real time over WebSocket#

US equities stream under product us, on the same socket that serves crypto and forex. One connection handles all three tickers.

const ws = new WebSocket(`wss://stream.sifting.io/ws/v1?key=${SIFTING_KEY}`);

ws.onopen = () => {
  ws.send(JSON.stringify({ op: "subscribe", product: "us",
    symbols: ["AAPL", "MSFT", "NVDA"] }));
  setInterval(() => ws.send(JSON.stringify({ op: "ping" })), 55_000);
};

const BAR_MS = 60_000;
let bar = null; // seed from the last REST bar

ws.onmessage = (ev) => {
  const msg = JSON.parse(ev.data);
  if (msg.f !== "tick" || msg.s !== "NVDA") return;
  const p = Number(msg.p);
  const bucket = Math.floor(msg.t / BAR_MS) * BAR_MS;
  if (bar & bar.t === bucket) {
    bar.h = Math.max(bar.h, p); bar.l = Math.min(bar.l, p); bar.c = p;
  } else {
    bar = { t: bucket, o: p, h: p, l: p, c: p };
  }
  candles.update({ time: bar.t / 1000, open: bar.o,
    high: bar.h, low: bar.l, close: bar.c });
};

Two server behaviors do real work here. On subscribe the stream first replays the last cached value for each symbol, so the page paints a price immediately instead of waiting for the next trade, even on a Sunday. And series.update() in Lightweight Charts amends the bar in place when the time matches the last bar and appends when it doesn't, so the tick-folding code above is the whole candle-building logic. The header price is the same msg.p, and the day change is that price against yesterday's adjusted close from the daily backfill. For the wider decision of when a page should stream and when polling is enough, see REST vs WebSocket for real-time market data.

Handle pre-market, after-hours, and the closed market#

US stocks trade in sessions, and the page has four states to represent: pre-market, regular hours, after-hours, closed. Don't infer the state from tick arrival. A silent stream is ambiguous between a closed market and a dropped connection, and that ambiguity is how a dashboard ends up showing a stale price under a live-looking chart. Ask instead:

curl -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/fnd/markets/us_equities/status"

The status endpoint returns whether the market is open right now, and /v1/fnd/markets/us_equities/hours returns the weekly schedule in venue-local time, enough to place "now" inside a session and label the badge. When the market is closed, show the last regular-session close with an explicit as-of timestamp (tick frames carry epoch milliseconds in t, so the label is a formatting call), stop the bar clock so empty candles don't accumulate, and relax the reconnect loop rather than hammering a stream with nothing to say. Holidays and half-days break naive weekday logic several times a year; the market hours API post covers that properly, including the holiday calendar endpoint, so the session logic here can stay small.

Common pitfalls#

The 406 that only appears in production. Browsers send Accept-Encoding: gzip on their own, so the backfill works all through development, then a server-side render or a cron warmer using a minimal HTTP client fails with 406 gzip_required. Set the header explicitly in any non-browser client, or use curl's --compressed.

A 10x cliff at the join. A page caches its adjusted daily backfill for a week to save quota, a ticker splits mid-week, and live ticks now arrive at a tenth of the cached scale, so the chart shows an overnight collapse that never happened. The cache is stale rather than wrong: adjustment factors changed at the ex-date and the cached series predates them. Refetching the backfill at every session start makes this impossible.

A connection that dies every 90 seconds. The server closes any connection idle for 90 seconds, and idle means frames from the client; a page that only listens gets disconnected on schedule. Send {"op":"ping"} at least once a minute. Watch for {"f":"error"} frames too: on the free tier a sixth symbol subscription returns max_subscriptions, and a handler that ignores error frames leaves that ticker looking permanently blank.

The whole page rests on two REST requests and one socket: adjusted history in, live ticks folded into the newest bar, session state deciding what renders. The free tier covers all of it during development. Start building free

Keep reading

Related posts