DEV Community

Cover image for How to Build a Real-Time Gold & Silver Candlestick Chart in JavaScript (10-Minute Guide)
San Si wu
San Si wu

Posted on

How to Build a Real-Time Gold & Silver Candlestick Chart in JavaScript (10-Minute Guide)

Candlestick charts in financial data visualization represent a typical challenge in frontend development—they must efficiently render large amounts of historical data, support real-time updates, and ensure smooth zooming and panning. While many charting libraries can draw line charts and bar charts, few are truly optimized for financial scenarios.

This article will guide you through building an interactive precious metals candlestick dashboard in under 10 minutes, complete with integration to real market data APIs. By the end, you'll master:

  • Comparison of frontend libraries best suited for candlestick charts
  • Quickly rendering your first candlestick using Lightweight Charts
  • Fetching historical data via HTTP + receiving real-time updates via WebSocket
  • Engineering essentials like volume sub-charts and secure proxy patterns

1. Library Comparison: Which Charting Library is Best for Candlesticks?

Library Positioning Native Candlestick Support Size (gzip) Performance Characteristics License
Lightweight Charts Professional Financial Charts ~12 KB Optimized for financial real-time, ultra-lightweight Attribution required
ECharts General Visualization ~80-130 KB Smooth Canvas rendering, versatile Apache 2.0
KLineChart Lightweight Financial Charts ~40 KB Renders 50k data points in 37ms Open Source & Free

Selection Recommendations:

  • Ultra-lightweight + financial professionalism → Lightweight Charts (used in this article)
  • Need rich chart types + Chinese documentation → ECharts
  • Massive data + ultimate rendering performance → KLineChart

2. Getting Started with Lightweight Charts (Mock Data Version)

2.1 Installation

npm install lightweight-charts
Enter fullscreen mode Exit fullscreen mode

Or use CDN directly:

<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
Enter fullscreen mode Exit fullscreen mode

2.2 Minimal Code: 5 Candlesticks

<div id="chart" style="width: 800px; height: 500px;"></div>

<script>
  const { createChart, CandlestickSeries } = LightweightCharts;

  const chart = createChart(document.getElementById("chart"), {
    width: 800,
    height: 500,
    layout: { backgroundColor: "#ffffff", textColor: "#333" },
  });

  const candlestickSeries = chart.addSeries(CandlestickSeries, {
    upColor: "#26a69a", // Bullish candle (up)
    downColor: "#ef5350", // Bearish candle (down)
    borderVisible: false,
  });

  candlestickSeries.setData([
    { time: "2024-01-01", open: 2040, high: 2060, low: 2020, close: 2055 },
    { time: "2024-01-02", open: 2055, high: 2080, low: 2045, close: 2070 },
    { time: "2024-01-03", open: 2070, high: 2090, low: 2060, close: 2085 },
    { time: "2024-01-04", open: 2085, high: 2100, low: 2075, close: 2080 },
    { time: "2024-01-05", open: 2080, high: 2095, low: 2065, close: 2075 },
  ]);
</script>
Enter fullscreen mode Exit fullscreen mode

Open the page to see a candlestick chart with crosshair cursor, zoomable and draggable.

3. Complete Runnable Example (Simulated Real-Time Updates)

