/** * Indicator Definition Object for Bollinger Bands (BB). * This object is used by the indicator manager to create and control the indicator. */ const BB_INDICATOR = { name: 'BB', label: 'Bollinger Bands (3 Sets)', usesBaseData: true, // This indicator needs the raw 1m data for its own aggregation params: [ { name: 'timeframe', type: 'number', defaultValue: 1440, min: 1, label: 'Timeframe (min)' }, ], // Hardcoded internal parameters, no longer exposed to the user. internalParams: { bb1_len_upper: 20, bb1_std_upper: 1.6, bb1_len_lower: 20, bb1_std_lower: 1.6, bb2_len_upper: 20, bb2_std_upper: 2.4, bb2_len_lower: 20, bb2_std_lower: 2.4, bb3_len_upper: 20, bb3_std_upper: 3.3, bb3_len_lower: 20, bb3_std_lower: 3.3, }, calculateFull: calculateFullBollingerBands, }; /** * Calculates all three sets of Bollinger Bands for the entire dataset. * @param {Array} data An array of candle objects with 'close' and 'time' properties. * @param {Object} params An object containing the indicator's parameters (only timeframe from UI). * @returns {Object} An object containing arrays of data points for each band. */ function calculateFullBollingerBands(data, params) { const { timeframe } = params; const internal = BB_INDICATOR.internalParams; // First, aggregate the base data into the desired timeframe for the BB calculation. // Assumes an 'aggregateCandles' function is defined elsewhere. const aggregatedData = aggregateCandles(data, timeframe); if (aggregatedData.length === 0) { return { bb1_upper: [], bb1_lower: [], bb2_upper: [], bb2_lower: [], bb3_upper: [], bb3_lower: [] }; } // Calculate each set of bands using the aggregated data. // The calculateBands function now returns data in a step-line format. const bb1 = calculateBands(aggregatedData, internal.bb1_len_upper, internal.bb1_std_upper, internal.bb1_len_lower, internal.bb1_std_lower); const bb2 = calculateBands(aggregatedData, internal.bb2_len_upper, internal.bb2_std_upper, internal.bb2_len_lower, internal.bb2_std_lower); const bb3 = calculateBands(aggregatedData, internal.bb3_len_upper, internal.bb3_std_upper, internal.bb3_len_lower, internal.bb3_std_lower); // Extend the last point of each band to the latest candle time const lastCandleTime = data[data.length - 1].time; const extendLine = (bandData) => { if (bandData.length > 0) { const lastPoint = bandData[bandData.length - 1]; if (lastPoint.time < lastCandleTime) { // Push a new point to extend the horizontal line to the end of the chart. bandData.push({ time: lastCandleTime, value: lastPoint.value }); } } return bandData; }; return { bb1_upper: extendLine(bb1.upper), bb1_lower: extendLine(bb1.lower), bb2_upper: extendLine(bb2.upper), bb2_lower: extendLine(bb2.lower), bb3_upper: extendLine(bb3.upper), bb3_lower: extendLine(bb3.lower), }; } /** * Helper function to calculate a single set of upper and lower Bollinger Bands. * MODIFIED: This function now generates points in a "step-line" format, where each * calculated value is held constant until the next calculation time. * @param {Array} data The aggregated candle data to calculate the bands from. * @param {number} upperLength The lookback period for the upper band. * @param {number} upperStdDev The standard deviation multiplier for the upper band. * @param {number} lowerLength The lookback period for the lower band. * @param {number} lowerStdDev The standard deviation multiplier for the lower band. * @returns {{upper: Array, lower: Array}} An object containing the point arrays for the upper and lower bands. */ function calculateBands(data, upperLength, upperStdDev, lowerLength, lowerStdDev) { const upperBand = []; const lowerBand = []; // --- Calculate Upper Band --- for (let i = upperLength - 1; i < data.length; i++) { const slice = data.slice(i - upperLength + 1, i + 1); const sma = slice.reduce((sum, candle) => sum + candle.close, 0) / upperLength; const stdDev = Math.sqrt(slice.reduce((sum, candle) => sum + Math.pow(candle.close - sma, 2), 0) / upperLength); const newTime = slice[slice.length - 1].time; const newValue = sma + (stdDev * upperStdDev); if (upperBand.length > 0) { // Get the value from the previous calculation period. const lastValue = upperBand[upperBand.length - 1].value; // Add a point at the new time with the *old* value. This creates the horizontal line segment. upperBand.push({ time: newTime, value: lastValue }); } // Add the point with the new value at the new time. This creates the vertical jump. upperBand.push({ time: newTime, value: newValue }); } // --- Calculate Lower Band --- for (let i = lowerLength - 1; i < data.length; i++) { const slice = data.slice(i - lowerLength + 1, i + 1); const sma = slice.reduce((sum, candle) => sum + candle.close, 0) / lowerLength; const stdDev = Math.sqrt(slice.reduce((sum, candle) => sum + Math.pow(candle.close - sma, 2), 0) / lowerLength); const newTime = slice[slice.length - 1].time; const newValue = sma - (stdDev * lowerStdDev); if (lowerBand.length > 0) { // Get the value from the previous calculation period. const lastValue = lowerBand[lowerBand.length - 1].value; // Add a point at the new time with the *old* value. This creates the horizontal line segment. lowerBand.push({ time: newTime, value: lastValue }); } // Add the point with the new value at the new time. This creates the vertical jump. lowerBand.push({ time: newTime, value: newValue }); } return { upper: upperBand, lower: lowerBand }; }