diff --git a/static/bb.js b/static/bb.js new file mode 100644 index 0000000..9f2f526 --- /dev/null +++ b/static/bb.js @@ -0,0 +1,92 @@ +/** + * 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: 16, bb1_std_upper: 1.9, bb1_len_lower: 17, bb1_std_lower: 1.5, + bb2_len_upper: 18, bb2_std_upper: 2.7, bb2_len_lower: 18, bb2_std_lower: 1.6, + bb3_len_upper: 16, bb3_std_upper: 2.6, bb3_len_lower: 16, bb3_std_lower: 1.8, + }, + 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. + 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 + 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) { + bandData.push({ ...lastPoint, time: lastCandleTime }); + } + } + 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. + * @returns {{upper: Array, lower: Array}} + */ +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); + upperBand.push({ + time: slice[slice.length - 1].time, + value: sma + (stdDev * upperStdDev) + }); + } + + // 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); + lowerBand.push({ + time: slice[slice.length - 1].time, + value: sma - (stdDev * lowerStdDev) + }); + } + + return { upper: upperBand, lower: lowerBand }; +}