switching TF, multi indicators on chart
This commit is contained in:
56
static/bb.js
56
static/bb.js
@ -14,7 +14,7 @@ const BB_INDICATOR = {
|
||||
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,
|
||||
};
|
||||
|
||||
@ -29,14 +29,12 @@ function calculateFullBollingerBands(data, 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.
|
||||
// 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);
|
||||
@ -47,8 +45,7 @@ function calculateFullBollingerBands(data, params) {
|
||||
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 });
|
||||
bandData.push({ ...lastPoint, time: lastCandleTime });
|
||||
}
|
||||
}
|
||||
return bandData;
|
||||
@ -63,57 +60,32 @@ function calculateFullBollingerBands(data, params) {
|
||||
|
||||
/**
|
||||
* 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<Object>} 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<Object>, lower: Array<Object>}} An object containing the point arrays for the upper and lower bands.
|
||||
* @returns {{upper: Array<Object>, lower: Array<Object>}}
|
||||
*/
|
||||
function calculateBands(data, upperLength, upperStdDev, lowerLength, lowerStdDev) {
|
||||
const upperBand = [];
|
||||
const lowerBand = [];
|
||||
|
||||
// --- Calculate Upper Band ---
|
||||
// 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 });
|
||||
upperBand.push({
|
||||
time: slice[slice.length - 1].time,
|
||||
value: sma + (stdDev * upperStdDev)
|
||||
});
|
||||
}
|
||||
|
||||
// --- Calculate Lower Band ---
|
||||
// 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 });
|
||||
lowerBand.push({
|
||||
time: slice[slice.length - 1].time,
|
||||
value: sma - (stdDev * lowerStdDev)
|
||||
});
|
||||
}
|
||||
|
||||
return { upper: upperBand, lower: lowerBand };
|
||||
|
||||
@ -6,25 +6,25 @@
|
||||
* @returns {Object} A manager object with public methods to control indicators.
|
||||
*/
|
||||
function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef) {
|
||||
// --- FIX: --- Added `debounceTimerId` to each slot object to track pending updates.
|
||||
const indicatorSlots = [
|
||||
{ id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {}, calculator: null },
|
||||
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {}, calculator: null },
|
||||
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {}, calculator: null },
|
||||
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {}, calculator: null },
|
||||
{ id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
||||
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
||||
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
||||
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
||||
];
|
||||
|
||||
// **FIX**: Updated colors object to match your styling request.
|
||||
const colors = {
|
||||
bb: {
|
||||
bb1_upper: 'rgba(128, 25, 34, 0.5)', // Highest opacity
|
||||
bb1_upper: 'rgba(128, 25, 34, 0.5)',
|
||||
bb2_upper: 'rgba(128, 25, 34, 0.75)',
|
||||
bb3_upper: 'rgba(128, 25, 34, 1)', // Lowest opacity
|
||||
bb1_lower: 'rgba(6, 95, 6, 0.5)', // Highest band, 50% opacity
|
||||
bb3_upper: 'rgba(128, 25, 34, 1)',
|
||||
bb1_lower: 'rgba(6, 95, 6, 0.5)',
|
||||
bb2_lower: 'rgba(6, 95, 6, 0.75)',
|
||||
bb3_lower: 'rgba(6, 95, 6, 1.0)', // Lowest band, 100% opacity
|
||||
bb3_lower: 'rgba(6, 95, 6, 1.0)',
|
||||
},
|
||||
hurst: { topBand: '#787b86', bottomBand: '#787b86', topBand_h: '#673ab7', bottomBand_h: '#673ab7' },
|
||||
default: ['#00BCD4', '#FFEB3B', '#4CAF50', '#E91E63'] // Cyan, Yellow, Green, Pink
|
||||
default: ['#00BCD4', '#FFEB3B', '#4CAF50', '#E91E63']
|
||||
};
|
||||
|
||||
function populateDropdowns() {
|
||||
@ -53,12 +53,19 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
const slot = indicatorSlots.find(s => s.id === slotId);
|
||||
if (!slot) return;
|
||||
|
||||
// --- FIX: --- Cancel any pending debounced update from the previous indicator's controls.
|
||||
// This is the core of the fix, preventing the race condition.
|
||||
if (slot.debounceTimerId) {
|
||||
clearTimeout(slot.debounceTimerId);
|
||||
slot.debounceTimerId = null;
|
||||
}
|
||||
|
||||
slot.series.forEach(s => chart.removeSeries(s));
|
||||
slot.series = [];
|
||||
slot.definition = null;
|
||||
slot.params = {};
|
||||
slot.calculator = null;
|
||||
|
||||
|
||||
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
||||
controlsContainer.innerHTML = '';
|
||||
|
||||
@ -68,12 +75,12 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
if (!definition) return;
|
||||
|
||||
slot.definition = definition;
|
||||
|
||||
|
||||
definition.params.forEach(param => {
|
||||
const label = document.createElement('label');
|
||||
label.textContent = param.label || param.name;
|
||||
label.style.fontSize = '12px';
|
||||
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = param.type;
|
||||
input.value = param.defaultValue;
|
||||
@ -82,13 +89,14 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
input.className = 'input-field';
|
||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||
|
||||
let debounceTimer;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
// --- FIX: --- Use the slot's `debounceTimerId` property to manage the timeout.
|
||||
clearTimeout(slot.debounceTimerId);
|
||||
slot.debounceTimerId = setTimeout(() => {
|
||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||
updateIndicator(slot.id, true);
|
||||
}, 500);
|
||||
slot.debounceTimerId = null; // Clear the ID after the function has run.
|
||||
}, 500);
|
||||
});
|
||||
const controlGroup = document.createElement('div');
|
||||
controlGroup.style.display = 'flex';
|
||||
@ -103,26 +111,28 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
|
||||
function updateIndicator(slotId, isFullRecalculation = false) {
|
||||
const slot = indicatorSlots.find(s => s.id === slotId);
|
||||
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
||||
if (!slot || !slot.definition) return;
|
||||
|
||||
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
||||
if (candleDataForCalc.length === 0) return;
|
||||
|
||||
if (!slot || !slot.definition || candleDataForCalc.length === 0) return;
|
||||
|
||||
if (isFullRecalculation) {
|
||||
slot.series.forEach(s => chart.removeSeries(s));
|
||||
slot.series = [];
|
||||
|
||||
|
||||
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
|
||||
|
||||
|
||||
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
||||
Object.keys(indicatorResult).forEach(key => {
|
||||
const seriesData = indicatorResult[key];
|
||||
const indicatorNameLower = slot.definition.name.toLowerCase();
|
||||
const series = chart.addLineSeries({
|
||||
color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
|
||||
lineWidth: 1, // **FIX**: Set line width to 1px
|
||||
title: '', // **FIX**: Remove title label
|
||||
lastValueVisible: false, // **FIX**: Remove price label on the right
|
||||
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
||||
lineWidth: 1,
|
||||
title: '',
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
series.setData(seriesData);
|
||||
slot.series.push(series);
|
||||
@ -130,53 +140,69 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
} else {
|
||||
const series = chart.addLineSeries({
|
||||
color: colors.default[slot.id - 1],
|
||||
lineWidth: 1, // **FIX**: Set line width to 1px
|
||||
title: '', // **FIX**: Remove title label
|
||||
lastValueVisible: false, // **FIX**: Remove price label on the right
|
||||
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
||||
lineWidth: 1,
|
||||
title: '',
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
series.setData(indicatorResult);
|
||||
slot.series.push(series);
|
||||
}
|
||||
|
||||
|
||||
if (slot.definition.createRealtime) {
|
||||
slot.calculator = slot.definition.createRealtime(slot.params);
|
||||
slot.calculator.prime(candleDataForCalc);
|
||||
}
|
||||
} else if (slot.calculator) {
|
||||
// **FIX**: This is the lightweight real-time update logic
|
||||
const lastCandle = candleDataForCalc[candleDataForCalc.length - 1];
|
||||
if (!lastCandle) return;
|
||||
|
||||
const newPoint = slot.calculator.update(lastCandle);
|
||||
|
||||
if (newPoint && typeof newPoint === 'object') {
|
||||
if (slot.series.length > 1) { // Multi-line indicator
|
||||
Object.keys(newPoint).forEach((key, index) => {
|
||||
if (slot.series[index] && newPoint[key]) {
|
||||
slot.series[index].update(newPoint[key]);
|
||||
}
|
||||
});
|
||||
} else if (slot.series.length === 1) { // Single-line indicator
|
||||
slot.series[0].update(newPoint);
|
||||
}
|
||||
if (slot.series.length > 1) { // Multi-line indicator
|
||||
Object.keys(newPoint).forEach((key, index) => {
|
||||
if (slot.series[index] && newPoint[key]) {
|
||||
slot.series[index].update(newPoint[key]);
|
||||
}
|
||||
});
|
||||
} else if (slot.series.length === 1) { // Single-line indicator
|
||||
slot.series[0].update(newPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function recalculateAllAfterHistory(baseData, displayedData) {
|
||||
baseCandleDataRef = baseData;
|
||||
displayedCandleDataRef = displayedData;
|
||||
indicatorSlots.forEach(slot => {
|
||||
if (slot.definition) updateIndicator(slot.id, true);
|
||||
baseCandleDataRef = baseData;
|
||||
displayedCandleDataRef = displayedData;
|
||||
|
||||
// --- FIX: --- Clear any pending debounced updates from parameter changes.
|
||||
// This prevents a stale update from a parameter input from running after
|
||||
// the chart has already been reset for a new timeframe.
|
||||
indicatorSlots.forEach(slot => {
|
||||
if (slot.debounceTimerId) {
|
||||
clearTimeout(slot.debounceTimerId);
|
||||
slot.debounceTimerId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// --- FIX: --- Defer the full recalculation to the next frame.
|
||||
// This prevents a race condition where indicators are removed/added while the chart
|
||||
// is still processing the main series' `setData` operation from a timeframe change.
|
||||
setTimeout(() => {
|
||||
indicatorSlots.forEach(slot => {
|
||||
if (slot.definition) {
|
||||
updateIndicator(slot.id, true);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// **FIX**: New lightweight function for real-time updates
|
||||
function updateAllOnNewCandle() {
|
||||
indicatorSlots.forEach(slot => {
|
||||
if (slot.definition) {
|
||||
updateIndicator(slot.id, false); // Perform a lightweight update
|
||||
updateIndicator(slot.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -184,6 +210,6 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
||||
return {
|
||||
populateDropdowns,
|
||||
recalculateAllAfterHistory,
|
||||
updateAllOnNewCandle, // Expose the new function
|
||||
updateAllOnNewCandle,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user