はじめに (対象読者・この記事でわかること)
この記事は、JavaScriptとNode.jsの基本的な知識がある開発者を対象にしています。特に金融データや株価情報に興味があり、リアルタイムで日本の株価1分足データを取得・処理する方法を学びたい方に最適です。
この記事を読むことで、Node.jsを使った株価データ取得の基本的な実装方法がわかります。具体的には、公開されている金融データAPIを利用したデータ取得、WebSocketを活用したリアルタイムデータの受信処理、そして取得したデータを保存・可視化するまでの一連のフローをマスターできます。また、実際の開発で発生しがちな問題点とその解決策についても理解を深められるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- JavaScriptの基本的な知識(ES6以降の構文を含む)
- Node.jsとnpmの基本的な使い方
- 非同期処理(Promiseやasync/await)の理解
- HTTPリクエストとWebSocketの基本的な概念
- コマンドライン操作の基礎
日本株価データ取得の背景と選択肢
日本の株価データをリアルタイムで取得するには、いくつかの方法が考えられます。代表的な選択肢として、以下の3つの方法が挙げられます。
-
証券会社のAPIを利用する方法 - 自分が口座を持っている証券会社のAPIを利用する - 個人利用の場合、制限がかかったり、利用条件が厳しかったりすることがある
-
金融データ提供会社のAPIを利用する方法 - SBI証券や楽天証券などが提供するAPIを利用する - 有料の場合が多いが、高品質なデータが提供される
-
無料のオープンデータAPIを利用する方法 - 日本取引所グループ(JPX)が提供するAPI - 他にも様々な無料・有料のAPIが存在する - 無料の場合、制限(リクエスト回数、データ量など)があることが多い
本記事では、特に開発者にとってアクセスしやすく、比較的高頻度のデータ取得が可能な「無料のオープンデータAPI」を中心に解説します。具体的には、日本取引所グループ(JPX)が提供するAPIと、サードパーティーが提供するWebSocket APIを組み合わせた方法を取り上げます。
Node.jsを使った株価データ取得の具体的な実装方法
それでは、実際にNode.jsを使って日本の株価1分足データをリアルタイムで取得する実装方法をステップバイステップで解説していきます。
ステップ1: 開発環境の準備
まずは、必要な開発環境を整えます。以下の手順で進めていきましょう。
-
Node.jsのインストール - 公式サイトから最新のLTS版をインストール - バージョン管理ツール(nodenvやnvm)を使用する方法もおすすめ
-
プロジェクトのセットアップ ```bash # プロジェクト用のディレクトリを作成 mkdir stock-price-monitor cd stock-price-monitor
# npmでプロジェクトを初期化 npm init -y
# 必要なパッケージをインストール npm install axios ws moment ```
-
.envファイルの作成
bash touch .env -
.envファイルにAPIキーなどを記載
API_KEY=your_api_key_here
上記のコマンドで、axios(HTTPリクエスト用)、ws(WebSocketクライアント用)、moment(日時処理用)のライブラリをインストールしました。これらのライブラリを使って、株価データの取得と処理を実装していきます。
ステップ2: 株価取得APIの選定と実装
次に、株価データを取得するためのAPIを実装します。ここでは、日本取引所グループ(JPX)が提供するAPIを例に取り上げます。
まず、APIクライアント用のファイルを作成します。
Bashtouch apiClient.js
次に、apiClient.jsに以下のコードを実装します。
Javascriptrequire('dotenv').config(); const axios = require('axios'); // APIの基本設定 const API_BASE_URL = 'https://api.jpx-japanese-markets.jp'; const API_KEY = process.env.API_KEY; // リクエストヘッダーの設定 const headers = { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' }; // 株価データを取得する関数 async function getStockPriceData(stockCode, date) { try { const response = await axios.get(`${API_BASE_URL}/v1/markets/daily-prices`, { headers: headers, params: { securitiesCode: stockCode, date: date } }); return response.data; } catch (error) { console.error('データ取得エラー:', error.response ? error.response.data : error.message); throw error; } } module.exports = { getStockPriceData };
上記のコードでは、axiosを使ってJPXのAPIにリクエストを送信し、指定した銘柄と日付の株価データを取得する関数を実装しています。環境変数からAPIキーを読み込むことで、セキュリティを確保しています。
次に、このAPIクライアントを使って実際にデータを取得するスクリプトを作成します。
Bashtouch fetchStockData.js
fetchStockData.jsに以下のコードを実装します。
Javascriptconst { getStockPriceData } = require('./apiClient'); const moment = require('moment'); // 取得したい銘柄コードと日付を設定 const stockCode = '7203'; // トヨタ自動車 const date = moment().format('YYYYMMDD'); // 今日の日付 // 株価データを取得 getStockPriceData(stockCode, date) .then(data => { console.log('取得した株価データ:', data); }) .catch(error => { console.error('エラーが発生しました:', error); });
このスクリプトを実行すると、指定した銘柄の当日の株価データが取得できます。
Bashnode fetchStockData.js
ステップ3: WebSocketを使ったリアルタイムデータ取得
次に、WebSocketを使ってリアルタイムの株価データを取得する方法を実装します。ここでは、サードパーティーが提供するWebSocketサービスを利用します。
まず、WebSocketクライアント用のファイルを作成します。
Bashtouch websocketClient.js
websocketClient.jsに以下のコードを実装します。
Javascriptconst WebSocket = require('ws'); const moment = require('moment'); const { insertStockData } = require('./database'); // WebSocketサーバーのURL const WEBSOCKET_URL = 'wss://api.example.com/realtime-stock'; // WebSocketクライアントの作成 const ws = new WebSocket(WEBSOCKET_URL); // 接続が確立された時の処理 ws.on('open', function open() { console.log('WebSocket接続が確立されました'); // 購読したい銘柄コードを指定してリクエストを送信 const subscribeMessage = { action: 'subscribe', symbols: ['7203', '9984', '4502'] // トヨタ自動車、ソフトバンク、日本ハム }; ws.send(JSON.stringify(subscribeMessage)); }); // メッセージを受信した時の処理 ws.on('message', function incoming(data) { try { const message = JSON.parse(data); // データが分足データの場合のみ処理 if (message.type === 'minute-data') { const timestamp = moment(message.timestamp).format('YYYY-MM-DD HH:mm:ss'); const stockCode = message.symbol; const price = message.price; const volume = message.volume; console.log(`[${timestamp}] ${stockCode}: ${price} 円出来高: ${volume}`); // データベースに保存 insertStockData(stockCode, timestamp, price, volume) .then(id => { console.log(`データが保存されました (ID: ${id})`); }) .catch(error => { console.error('データ保存エラー:', error); }); } } catch (error) { console.error('メッセージ処理エラー:', error); } }); // エラーが発生した時の処理 ws.on('error', function error(error) { console.error('WebSocketエラー:', error); }); // 接続が閉じられた時の処理 ws.on('close', function close() { console.log('WebSocket接続が閉じられました'); });
上記のコードでは、WebSocketサーバーに接続し、リアルタイムで送られてくる株価データを受信・処理する実装を行っています。接続が確立されたら、購読したい銘柄コードを指定してリクエストを送信し、受信したデータはコンソールに表示しています。
このスクリプトを実行すると、リアルタイムで株価データが表示されます。
Bashnode websocketClient.js
ステップ4: データの保存と可視化
取得した株価データを保存し、可視化する方法を実装します。ここでは、データベースとしてSQLiteを利用し、可視化にはChart.jsを使います。
まず、必要なパッケージをインストールします。
Bashnpm install sqlite3 chart.js express
次に、データベース操作用のファイルを作成します。
Bashtouch database.js
database.jsに以下のコードを実装します。
Javascriptconst sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database('./stock_data.db'); // テーブルを作成 db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS stock_prices ( id INTEGER PRIMARY KEY AUTOINCREMENT, stock_code TEXT NOT NULL, timestamp DATETIME NOT NULL, price REAL NOT NULL, volume INTEGER NOT NULL )`); // インデックスの作成 db.run(`CREATE INDEX IF NOT EXISTS idx_stock_code ON stock_prices(stock_code)`); db.run(`CREATE INDEX IF NOT EXISTS idx_timestamp ON stock_prices(timestamp)`); db.run(`CREATE INDEX IF NOT EXISTS idx_stock_code_timestamp ON stock_prices(stock_code, timestamp)`); }); // 排他制御のためのミューテックス const mutex = require('async-mutex').Mutex; // データを挿入する関数(排他制御付き) async function insertStockData(stockCode, timestamp, price, volume) { const release = await mutex.acquire(); try { return new Promise((resolve, reject) => { const stmt = db.prepare(`INSERT INTO stock_prices (stock_code, timestamp, price, volume) VALUES (?, ?, ?, ?)`); stmt.run(stockCode, timestamp, price, volume, function(err) { if (err) { reject(err); } else { resolve(this.lastID); } }); stmt.finalize(); }); } finally { release(); } } module.exports = { insertStockData };
次に、Webサーバー用のファイルを作成します。
Bashtouch server.js
server.jsに以下のコードを実装します。
Javascriptconst express = require('express'); const path = require('path'); const { insertStockData } = require('./database'); const app = express(); const port = 3000; // 静的ファイルの提供 app.use(express.static('public')); // データベースに保存されたデータを取得するAPIエンドポイント app.get('/api/stock-data/:stockCode', async (req, res) => { try { const stockCode = req.params.stockCode; const db = require('./database').db; db.all(`SELECT * FROM stock_prices WHERE stock_code = ? ORDER BY timestamp DESC LIMIT 100`, [stockCode], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); } else { res.json(rows); } }); } catch (error) { res.status(500).json({ error: error.message }); } }); // サーバーの起動 app.listen(port, () => { console.log(`サーバーが http://localhost:${port} で起動しました`); });
次に、フロントエンド用のファイルを作成します。
Bashmkdir public touch public/index.html
public/index.htmlに以下のコードを実装します。
Html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>株価チャート</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; } .chart-container { position: relative; height: 400px; width: 100%; } .stock-selector { margin-bottom: 20px; } select { padding: 8px; font-size: 16px; } </style> </head> <body> <h1>リアルタイム株価チャート</h1> <div class="stock-selector"> <label for="stockCode">銘柄を選択:</label> <select id="stockCode"> <option value="7203">トヨタ自動車 (7203)</option> <option value="9984">ソフトバンクグループ (9984)</option> <option value="4502">日本ハム (4502)</option> </select> </div> <div class="chart-container"> <canvas id="stockChart"></canvas> </div> <script> document.addEventListener('DOMContentLoaded', function() { const ctx = document.getElementById('stockChart').getContext('2d'); const stockCodeSelect = document.getElementById('stockCode'); // グラフの初期化 const stockChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: '株価(円)', data: [], borderColor: 'rgb(75, 192, 192)', tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: false } } } }); // 銘柄が変更された時の処理 stockCodeSelect.addEventListener('change', function() { updateChart(this.value); }); // チャートを更新する関数 async function updateChart(stockCode) { try { const response = await fetch(`/api/stock-data/${stockCode}`); const data = await response.json(); // ラベルとデータを更新 stockChart.data.labels = data.map(item => { const date = new Date(item.timestamp); return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; }); stockChart.data.datasets[0].data = data.map(item => item.price); stockChart.data.datasets[0].label = `株価(円) - ${stockCode}`; // グラフを更新 stockChart.update(); } catch (error) { console.error('データ取得エラー:', error); } } // 初期データのロード updateChart(stockCodeSelect.value); }); </script> </body> </html>
これで、WebSocketクライアントで取得したリアルタイムデータがデータベースに保存され、Webブラウザでチャートとして表示されるようになりました。
Bash# WebSocketクライアントとWebサーバーを同時に実行 node websocketClient.js & node server.js
ハマった点やエラー解決
実際にこのシステムを構築する際に、いくつかの問題点に遭遇しました。以下にその内容と解決策を記載します。
問題1: WebSocket接続が頻繁に切断される
現象: WebSocket接続が数分ごとに切断され、再接続するまでデータが取得できない。
原因: 多くのWebSocketサービスでは、アイドル状態が続くと接続を自動的に切断する設定になっています。また、ネットワークの不安定さやプロキシ/ファイアウォールの設定によっても接続が切断されることがあります。
解決策: 自動再接続ロジックを実装しました。
Javascript// websocketClient.jsの修正部分 // 再接続の試行回数と間隔 const MAX_RECONNECT_ATTEMPTS = 5; const RECONNECT_INTERVAL = 5000; // 5秒 let reconnectAttempts = 0; // 再接続関数 function reconnect() { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { console.log(`再接続を試行します (${reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`); reconnectAttempts++; setTimeout(() => { ws = new WebSocket(WEBSOCKET_URL); setupWebSocketHandlers(ws); }, RECONNECT_INTERVAL); } else { console.error('再接続の試行回数が上限に達しました'); } } // WebSocketハンドラのセットアップ関数 function setupWebSocketHandlers(wsConnection) { wsConnection.on('open', function open() { console.log('WebSocket接続が確立されました'); reconnectAttempts = 0; // 接続成功時にカウントをリセット // 購送リクエストを再送 const subscribeMessage = { action: 'subscribe', symbols: ['7203', '9984', '4502'] }; wsConnection.send(JSON.stringify(subscribeMessage)); }); wsConnection.on('message', function incoming(data) { try { const message = JSON.parse(data); // データが分足データの場合のみ処理 if (message.type === 'minute-data') { const timestamp = moment(message.timestamp).format('YYYY-MM-DD HH:mm:ss'); const stockCode = message.symbol; const price = message.price; const volume = message.volume; console.log(`[${timestamp}] ${stockCode}: ${price} 円出来高: ${volume}`); // データベースに保存 insertStockData(stockCode, timestamp, price, volume) .then(id => { console.log(`データが保存されました (ID: ${id})`); }) .catch(error => { console.error('データ保存エラー:', error); }); } } catch (error) { console.error('メッセージ処理エラー:', error); } }); wsConnection.on('error', function error(error) { console.error('WebSocketエラー:', error); }); wsConnection.on('close', function close() { console.log('WebSocket接続が閉じられました'); reconnect(); }); } // 初期セットアップ let ws = new WebSocket(WEBSOCKET_URL); setupWebSocketHandlers(ws);
上記のように、接続が切断された場合に自動的に再接続を試行するロジックを実装しました。これにより、接続が不安定な環境でもデータ取得を継続できるようになりました。
問題2: データベースへの同時書き込みで競合が発生する
現象: 複数の銘柄のデータを同時に取得しようとすると、SQLiteデータベースで「database is locked」エラーが頻発する。
原因: SQLiteはデフォルトで同時書き込みをサポートしておらず、書き込み中のデータベースに他のプロセスがアクセスしようとするとロックがかかります。
解決策: 接続プールと排他制御を実装しました。
Javascript// database.jsの修正部分 const sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database('./stock_data.db'); // テーブル作成(前回と同じ) db.serialize(() => { // (前回と同じテーブル作成処理) }); // 排他制御のためのミューテックス const mutex = require('async-mutex').Mutex; // データを挿入する関数(排他制御付き) async function insertStockData(stockCode, timestamp, price, volume) { const release = await mutex.acquire(); try { return new Promise((resolve, reject) => { const stmt = db.prepare(`INSERT INTO stock_prices (stock_code, timestamp, price, volume) VALUES (?, ?, ?, ?)`); stmt.run(stockCode, timestamp, price, volume, function(err) { if (err) { reject(err); } else { resolve(this.lastID); } }); stmt.finalize(); }); } finally { release(); } } module.exports = { insertStockData };
また、SQLiteの設定を変更して同時書き込みをサポートするようにも対応しました。
Javascript// database.jsの先頭に追加 db.configure('busyTimeout', 5000); // 5秒間ロック待機
これにより、データベースへの同時書き込み時に競合が発生する問題を解決し、パフォーマンスも向上しました。
問題3: データ量が増加した際のパフォーマンス低下
現象: 運用を続けていると、データベースのサイズが増加し、チャートの描画が遅くなる。
原因: SQLiteデータベースが肥大化し、検索処理が遅くなっている。また、古いデータを削除せずに保持し続けているため、メモリ使用量も増加している。
解決策: データの定期的なクリーンアップとインデックスの最適化を行いました。
Javascript// database.jsに追加機能 // 古いデータを削除する関数 function cleanupOldData(daysToKeep = 30) { return new Promise((resolve, reject) => { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); db.run(`DELETE FROM stock_prices WHERE timestamp < ?`, [cutoffDate.toISOString()], function(err) { if (err) { reject(err); } else { console.log(`古いデータを削除しました: ${this.changes}件`); resolve(this.changes); } }); }); } // データベースを最適化する関数 function optimizeDatabase() { return new Promise((resolve, reject) => { db.run('VACUUM', (err) => { if (err) { reject(err); } else { console.log('データベースを最適化しました'); resolve(); } }); }); } module.exports = { insertStockData, cleanupOldData, optimizeDatabase };
また、定期的にクリーンアップ処理を実行するスケジューラーも追加しました。
Bashtouch scheduler.js
Javascriptconst { cleanupOldData, optimizeDatabase } = require('./database'); // 毎日深夜0時に実行するスケジュール function scheduleCleanup() { // 毎日深夜0時に実行 const runCleanup = () => { const now = new Date(); const nextRun = new Date(); nextRun.setDate(now.getDate() + 1); nextRun.setHours(0, 0, 0, 0); const msToNextRun = nextRun - now; console.log(`次のクリーンアップまで: ${msToNextRun / 1000 / 60 / 60}時間`); setTimeout(() => { console.log('データクリーンアップを開始します'); cleanupOldData() .then(() => optimizeDatabase()) .then(() => { console.log('データクリーンアップが完了しました'); scheduleCleanup(); // 次のスケジュールを設定 }) .catch(error => { console.error('クリーンアップ中にエラーが発生しました:', error); scheduleCleanup(); // エラーでも次のスケジュールを設定 }); }, msToNextRun); }; runCleanup(); } // スケジュールを開始 scheduleCleanup();
これらの対策により、データ量が増加してもシステムのパフォーマンスを維持できるようになりました。
まとめ
本記事では、Node.jsとWebSocket APIを活用し、日本の株価1分足データをリアルタイムで取得する方法を解説しました。
- 要点1: 日本取引所グループ(JPX)が提供するAPIとWebSocketを組み合わせることで、リアルタイムの株価データを効率的に取得できる
- 要点2: WebSocket接続が不安定な環境でもデータ取得を継続するための自動再接続ロジックを実装
- 要点3: SQLiteデータベースを活用して取得したデータを保存し、Chart.jsで可視化する方法を紹介
- 要点4: データ量が増加した際のパフォーマンス問題を解決するための最適化策を提案
この記事を通して、読者がリアルタイムの金融データを取得・処理するための実践的なスキルを習得できたことでしょう。また、実際の開発で遭遇する問題とその解決策についても理解を深められたはずです。
今後は、このシステムに以下のような機能を追加することで、さらに価値の高いアプリケーションに発展させていく予定です。
- 保存したデータをCSVやExcel形式でエクスポートする機能
- 複数の銘柄を同時に比較分析できる機能
- テクニカル指標(移動平均やRSIなど)の計算機能
- 価格変動に応じて通知を送る機能
参考資料
本記事を作成するにあたり、以下の資料を参考にしました。