UNPKG

@neabyte/chart-to-image

Version:

Convert trading charts to images using Node.js canvas with advanced features: 6 chart types, VWAP/EMA/SMA indicators, custom colors, themes, hide elements, scaling, and PNG/JPEG export formats.

191 lines (190 loc) 7.51 kB
import { ChartConfig } from '../core/config.js'; import { ChartRenderer } from '../core/renderer.js'; import { ComparisonRenderer } from '../renderer/comparison.js'; import { DataProvider } from '../utils/provider.js'; export class ComparisonService { static DEFAULT_LAYOUT_TYPE = 'side-by-side'; config; dataProvider; constructor(config) { this.config = config; this.dataProvider = new DataProvider({ name: (config.exchange || 'binance'), sandbox: false }); } async generateComparison() { try { const { symbols, layout = { type: ComparisonService.DEFAULT_LAYOUT_TYPE }, width = 1600, height = 800 } = this.config; const comparisonRenderer = new ComparisonRenderer(width, height, layout); await this.addChartsToRenderer(comparisonRenderer, symbols); await comparisonRenderer.renderComparison(); const buffer = await comparisonRenderer.exportComparison({ format: 'png' }); return this.createSuccessResult(buffer); } catch (error) { return this.createErrorResult(error); } } async addChartsToRenderer(comparisonRenderer, symbols) { if (this.config.timeframes && this.config.timeframes.length > 0) { await this.addTimeframeCharts(comparisonRenderer, symbols); } else { await this.addSymbolCharts(comparisonRenderer, symbols); } } async addTimeframeCharts(comparisonRenderer, symbols) { const symbol = symbols[0]; const maxCharts = Math.min(symbols.length, this.config.timeframes.length); for (let i = 0; i < maxCharts; i++) { const timeframe = this.config.timeframes[i]; const chartData = await this.generateChartData(symbol, timeframe); if (chartData) { comparisonRenderer.addChart(chartData); } } } async addSymbolCharts(comparisonRenderer, symbols) { for (const symbol of symbols) { const chartData = await this.generateChartData(symbol); if (chartData) { comparisonRenderer.addChart(chartData); } } } async createSuccessResult(buffer) { if (this.config.outputPath) { const fs = await import('fs/promises'); await fs.writeFile(this.config.outputPath, buffer); return { success: true, outputPath: this.config.outputPath, buffer }; } return { success: true, buffer }; } createErrorResult(error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } async generateChartData(symbol, timeframe) { try { const chartConfig = new ChartConfig({ symbol, timeframe: timeframe || this.config.timeframe || '1h', exchange: this.config.exchange || 'binance', width: 800, height: 600, theme: this.config.theme || 'dark', chartType: this.config.chartType || 'candlestick', showTitle: true, showTimeAxis: true, showGrid: true }); const renderer = new ChartRenderer(chartConfig); const result = await renderer.generateChart(); if (result.success) { const data = await this.dataProvider.fetchOHLCV(symbol, timeframe || this.config.timeframe || '1h', 100); const chartData = this.createChartData(data, chartConfig); return chartData; } return null; } catch (error) { console.warn(`Failed to generate chart for ${symbol}:`, error); return null; } } createChartData(data, config) { const ohlc = data.map(item => ({ time: item.timestamp, open: item.open, high: item.high, low: item.low, close: item.close, ...(item.volume !== undefined && { volume: item.volume }) })); return { ohlc, levels: [], config: { width: config.width, height: config.height, backgroundColor: config.backgroundColor || '#1e222d', textColor: config.textColor || '#ffffff', gridColor: '#2b2b43', borderColor: '#2b2b43', chartType: config.chartType || 'candlestick', customBarColors: { bullish: this.config.customBarColors?.bullish || '#26a69a', bearish: this.config.customBarColors?.bearish || '#ef5350', wick: this.config.customBarColors?.wick || '#424242', border: this.config.customBarColors?.border || '#E0E0E0' }, horizontalLevels: [], title: `${config.symbol} ${config.timeframe}`, showTitle: true, showTimeAxis: true, showGrid: true, showVWAP: this.config.showVWAP === true, showEMA: this.config.showEMA === true, ...(this.config.emaPeriod !== undefined && { emaPeriod: this.config.emaPeriod }), showSMA: this.config.showSMA === true, ...(this.config.smaPeriod !== undefined && { smaPeriod: this.config.smaPeriod }), showBollingerBands: this.config.showBollingerBands === true, ...(this.config.bbPeriod !== undefined && { bbPeriod: this.config.bbPeriod }), ...(this.config.bbStandardDeviations !== undefined && { bbStandardDeviations: this.config.bbStandardDeviations }), ...(this.config.bbColors !== undefined && { bbColors: this.config.bbColors }) } }; } static async sideBySide(symbols, outputPath, config) { const service = new ComparisonService({ symbols, outputPath, layout: { type: ComparisonService.DEFAULT_LAYOUT_TYPE, gap: 20 }, ...config }); return service.generateComparison(); } static async grid(symbols, columns, outputPath, config) { if (symbols.length > 2) { return { success: false, error: `Grid layout supports maximum 2 symbols. Got ${symbols.length} symbols: ${symbols.join(', ')}` }; } if (columns > 2) { return { success: false, error: `Grid layout supports maximum 2 columns. Got ${columns} columns` }; } const service = new ComparisonService({ symbols, outputPath, layout: { type: 'grid', columns, gap: 15 }, ...config }); return service.generateComparison(); } static async timeframeComparison(symbol, timeframes, outputPath, config) { const service = new ComparisonService({ symbols: [symbol], timeframes, outputPath, layout: { type: ComparisonService.DEFAULT_LAYOUT_TYPE, gap: 20 }, ...config }); return service.generateComparison(); } }