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