Save as index.html and open it to see a gold 1-minute candlestick chart with support for "Add a New Candlestick" to simulate real-time updates:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Gold Candlestick Chart · Simulated Real-Time Market Data</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        background: #f5f7fa;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        padding: 20px;
      }
      .card {
        background: white;
        border-radius: 16px;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
        padding: 20px;
        width: 100%;
        max-width: 1100px;
      }
      .header {
        display: flex;
        justify-content: space-between;
        margin-bottom: 16px;
        align-items: baseline;
      }
      h1 {
        font-size: 1.5rem;
      }
      .price-info {
        font-size: 1.25rem;
        font-weight: 600;
        color: #26a69a;
      }
      .chart-container {
        width: 100%;
        height: 500px;
      }
      .controls {
        margin-top: 16px;
        display: flex;
        gap: 12px;
        justify-content: flex-end;
      }
      button {
        background: #f1f5f9;
        border: none;
        padding: 8px 16px;
        border-radius: 8px;
        cursor: pointer;
      }
      .update-badge {
        font-size: 0.75rem;
        color: #94a3b8;
      }
    </style>
    <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
  </head>
  <body>
    <div class="card">
      <div class="header">
        <div>
          <h1>Gold 1-Minute Candlestick Chart</h1>
          <div class="update-badge" id="update-time">Mock Data Demo</div>
        </div>
        <div class="price-info">
          Latest Price: <span id="latest-price">0.00</span>
        </div>
      </div>
      <div id="kline-chart" class="chart-container"></div>
      <div class="controls">
        <button id="reset-view">Reset View</button>
        <button id="add-data">Add 1 Simulated Candlestick</button>
      </div>
    </div>

    <script>
      (function () {
        // Generate 30 simulated candlesticks
        const generateMockData = () => {
          let basePrice = 2040;
          const data = [];
          const now = new Date();
          for (let i = 30; i >= 1; i--) {
            const minute = new Date(now.getTime() - i * 60 * 1000);
            const timeStr = minute.toISOString().slice(0, 19).replace("T", " ");
            const variation = (Math.random() - 0.5) * 4;
            const open = basePrice;
            const close = +(open + variation).toFixed(2);
            const high = Math.max(open, close) + Math.random() * 2;
            const low = Math.min(open, close) - Math.random() * 2;
            data.push({
              time: timeStr,
              open: +open.toFixed(2),
              high: +high.toFixed(2),
              low: +low.toFixed(2),
              close,
            });
            basePrice = close;
          }
          return data;
        };

        let mockData = generateMockData();
        const chart = LightweightCharts.createChart(
          document.getElementById("kline-chart"),
          {
            width: document.getElementById("kline-chart").clientWidth,
            height: 500,
            layout: { backgroundColor: "#ffffff", textColor: "#1e293b" },
            grid: {
              vertLines: { color: "#e2e8f0" },
              horzLines: { color: "#e2e8f0" },
            },
            timeScale: { timeVisible: true, secondsVisible: false },
          }
        );
        const candlestickSeries = chart.addSeries(
          LightweightCharts.CandlestickSeries,
          {
            upColor: "#26a69a",
            downColor: "#ef5350",
            borderVisible: false,
          }
        );
        candlestickSeries.setData(mockData);
        chart.timeScale().fitContent();

        const updateLatestPrice = () => {
          if (mockData.length) {
            const latest = mockData[mockData.length - 1];
            document.getElementById("latest-price").innerText =
              latest.close.toFixed(2);
            document.getElementById(
              "update-time"
            ).innerHTML = `Last Update: ${latest.time}`;
          }
        };
        updateLatestPrice();

        const addNewCandle = () => {
          const last = mockData[mockData.length - 1];
          const now = new Date();
          const timeStr = now.toISOString().slice(0, 19).replace("T", " ");
          const variation = (Math.random() - 0.5) * 3;
          const open = last.close;
          const close = +(open + variation).toFixed(2);
          const high = Math.max(open, close) + Math.random() * 2;
          const low = Math.min(open, close) - Math.random() * 2;
          const newCandle = {
            time: timeStr,
            open: +open.toFixed(2),
            high: +high.toFixed(2),
            low: +low.toFixed(2),
            close,
          };
          mockData.push(newCandle);
          candlestickSeries.update(newCandle);
          updateLatestPrice();
          chart.timeScale().scrollToRealTime();
        };

        document
          .getElementById("add-data")
          .addEventListener("click", addNewCandle);
        document
          .getElementById("reset-view")
          .addEventListener("click", () => chart.timeScale().fitContent());
        window.addEventListener("resize", () =>
          chart.applyOptions({
            width: document.getElementById("kline-chart").clientWidth,
          })
        );
      })();
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

4. Integrating with Real Market Data APIs

In real-world scenarios, you need to connect to market data sources. Below, we use the iTick API that supports both REST + WebSocket as an example to demonstrate integration methods. You can replace it with any service providing similar interfaces (such as your own backend or third-party data providers).

4.1 Historical Candlesticks: Fetching via REST API

iTick's market data API provides futures historical candlestick endpoints:

GET https://api.itick.org/future/kline?symbol=GC&region=US&kType=1&limit=100

Response format example:

