UNPKG

candlestick-chart-generator

Version:

A Node.js library for generating candlestick chart screenshots using financial data

178 lines (152 loc) 5.32 kB
const yahooFinance = require("yahoo-finance2").default; class DataFetcher { constructor() { yahooFinance.suppressNotices(["ripHistorical"]); } /** * Fetch historical OHLC data for a given symbol and interval * @param {string} symbol - The ticker symbol (e.g., 'AAPL', 'BTC-USD', 'EURUSD=X') * @param {string} interval - The time interval ('1m', '1h', '1d', etc.) * @param {string|Date} startDate - Start date for historical data * @param {string|Date} endDate - End date for historical data * @param {Object} [options] - Additional fetch options * @param {boolean} [options.includePrePost=false] - Include pre-market and after-hours data (stocks) * @returns {Promise<Array>} Array of OHLC data formatted for Lightweight Charts */ async fetchHistoricalData(symbol, interval, startDate, endDate, options = {}) { try { const yahooInterval = this._convertInterval(interval); const queryOptions = { period1: startDate ? new Date(startDate) : new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), period2: endDate ? new Date(endDate) : new Date(), interval: yahooInterval, includePrePost: options.includePrePost === true }; const result = await yahooFinance.chart(symbol, queryOptions); if (!result || !result.quotes || result.quotes.length === 0) { throw new Error(`No data found for symbol: ${symbol}`); } let transformedData = this._transformChartData(result.quotes); if (interval === "4h") { transformedData = this._aggregateTo4Hour(transformedData); } return transformedData; } catch (error) { throw new Error(`Failed to fetch data for ${symbol}: ${error.message}`); } } /** * Convert interval string to yahoo-finance2 format * @param {string} interval - Input interval (e.g., '1m', '1h', '1d') * @returns {string} Yahoo Finance interval format */ _convertInterval(interval) { const intervalMap = { "1m": "1m", "2m": "2m", "5m": "5m", "15m": "15m", "30m": "30m", "60m": "60m", "90m": "90m", "1h": "1h", "4h": "1h", "1d": "1d", "5d": "5d", "1wk": "1wk", "1mo": "1mo", "3mo": "3mo", }; const yahooInterval = intervalMap[interval]; if (!yahooInterval) { throw new Error( `Unsupported interval: ${interval}. Supported intervals: ${Object.keys( intervalMap ).join(", ")}` ); } return yahooInterval; } /** * Transform Yahoo Finance chart data to Lightweight Charts format * @param {Array} quotes - Raw quotes data from Yahoo Finance chart API * @returns {Array} Transformed data for Lightweight Charts */ _transformChartData(quotes) { return quotes .filter( (quote) => quote.open !== null && quote.high !== null && quote.low !== null && quote.close !== null ) .map((quote) => ({ time: Math.floor(quote.date.getTime() / 1000), open: parseFloat(quote.open), high: parseFloat(quote.high), low: parseFloat(quote.low), close: parseFloat(quote.close), volume: quote.volume ? parseFloat(quote.volume) : 0, })) .sort((a, b) => a.time - b.time); } /** * Aggregate 1-hour data into 4-hour candles * @param {Array} data - Array of 1-hour OHLC data points * @returns {Array} Aggregated 4-hour data */ _aggregateTo4Hour(data) { if (data.length === 0) return data; const aggregatedData = []; let currentGroup = []; let groupStartTime = null; for (const candle of data) { const candleTime = new Date(candle.time * 1000); if (groupStartTime === null) { groupStartTime = new Date(candleTime); groupStartTime.setMinutes(0, 0, 0); } const hoursDiff = (candleTime - groupStartTime) / (1000 * 60 * 60); if (hoursDiff < 4) { currentGroup.push(candle); } else { if (currentGroup.length > 0) { aggregatedData.push(this._createAggregatedCandle(currentGroup)); } currentGroup = [candle]; groupStartTime = new Date(candleTime); groupStartTime.setMinutes(0, 0, 0); } } if (currentGroup.length > 0) { aggregatedData.push(this._createAggregatedCandle(currentGroup)); } return aggregatedData; } /** * Create an aggregated candle from a group of candles * @param {Array} candles - Array of candles to aggregate * @returns {Object} Aggregated candle */ _createAggregatedCandle(candles) { if (candles.length === 0) return null; const open = candles[0].open; const close = candles[candles.length - 1].close; const high = Math.max(...candles.map((c) => c.high)); const low = Math.min(...candles.map((c) => c.low)); const volume = candles.reduce((sum, c) => sum + (c.volume || 0), 0); const time = candles[0].time; return { time, open, high, low, close, volume, }; } } module.exports = DataFetcher;