Node.jsとSSEを用いた株価リアルタイムプッシュの実装

本記事では、Server-Sent Events (SSE) 技術を活用して株価情報をリアルタイムでフロントエンドにプッシュするシステムの構築方法について解説します。SSEは、HTTP長接続を利用してサーバーからクライアントへ一方的にデータを送信する標準技術であり、特に頻繁な更新が必要だが双方向通信は不要なユースケースに適しています。WebSocketと比較して、SSEは自動再接続機能、シンプルなプロトコル、そして株価のような単方向データフローに適している点が利点です。

サーバーサイド実装 (Node.js)

Expressフレームワークを使用してサーバーを構築します。SSE接続を確立するためには、レスポンスヘッダーに Content-Type: text/event-stream を設定することが不可欠です。ここでは、5つの架空の株式(例: AAPL, TSLA)の価格変動を1秒ごとにシミュレートします。


const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors()); // CORSを許可

const stocks = {
    'AAPL': { price: 170, change: 0 },
    'TSLA': { price: 250, change: 0 },
    'GOOG': { price: 1500, change: 0 },
    'AMZN': { price: 3000, change: 0 },
    'MSFT': { price: 280, change: 0 }
};

function updateStockPrices() {
    for (const symbol in stocks) {
        const stock = stocks[symbol];
        // -5% から +5% の範囲でランダムな変動を生成
        const fluctuation = (Math.random() - 0.5) * 0.10; // 10%の変動幅
        const priceChange = stock.price * fluctuation;
        stock.price += priceChange;
        stock.change = priceChange;
        // 価格がマイナスにならないように調整
        if (stock.price < 0) stock.price = 0.01;
    }
}

app.get('/stock-stream', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    const intervalId = setInterval(() => {
        updateStockPrices();
        const dataToSend = Object.keys(stocks).map(symbol => {
            const stock = stocks[symbol];
            const percentageChange = (stock.change / (stock.price - stock.change)) * 100;
            return {
                symbol: symbol,
                price: stock.price.toFixed(2),
                change: stock.change.toFixed(2),
                percentageChange: percentageChange.toFixed(2) + '%'
            };
        });
        res.write(`data: ${JSON.stringify(dataToSend)}\n\n`);
    }, 1000); // 1秒ごとに更新

    // クライアント切断時の処理
    req.on('close', () => {
        clearInterval(intervalId);
        res.end();
    });
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

フロントエンド実装

フロントエンドでは、EventSource オブジェクトを使用してサーバーのエンドポイントに接続し、リアルタイムで送信される株価データを受信します。Chart.js を使用して、受信したデータをリアルタイムに更新されるK線グラフとして表示します。

データ受信とグラフ更新

EventSource を利用してサーバーからのメッセージをリッスンし、 chart.update() メソッドを呼び出してグラフを再描画します。グラフのK線データは、古いデータを削除し、新しいデータを追加する形式で管理します。

値動きの視覚化

株価の変動に応じて、K線の色を赤(上昇)または緑(下落)で表示します。最新価格と変動率(パーセンテージ)も画面に表示します。


// HTML (index.html)
<!DOCTYPE html>
<html>
<head>
    <title>株価リアルタイム表示</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/moment"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment"></script>
    <style>
        .stock-info { margin-bottom: 20px; }
        .price { font-size: 1.2em; }
        .change-up { color: red; }
        .change-down { color: green; }
    </style>
</head>
<body>
    <h1>株価リアルタイムダッシュボード</h1>
    <div id="stockData"></div>
    <canvas id="stockChart" width="800" height="400"></canvas>

    <script src="app.js"></script>
</body>
</html>

// JavaScript (app.js)
const ctx = document.getElementById('stockChart').getContext('2d');
const stockDisplay = document.getElementById('stockData');
const MAX_DATA_POINTS = 50; // 表示する最大データ数

const stockChart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: 'AAPL Price',
            data: [],
            borderColor: 'rgb(75, 192, 192)',
            tension: 0.1,
            pointRadius: 1 // データポイントの半径を小さく
        }]
    },
    options: {
        scales: {
            x: {
                type: 'time',
                time: {
                    unit: 'second',
                    tooltipFormat: 'h:mm:ss a'
                },
                title: {
                    display: true,
                    text: '時刻'
                }
            },
            y: {
                title: {
                    display: true,
                    text: '価格'
                }
            }
        },
        animation: false, // アニメーションを無効化してパフォーマンス向上
        plugins: {
            legend: {
                display: false // 凡例を非表示
            }
        }
    }
});

const eventSource = new EventSource('http://localhost:3000/stock-stream'); // サーバーアドレスを適宜変更

eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    const now = new Date();

    // 最新の株価情報を表示
    let stockHtml = '';
    data.forEach(stock => {
        const changeClass = parseFloat(stock.change) >= 0 ? 'change-up' : 'change-down';
        stockHtml += `
            <div class="stock-info">
                <strong>${stock.symbol}:</strong>
                <span class="price">¥${stock.price}</span>
                <span class="${changeClass}"> (${stock.change} / ${stock.percentageChange})</span>
            </div>
        `;
    });
    stockDisplay.innerHTML = stockHtml;


    // グラフデータを更新
    const labels = stockChart.data.labels;
    const dataset = stockChart.data.datasets[0];

    // データポイントが最大数を超えたら古いものを削除
    if (labels.length >= MAX_DATA_POINTS) {
        labels.shift();
        dataset.data.shift();
    }

    // 新しいデータを追加
    labels.push(now);
    // Chart.jsのdatasetには数値または{x: time, y: value}形式のオブジェクトを入れる
    // ここでは単純な数値配列を使用
    // 複数の銘柄を扱う場合は、datasets配列を動的に管理する必要がある
    if (data.length > 0) {
         // 例としてAAPLの価格のみをプロット
        const aaplData = data.find(s => s.symbol === 'AAPL');
        if (aaplData) {
            dataset.data.push(parseFloat(aaplData.price));
        } else {
             // AAPLデータがない場合、nullを追加してグラフの連続性を保つ
             dataset.data.push(null);
        }
    } else {
         dataset.data.push(null); // データがない場合もnullを追加
    }

    stockChart.update();
};

eventSource.onopen = function() {
    console.log('Connection to server opened.');
};

eventSource.onerror = function() {
    console.error('EventSource failed.');
    eventSource.close();
};

パフォーマンス最適化

  • サーバーサイドでgzip圧縮ミドルウェアを有効にすると、送信データ量を削減できます。
  • フロントエンドでは、requestAnimationFrame を使用してDOM操作の頻度を最適化し、グラフの描画パフォーマンスを向上させます。
  • 大量の履歴データを扱う場合は、バイナリ形式(例: ArrayBuffer)でデータを送受信することで、さらに効率を高めることができます。

デプロイと体験

このシステムは、InsCode (快馬) プラットフォームのようなクラウド環境で簡単にデプロイできます。これらのプラットフォームはNode.js環境を事前に設定しており、必要な依存関係も管理されているため、ローカルでの環境構築の手間が省けます。デプロイ後すぐにアクセス可能なURLが生成されるため、デモンストレーションや共有が容易になります。

アイデアの検証を迅速に行いたい開発者にとって、このようなすぐに利用できる環境は非常に役立ちます。コーディングからオンラインデプロイまで、わずか数時間で完了します。特に、デプロイメントはプロジェクト名を入力してボタンをクリックするだけで完了しました。

タグ: Node.js express SSE javascript Chart.js

8月19日 05:37 投稿