{
  "code": 0,
  "data": [
    {
      "t": 1704067200000,
      "o": 2040.5,
      "h": 2060.2,
      "l": 2020.8,
      "c": 2055.3,
      "v": 12500
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

When calling from the frontend, it's recommended to proxy through your own backend to avoid exposing API keys. Backend example (Node.js + Express):

app.get("/api/kline", async (req, res) => {
  const { symbol, region, kType, limit } = req.query;
  const response = await fetch(
    `https://api.itick.org/future/kline?symbol=${symbol}&region=${region}&kType=${kType}&limit=${limit}`,
    { headers: { token: process.env.API_KEY } }
  );
  const data = await response.json();
  res.json(data);
});
Enter fullscreen mode Exit fullscreen mode

Frontend call and conversion to Lightweight Charts required format:

fetch("/api/kline?symbol=GC&region=US&kType=1&limit=100")
  .then((res) => res.json())
  .then((data) => {
    if (data.code === 0 && data.data) {
      const klineData = data.data.map((item) => ({
        time: Math.floor(item.t / 1000), // Millisecond timestamp → seconds
        open: item.o,
        high: item.h,
        low: item.l,
        close: item.c,
      }));
      candlestickSeries.setData(klineData);
      chart.timeScale().fitContent();
    }
  });
Enter fullscreen mode Exit fullscreen mode

Note: Different APIs may return timestamps in different units (milliseconds/seconds), so conversion is needed based on actual circumstances.

4.2 Real-Time Updates: WebSocket Subscription for Candlestick Updates

Similarly, using backend proxy for WebSocket as an example, or connecting directly to the market gateway from the frontend (ensuring API Key is not exposed). Below shows a direct frontend connection example to a market WebSocket (assuming the service allows frontend to use temporary tokens directly):

let ws = null;

function connectWebSocket() {
  ws = new WebSocket("wss://api.itick.org/future");

  ws.onopen = () => {
    // Send authentication (specific format depends on API)
    ws.send(JSON.stringify({ action: "auth", token: "your_temp_token" }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.code === 1 && msg.msg === "Connected Successfully") {
      console.log("Connection successful");
    }

    // Subscribe to candlestick channel after successful authentication
    if (msg.code === 1 && msg.resAc === "auth") {
      ws.send(
        JSON.stringify({
          ac: "subscribe",
          params: "GC$US,SI$US", // Format: {symbol}${region}
          types: "kline@1", // Options: depth, quote, tick, kline@1
        })
      );
    }

    // Handle candlestick push
    if (msg.type === "kline@1") {
      const candle = msg.data;
      candlestickSeries.update({
        time: Math.floor(candle.t / 1000),
        open: candle.o,
        high: candle.h,
        low: candle.l,
        close: candle.c,
      });
    }
  };

  ws.onclose = () => setTimeout(() => connectWebSocket(), 3000);
  ws.onerror = (err) => console.error("WebSocket error", err);
}

connectWebSocket();
Enter fullscreen mode Exit fullscreen mode

Security Warning: API keys and tokens should never be written directly in frontend code. The above examples are for demonstration purposes only. In production environments, sensitive information should be protected through backend proxies or short-lived tokens.

5. Advanced Features: Volume Sub-Chart and Performance Optimization

5.1 Adding Volume Bar Chart

Lightweight Charts supports multiple series overlay, making it easy to add a volume sub-chart:

const volumeSeries = chart.addSeries(LightweightCharts.HistogramSeries, {
  color: "#26a69a",
  priceFormat: { type: "volume" },
  priceScaleId: "", // Independent right axis
});

// Assuming the data obtained from API includes volume v
volumeSeries.setData(
  apiData.map((item) => ({
    time: Math.floor(item.t / 1000),
    value: item.v,
    color: item.close >= item.open ? "#26a69a" : "#ef5350",
  }))
);
Enter fullscreen mode Exit fullscreen mode

5.2 Technical Indicators (MACD / RSI)

Lightweight Charts doesn't provide built-in indicator calculations, but you can:

  • Use ta.js or tulind for frontend calculations.
  • Overlay calculation results as LineSeries on the chart.
const macdLine = chart.addSeries(LightweightCharts.LineSeries, {
  color: "#FF9800",
});
macdLine.setData(macdValues);
Enter fullscreen mode Exit fullscreen mode

If you need comprehensive built-in technical indicator support, consider using react-stockcharts or KLineChart.

5.3 Performance Optimization Tips

  • Data Updates: Use setData for large amounts of historical data, and update for single real-time candlesticks.
  • Viewport Management: No need to render hundreds of thousands of candlesticks at once; leverage the library's built-in data sampling.
  • Window Resize: Bind resize event and call chart.applyOptions({ width }).

6. Summary

This article demonstrated how to build a precious metals candlestick chart frontend dashboard in 10 minutes using Lightweight Charts, and provided general methods for integrating real market data (REST for historical data + WebSocket for real-time updates). Key technical points include:

  • Comparing pros and cons of mainstream candlestick chart libraries and selecting based on scenarios.
  • Core usage of Lightweight Charts: createChart, CandlestickSeries, setData / update.
  • Rapid prototyping with mock data and how to replace with real APIs.
  • Extension ideas like volume sub-charts and indicator overlays.
  • Security warning: API keys must never be written in frontend code; backend proxy is mandatory.

Reference Documentation: https://docs.itick.org/websocket/future
GitHub: https://github.com/itick-org/

Top comments (0)