DEV Community

Cover image for JavaScript 前端集成贵金属 K 线图:10 分钟快速实现
San Si wu
San Si wu

Posted on

JavaScript 前端集成贵金属 K 线图:10 分钟快速实现

金融数据可视化中的 K 线图(Candlestick Chart)是前端开发中一个典型难点——既要高效渲染大量历史数据,又要支持实时推送,还要保证缩放、拖拽足够流畅。市面上很多图表库能画折线图、柱状图,但真正为金融场景优化的并不多。

本文将带你10 分钟内跑通一个可交互的贵金属 K 线看板,并给出接入真实行情 API。读完你将掌握:

  • 最适合 K 线图的前端库选型对比
  • 用 Lightweight Charts 快速渲染第一根 K 线
  • 通过 HTTP 拉取历史数据 + WebSocket 接收实时更新
  • 成交量副图、安全代理等工程要点

一、选型对比:哪个图表库更适合 K 线?

定位 K 线原生支持 体积 (gzip) 性能特点 授权
Lightweight Charts 专业金融图表 ~12 KB 金融实时优化,极轻量 需注明版权
ECharts 通用可视化 ~80-130 KB Canvas 流畅,全能 Apache 2.0
KLineChart 轻量级金融图表 ~40 KB 5 万条数据 37ms 渲染 开源免费

选型建议

  • 追求极轻量 + 金融专业性 → Lightweight Charts(本文使用)
  • 需要丰富图表类型 + 中文文档 → ECharts
  • 海量数据 + 极致渲染性能 → KLineChart

二、Lightweight Charts 极速上手(模拟数据版)

2.1 安装

npm install lightweight-charts
Enter fullscreen mode Exit fullscreen mode

或直接使用 CDN:

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

2.2 最简代码:5 根 K 线

<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", // 阳线(涨)
    downColor: "#ef5350", // 阴线(跌)
    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

打开页面即可看到带十字光标、可缩放拖拽的 K 线图。

三、完整可运行示例(模拟实时推送)

保存为 index.html 打开,即可看到一个黄金 1 分钟 K 线图,并支持“添加一根新 K 线”来模拟实时推送:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>黄金 K 线图 · 模拟实时行情</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>黄金 1 分钟 K 线图</h1>
          <div class="update-badge" id="update-time">模拟数据演示</div>
        </div>
        <div class="price-info">
          最新价: <span id="latest-price">0.00</span>
        </div>
      </div>
      <div id="kline-chart" class="chart-container"></div>
      <div class="controls">
        <button id="reset-view">重置视图</button>
        <button id="add-data">添加 1 根模拟 K 线</button>
      </div>
    </div>

    <script>
      (function () {
        // 生成 30 根模拟 K 线
        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 = `最后更新: ${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

四、接入真实行情 API

真实场景中需要对接行情数据源。下面以支持 REST + WebSocket 的行情 iTick API 为例,展示接入方法。你可以替换成任何提供类似接口的服务(如自己的后端、第三方数据商等)。

4.1 历史 K 线:REST API 拉取

iTick 的行情 API 提供了期货历史 K 线接口:

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

返回格式示例:

{
  "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

前端调用时,建议通过自己的后端代理,避免暴露 API 密钥。后端示例(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

前端调用并转换为 Lightweight Charts 所需格式:

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), // 毫秒时间戳 → 秒
        open: item.o,
        high: item.h,
        low: item.l,
        close: item.c,
      }));
      candlestickSeries.setData(klineData);
      chart.timeScale().fitContent();
    }
  });
Enter fullscreen mode Exit fullscreen mode

注意:不同的 API 返回的时间戳单位可能不同(毫秒/秒),需要根据实际情况转换。

4.2 实时推送:WebSocket 订阅 K 线更新

同样以后端代理 WebSocket 为例,或者直接在前端连接行情网关(需确保 API Key 不暴露)。以下直接展示前端连接某行情 WebSocket 的示例(假设该服务允许前端直接使用临时 token):

let ws = null;

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

  ws.onopen = () => {
    // 发送认证(具体格式依 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("连接成功");
    }

    // 认证成功后订阅 K 线频道
    if (msg.code === 1 && msg.resAc === "auth") {
      ws.send(
        JSON.stringify({
          ac: "subscribe",
          params: "GC$US,SI$US", // 格式为:{symbol}${region}
          types: "kline@1", // 可选:depth, quote, tick, kline@1
        })
      );
    }

    // 处理 K 线推送
    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

安全提醒:任何 API 密钥、token 都不应直接写在前端代码中。上述示例仅为演示逻辑,实际生产环境应通过后端代理或短期令牌方式保护敏感信息。

五、进阶功能:成交量副图与性能优化

5.1 添加成交量柱状图

Lightweight Charts 支持多系列叠加,可轻松添加成交量副图:

const volumeSeries = chart.addSeries(LightweightCharts.HistogramSeries, {
  color: "#26a69a",
  priceFormat: { type: "volume" },
  priceScaleId: "", // 独立右侧轴
});

// 假设从 API 获取的数据中包含成交量 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 技术指标(MACD / RSI)

Lightweight Charts 本身不提供指标计算,可以:

  • 使用 ta.jstulind 在前端计算。
  • 将计算结果作为 LineSeries 叠加到图表。
const macdLine = chart.addSeries(LightweightCharts.LineSeries, {
  color: "#FF9800",
});
macdLine.setData(macdValues);
Enter fullscreen mode Exit fullscreen mode

若需要完整的技术指标内置支持,可考虑使用 react-stockchartsKLineChart

5.3 性能优化建议

  • 数据更新:大量历史数据使用 setData,单根实时 K 线使用 update
  • 视口管理:不需要一次性渲染几十万根 K 线,利用库自带的数据采样。
  • 窗口 resize:绑定 resize 事件并调用 chart.applyOptions({ width })

六、总结

本文介绍了如何用 Lightweight Charts 在 10 分钟内搭建一个贵金属 K 线图前端看板,并给出了接入真实行情数据的通用方法(REST 拉取历史 + WebSocket 实时更新)。关键技术点包括:

  • 对比主流 K 线图表库的优缺点,根据场景选型。
  • Lightweight Charts 核心用法:createChartCandlestickSeriessetData / update
  • 模拟数据快速原型,以及如何替换为真实 API。
  • 成交量副图、指标叠加等扩展思路。
  • 安全警示:API 密钥绝不能写在前端,必须后端代理。

参考文档:https://docs.itick.org/websocket/future
GitHub:https://github.com/itick-org/

Top comments (0)