UNPKG

candlestick-chart-generator

Version:

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

256 lines (222 loc) 8.58 kB
const DataFetcher = require("./dataFetcher"); const ChartRendererSimple = require("./chartRenderer_simple"); const fs = require("fs"); class CandlestickChartGeneratorSimple { constructor() { this.dataFetcher = new DataFetcher(); this.chartRenderer = null; } /** * Generate a candlestick chart screenshot using Simple Canvas Renderer * @param {Object} params - Chart generation parameters * @param {string} params.symbol - The ticker symbol (e.g., 'AAPL', 'BTC-USD', 'EURUSD=X') * @param {string} params.interval - The time interval ('1m', '1h', '1d', etc.) * @param {string|Date} [params.startDate] - Start date for historical data * @param {string|Date} [params.endDate] - End date for historical data * @param {string} [params.outputPath='chart.png'] - Path where the screenshot will be saved * @param {number} [params.width=1200] - Chart width in pixels * @param {number} [params.height=600] - Chart height in pixels * @param {Object} [params.chartOptions={}] - Chart rendering options for simple renderer * @param {number} [params.desiredBars] - Target number of visible candles. If provided and startDate is omitted, startDate will be auto-computed to approximate this bar count based on the symbol's market hours. * @param {boolean} [params.includePrePost] - Include pre/post market for stocks when fetching intraday data * @returns {Promise<string>} Path to the saved screenshot */ async generateChartScreenshot({ symbol, interval, startDate = null, endDate = null, outputPath = "chart.png", width = 1200, height = 600, chartOptions = {}, desiredBars, includePrePost, }) { try { this.chartRenderer = new ChartRendererSimple(width, height); if (!startDate) { const computedStart = this._computeStartDateForDesiredBars({ symbol, interval, endDate, desiredBars, includePrePost, }); if (computedStart) { startDate = computedStart; } } const effectiveEndDate = endDate ? new Date(endDate) : new Date(); console.log(`Fetching data for ${symbol} with interval ${interval}...`); const data = await this.dataFetcher.fetchHistoricalData( symbol, interval, startDate, effectiveEndDate, { includePrePost: includePrePost === true } ); if (!data || data.length === 0) { throw new Error( `No data available for ${symbol} with the specified parameters` ); } console.log(`Fetched ${data.length} data points`); console.log("Rendering chart with Simple Canvas..."); const chartBuffer = await this.chartRenderer.renderChart(data, { symbol, timeframe: interval, chartOptions, }); console.log(`Saving chart to ${outputPath}...`); fs.writeFileSync(outputPath, chartBuffer); console.log(`Chart screenshot saved successfully to ${outputPath}`); return outputPath; } catch (error) { throw new Error(`Failed to generate chart screenshot: ${error.message}`); } } /** * Generate chart as base64 image data instead of saving to file * @param {Object} params - Same parameters as generateChartScreenshot (except outputPath) * @returns {Promise<string>} Base64 encoded image data */ async generateChartBase64(params) { const { outputPath, ...chartParams } = params; try { this.chartRenderer = new ChartRendererSimple( chartParams.width || 1200, chartParams.height || 600 ); if (!chartParams.startDate) { const computedStart = this._computeStartDateForDesiredBars({ symbol: chartParams.symbol, interval: chartParams.interval, endDate: chartParams.endDate, desiredBars: chartParams.desiredBars, includePrePost: chartParams.includePrePost, }); if (computedStart) { chartParams.startDate = computedStart; } } const effectiveEndDate = chartParams.endDate ? new Date(chartParams.endDate) : new Date(); const data = await this.dataFetcher.fetchHistoricalData( chartParams.symbol, chartParams.interval, chartParams.startDate, effectiveEndDate, { includePrePost: chartParams.includePrePost === true } ); const chartBuffer = await this.chartRenderer.renderChart(data, { symbol: chartParams.symbol, timeframe: chartParams.interval, chartOptions: chartParams.chartOptions || {}, }); return chartBuffer.toString("base64"); } catch (error) { throw new Error(`Failed to generate chart base64: ${error.message}`); } } /** * Compute a suitable start date to approximate a target number of visible bars * using market sessions: * - Stocks: 6.5 trading hours/day regular, optionally extended (~13h) if includePrePost * - Crypto: 24h * - Forex: ~24h on 5 days; averaged over calendar days */ _computeStartDateForDesiredBars({ symbol, interval, endDate, desiredBars, includePrePost }) { const effectiveEnd = endDate ? new Date(endDate) : new Date(); const intervalLower = String(interval || "").toLowerCase(); const targetBars = typeof desiredBars === "number" && desiredBars > 0 ? desiredBars : this._defaultTargetBars(intervalLower); if (!targetBars) return null; const barsPerCalendarDay = this._estimateBarsPerCalendarDay( symbol, intervalLower, includePrePost === true ); if (!barsPerCalendarDay || barsPerCalendarDay <= 0) return null; const neededDays = Math.max(1, Math.ceil(targetBars / barsPerCalendarDay)); const startMs = effectiveEnd.getTime() - neededDays * 24 * 60 * 60 * 1000; return new Date(startMs); } _defaultTargetBars(intervalLower) { switch (intervalLower) { case "1m": return 300; // ~one active session for stocks case "5m": return 240; // ~a couple of sessions case "15m": return 200; case "30m": return 180; case "1h": return 160; case "4h": return 200; case "1d": return 180; default: return null; } } _estimateBarsPerCalendarDay(symbol, intervalLower, includePrePost) { const market = this._classifySymbolMarket(symbol); // Special-case daily bars: one bar per trading day irrespective of session hours if (intervalLower === "1d") { if (market === "crypto" || market === "forex") return 1; // one bar per calendar day return 5 / 7; // stocks: 5 trading days per week, averaged across calendar days } // minutes per bar const minutesPerBar = this._minutesPerBar(intervalLower); if (!minutesPerBar) return null; if (market === "crypto") { // 24h per day const barsPerDay = (24 * 60) / minutesPerBar; return barsPerDay; } if (market === "forex") { // 24h but effectively 5 days/week; average across 7 calendar days const barsPerTradingDay = (24 * 60) / minutesPerBar; return barsPerTradingDay * (5 / 7); } // stocks: use session hours for intraday bars const regularHoursMinutes = 6.5 * 60; // 09:30 - 16:00 (~6.5h) const extendedHoursMinutes = includePrePost ? 13 * 60 : regularHoursMinutes; // rough: pre+post doubles window const barsPerTradingDay = extendedHoursMinutes / minutesPerBar; // average across calendar days (5 trading days a week) const barsPerCalendarDay = barsPerTradingDay * (5 / 7); return barsPerCalendarDay; } _minutesPerBar(intervalLower) { if (intervalLower.endsWith("m")) { return parseInt(intervalLower.replace("m", ""), 10); } if (intervalLower.endsWith("h")) { return parseInt(intervalLower.replace("h", ""), 10) * 60; } if (intervalLower === "1d") return 24 * 60; if (intervalLower === "4h") return 4 * 60; if (intervalLower === "1wk") return 7 * 24 * 60; if (intervalLower === "1mo") return 30 * 24 * 60; return null; } _classifySymbolMarket(symbol) { const sym = String(symbol || "").toUpperCase(); if (sym.endsWith("=X")) return "forex"; if (sym.includes("-") || sym.includes("/")) return "crypto"; return "stock"; } /** * Close and clean up resources (no-op for canvas renderer) */ async close() { console.log("Simple Canvas chart generator closed"); } } module.exports = CandlestickChartGeneratorSimple;