/** * Aggregates fine-grained candle data into a larger timeframe. * For example, it can convert 1-minute candles into 5-minute candles. * * @param {Array} data - An array of candle objects, sorted by time. * Each object must have { time, open, high, low, close }. * @param {number} intervalMinutes - The desired new candle interval in minutes (e.g., 5 for 5m). * @returns {Array} A new array of aggregated candle objects. */ function aggregateCandles(data, intervalMinutes) { if (!data || data.length === 0 || !intervalMinutes || intervalMinutes < 1) { return []; } const intervalSeconds = intervalMinutes * 60; const aggregated = []; let currentAggCandle = null; data.forEach(candle => { // Validate candle data if (!candle || !candle.time || isNaN(candle.open) || isNaN(candle.high) || isNaN(candle.low) || isNaN(candle.close) || candle.open <= 0 || candle.high <= 0 || candle.low <= 0 || candle.close <= 0) { console.warn('Skipping invalid candle during aggregation:', candle); return; // Skip this candle } // Calculate the timestamp for the start of the interval bucket // Properly align to interval boundaries (e.g., 5-min intervals start at :00, :05, :10, etc.) const bucketTimestamp = Math.floor(candle.time / intervalSeconds) * intervalSeconds; if (!currentAggCandle || bucketTimestamp !== currentAggCandle.time) { // If a previous aggregated candle exists, push it to the results if (currentAggCandle) { aggregated.push(currentAggCandle); } // Start a new aggregated candle currentAggCandle = { time: bucketTimestamp, open: candle.open, high: candle.high, low: candle.low, close: candle.close, }; } else { // This candle belongs to the current aggregated candle, so update it currentAggCandle.high = Math.max(currentAggCandle.high, candle.high); currentAggCandle.low = Math.min(currentAggCandle.low, candle.low); currentAggCandle.close = candle.close; // The close is always the latest one } }); // Add the last aggregated candle if it exists if (currentAggCandle) { aggregated.push(currentAggCandle); } return aggregated; }