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.

555 lines (550 loc) 19.7 kB
import { ChartConfig } from '../core/config.js'; import { ChartRenderer } from '../core/renderer.js'; export function parseArgs() { const args = process.argv.slice(2); const parsed = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; const nextArg = args[i + 1]; switch (arg) { case '--symbol': case '-s': if (nextArg && !nextArg.startsWith('-')) { parsed.symbol = nextArg; i++; } break; case '--timeframe': case '-t': if (nextArg && !nextArg.startsWith('-')) { parsed.timeframe = nextArg; i++; } break; case '--exchange': case '-e': if (nextArg && !nextArg.startsWith('-')) { parsed.exchange = nextArg; i++; } break; case '--output': case '-o': if (nextArg && !nextArg.startsWith('-')) { parsed.output = nextArg; i++; } break; case '--width': case '-w': if (nextArg && !nextArg.startsWith('-')) { const width = parseInt(nextArg); if (!isNaN(width)) { parsed.width = width; i++; } } break; case '--height': case '-h': if (nextArg && !nextArg.startsWith('-')) { const height = parseInt(nextArg); if (!isNaN(height)) { parsed.height = height; i++; } } break; case '--theme': if (nextArg && !nextArg.startsWith('-')) { parsed.theme = nextArg; i++; } break; case '--chart-type': case '--type': if (nextArg && !nextArg.startsWith('-')) { parsed.chartType = nextArg; i++; } break; case '--scale-x': if (nextArg && !nextArg.startsWith('-')) { const scale = parseFloat(nextArg); if (!isNaN(scale)) { parsed.scaleX = scale; i++; } } break; case '--scale-y': if (nextArg && !nextArg.startsWith('-')) { const scale = parseFloat(nextArg); if (!isNaN(scale)) { parsed.scaleY = scale; i++; } } break; case '--auto-scale': parsed.autoScale = true; break; case '--min-scale': if (nextArg && !nextArg.startsWith('-')) { const scale = parseFloat(nextArg); if (!isNaN(scale)) { parsed.minScale = scale; i++; } } break; case '--max-scale': if (nextArg && !nextArg.startsWith('-')) { const scale = parseFloat(nextArg); if (!isNaN(scale)) { parsed.maxScale = scale; i++; } } break; case '--limit': case '-l': if (nextArg && !nextArg.startsWith('-')) { const limit = parseInt(nextArg); if (!isNaN(limit)) { parsed.limit = limit; i++; } } break; case '--custom-colors': if (nextArg && !nextArg.startsWith('-')) { parsed.customColors = nextArg; i++; } break; case '--levels': if (nextArg && !nextArg.startsWith('-')) { parsed.levels = nextArg; i++; } break; case '--title': if (nextArg && !nextArg.startsWith('-')) { parsed.title = nextArg; i++; } break; case '--watermark': if (nextArg && !nextArg.startsWith('-')) { parsed.watermark = nextArg; i++; } break; case '--watermark-position': if (nextArg && !nextArg.startsWith('-')) { parsed.watermarkPosition = nextArg; i++; } break; case '--watermark-color': if (nextArg && !nextArg.startsWith('-')) { parsed.watermarkColor = nextArg; i++; } break; case '--watermark-size': if (nextArg && !nextArg.startsWith('-')) { const size = parseInt(nextArg); if (!isNaN(size)) { parsed.watermarkSize = size; i++; } } break; case '--watermark-opacity': if (nextArg && !nextArg.startsWith('-')) { const opacity = parseFloat(nextArg); if (!isNaN(opacity)) { parsed.watermarkOpacity = opacity; i++; } } break; case '--hide-title': parsed.hideTitle = true; break; case '--hide-time-axis': parsed.hideTimeAxis = true; break; case '--hide-grid': parsed.hideGrid = true; break; case '--vwap': parsed.showVWAP = true; break; case '--ema': parsed.showEMA = true; parsed.emaPeriod = 20; break; case '--sma': parsed.showSMA = true; parsed.smaPeriod = 20; break; case '--bb': case '--bollinger-bands': parsed.showBollingerBands = true; parsed.bbPeriod = 20; parsed.bbStandardDeviations = 2; break; case '--bb-upper-color': if (nextArg && !nextArg.startsWith('-')) { parsed.bbUpperColor = nextArg; i++; } break; case '--bb-middle-color': if (nextArg && !nextArg.startsWith('-')) { parsed.bbMiddleColor = nextArg; i++; } break; case '--bb-lower-color': if (nextArg && !nextArg.startsWith('-')) { parsed.bbLowerColor = nextArg; i++; } break; case '--bb-background-color': if (nextArg && !nextArg.startsWith('-')) { parsed.bbBackgroundColor = nextArg; i++; } break; case '--bb-background-opacity': if (nextArg && !nextArg.startsWith('-')) { const opacity = parseFloat(nextArg); if (!isNaN(opacity) && opacity >= 0 && opacity <= 1) { parsed.bbBackgroundOpacity = opacity; i++; } } break; case '--background-color': case '--bg-color': if (nextArg && !nextArg.startsWith('-')) { parsed.backgroundColor = nextArg; i++; } break; case '--text-color': case '--color': if (nextArg && !nextArg.startsWith('-')) { parsed.textColor = nextArg; i++; } break; case '--fetch': parsed.fetch = true; break; case '--batch': parsed.batch = []; break; case '--help': parsed.help = true; break; case '--compare': if (nextArg && !nextArg.startsWith('-')) { parsed.compare = nextArg; i++; } break; case '--layout': if (nextArg && !nextArg.startsWith('-')) { parsed.layout = nextArg; i++; } break; case '--columns': if (nextArg && !nextArg.startsWith('-')) { const columns = parseInt(nextArg); if (!isNaN(columns)) { parsed.columns = columns; i++; } } break; case '--rows': if (nextArg && !nextArg.startsWith('-')) { const rows = parseInt(nextArg); if (!isNaN(rows)) { parsed.rows = rows; i++; } } break; case '--gap': if (nextArg && !nextArg.startsWith('-')) { const gap = parseInt(nextArg); if (!isNaN(gap)) { parsed.gap = gap; i++; } } break; case '--timeframes': if (nextArg && !nextArg.startsWith('-')) { parsed.timeframes = nextArg; i++; } break; default: if (arg.startsWith('-')) { console.warn(`Unknown option: ${arg}`); } break; } } return parsed; } export function validateArgs(args) { if (args.compare) { if (!args.output) { console.error('Error: Output path is required for comparison (--output or -o)'); return false; } return true; } if (!args.symbol) { console.error('Error: Symbol is required (--symbol or -s)'); return false; } if (!args.timeframe) { console.error('Error: Timeframe is required (--timeframe or -t)'); return false; } if (!args.output) { console.error('Error: Output path is required (--output or -o)'); return false; } return true; } export function showHelp() { console.log(` Chart To Image - Generate trading chart images Usage: chart-to-image [options] Required Options: --symbol, -s <symbol> Trading symbol (e.g., BTC/USDT) --timeframe, -t <timeframe> Timeframe (1m, 5m, 15m, 1h, 4h, 1d) --output, -o <path> Output file path Optional Options: --exchange, -e <exchange> Exchange (default: binance) --width, -w <width> Chart width (default: 1200) --height, -h <height> Chart height (default: 800) --theme <theme> Theme: light or dark (default: dark) --chart-type, --type <type> Chart type: candlestick, line, area, heikin-ashi, renko, line-break --scale-x <scale> X-axis scale factor --scale-y <scale> Y-axis scale factor --auto-scale Enable auto-scaling --min-scale <scale> Minimum scale --max-scale <scale> Maximum scale --limit, -l <limit> Number of candles (default: 100) --custom-colors <colors> Custom colors (format: type=color,type=color) --levels <levels> Horizontal levels (format: value:color:style:label,value:color:style:label) --title <title> Chart title --watermark <text> Watermark text --watermark-position <pos> Watermark position: top-left, top-right, bottom-left, bottom-right, center --watermark-color <color> Watermark color --watermark-size <size> Watermark font size --watermark-opacity <op> Watermark opacity (0-1) --hide-title Hide chart title --hide-time-axis Hide time axis --hide-grid Hide grid --vwap Show VWAP indicator --ema Show EMA indicator (default: 20 period) --sma Show SMA indicator (default: 20 period) --bb Show Bollinger Bands indicator (default: 20 period, 2 std dev) --bb-upper-color <color> Upper band color (e.g., #ff6b6b) --bb-middle-color <color> Middle band color (e.g., #4ecdc4) --bb-lower-color <color> Lower band color (e.g., #ff6b6b) --bb-background-color <color> Background fill color (e.g., #ff6b6b) --bb-background-opacity <0-1> Background fill opacity (0.0-1.0, default: 0.1) --background-color <color> Background color --text-color <color> Text color --fetch Fetch fresh data --batch Batch mode --help Show this help Examples: chart-to-image --symbol BTC/USDT --timeframe 1h --output chart.png chart-to-image -s ETH/USDT -t 4h -o eth.png --theme light --chart-type line chart-to-image -s BTC/USDT -t 1d -o btc.png --watermark "My Chart" --watermark-position center `); } export function argsToConfig(args) { const config = { symbol: args.symbol, timeframe: args.timeframe, exchange: args.exchange || 'binance', outputPath: args.output, width: args.width || 1200, height: args.height || 800, theme: args.theme || 'dark', chartType: args.chartType || 'candlestick', limit: args.limit || 100, fetch: args.fetch || false }; addScalingOptions(config, args); addCustomColors(config, args); addHorizontalLevels(config, args); addTitle(config, args); addWatermark(config, args); addHideOptions(config, args); addColors(config, args); addComparisonOptions(config, args); return new ChartConfig(config); } function addScalingOptions(config, args) { if (args.scaleX !== undefined) config.scaleX = args.scaleX; if (args.scaleY !== undefined) config.scaleY = args.scaleY; if (args.autoScale) config.autoScale = true; if (args.minScale !== undefined) config.minScale = args.minScale; if (args.maxScale !== undefined) config.maxScale = args.maxScale; } function addCustomColors(config, args) { if (args.customColors) { try { const colorParts = args.customColors.split(','); const customBarColors = {}; colorParts.forEach(part => { const [type, color] = part.split('='); if (type && color) { customBarColors[type.trim()] = color.trim(); } }); if (Object.keys(customBarColors).length > 0) { config.customBarColors = customBarColors; } } catch { console.warn('Invalid custom colors format. Use: type=color,type=color (e.g., bullish=#00ff88,bearish=#ff4444)'); } } } function addHorizontalLevels(config, args) { if (args.levels) { try { const levelParts = args.levels.split(','); const horizontalLevels = levelParts.map(level => { const [value, color, lineStyle, label] = level.split(':'); return { value: parseFloat(value), color: color, lineStyle: lineStyle, label: label, type: 'custom' }; }); if (horizontalLevels.length > 0) { config.horizontalLevels = horizontalLevels; } } catch { console.warn('Invalid levels format. Use: value:color:style:label,value:color:style:label (e.g., 45000:#ff0000:solid:Resistance,40000:#00ff00:dotted:Support)'); } } } function addTitle(config, args) { if (args.title) { config.title = args.title; } } function addWatermark(config, args) { if (args.watermark) { config.watermark = { text: args.watermark, position: args.watermarkPosition || 'bottom-right', color: args.watermarkColor, fontSize: args.watermarkSize, opacity: args.watermarkOpacity }; } } function addHideOptions(config, args) { if (args.hideTitle) config.showTitle = false; if (args.hideTimeAxis) config.showTimeAxis = false; if (args.hideGrid) config.showGrid = false; if (args.showVWAP) config.showVWAP = true; if (args.showEMA) config.showEMA = true; if (args.emaPeriod) config.emaPeriod = args.emaPeriod; if (args.showSMA) config.showSMA = true; if (args.smaPeriod) config.smaPeriod = args.smaPeriod; if (args.showBollingerBands) config.showBollingerBands = true; if (args.bbPeriod) config.bbPeriod = args.bbPeriod; if (args.bbStandardDeviations) config.bbStandardDeviations = args.bbStandardDeviations; } function addColors(config, args) { if (args.backgroundColor) config.backgroundColor = args.backgroundColor; if (args.textColor) config.textColor = args.textColor; } function addComparisonOptions(config, args) { if (args.compare) { const symbols = args.compare.split(',').map(s => s.trim()); config.symbols = symbols; } if (args.layout) { config.layout = { type: args.layout, columns: args.columns, rows: args.rows, gap: args.gap }; } if (args.timeframes) { const timeframes = args.timeframes.split(',').map(t => t.trim()); config.timeframes = timeframes; } } export async function executeFromArgs(args) { try { if (args.help) { showHelp(); return { success: true }; } if (!validateArgs(args)) { return { success: false, error: 'Invalid arguments' }; } const config = argsToConfig(args); const renderer = new ChartRenderer(config); const result = await renderer.generateChart(); if (result.success) { console.log(`✅ Chart generated: ${args.output}`); } else { console.error(`❌ Failed to generate chart: ${result.error}`); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`❌ Error: ${errorMessage}`); return { success: false, error: errorMessage }; } }