candlestick-chart-generator
Version:
A Node.js library for generating candlestick chart screenshots using financial data
178 lines (152 loc) • 5.32 kB
JavaScript
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;