switching TF, multi indicators on chart
This commit is contained in:
2
app.py
2
app.py
@ -46,7 +46,7 @@ def stream_historical_data(sid):
|
|||||||
all_klines = client.get_historical_klines(
|
all_klines = client.get_historical_klines(
|
||||||
SYMBOL,
|
SYMBOL,
|
||||||
Client.KLINE_INTERVAL_1MINUTE,
|
Client.KLINE_INTERVAL_1MINUTE,
|
||||||
start_str="1 week ago UTC" # Fetches data starting from 7 days ago until now
|
start_str="8 weeks ago UTC" # Fetches data starting from 8 weeks ago until now
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- ORIGINAL SOLUTION COMMENTED OUT ---
|
# --- ORIGINAL SOLUTION COMMENTED OUT ---
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
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,
|
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,
|
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,
|
bb3_len_upper: 20, bb3_std_upper: 3.3, bb3_len_lower: 20, bb3_std_lower: 3.3,
|
||||||
},
|
},
|
||||||
calculateFull: calculateFullBollingerBands,
|
calculateFull: calculateFullBollingerBands,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,14 +29,12 @@ function calculateFullBollingerBands(data, params) {
|
|||||||
const internal = BB_INDICATOR.internalParams;
|
const internal = BB_INDICATOR.internalParams;
|
||||||
|
|
||||||
// First, aggregate the base data into the desired timeframe for the BB calculation.
|
// 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);
|
const aggregatedData = aggregateCandles(data, timeframe);
|
||||||
if (aggregatedData.length === 0) {
|
if (aggregatedData.length === 0) {
|
||||||
return { bb1_upper: [], bb1_lower: [], bb2_upper: [], bb2_lower: [], bb3_upper: [], bb3_lower: [] };
|
return { bb1_upper: [], bb1_lower: [], bb2_upper: [], bb2_lower: [], bb3_upper: [], bb3_lower: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate each set of bands using the aggregated data.
|
// 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 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 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);
|
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) {
|
if (bandData.length > 0) {
|
||||||
const lastPoint = bandData[bandData.length - 1];
|
const lastPoint = bandData[bandData.length - 1];
|
||||||
if (lastPoint.time < lastCandleTime) {
|
if (lastPoint.time < lastCandleTime) {
|
||||||
// Push a new point to extend the horizontal line to the end of the chart.
|
bandData.push({ ...lastPoint, time: lastCandleTime });
|
||||||
bandData.push({ time: lastCandleTime, value: lastPoint.value });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bandData;
|
return bandData;
|
||||||
@ -63,57 +60,32 @@ function calculateFullBollingerBands(data, params) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to calculate a single set of upper and lower Bollinger Bands.
|
* 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
|
* @returns {{upper: Array<Object>, lower: Array<Object>}}
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
function calculateBands(data, upperLength, upperStdDev, lowerLength, lowerStdDev) {
|
function calculateBands(data, upperLength, upperStdDev, lowerLength, lowerStdDev) {
|
||||||
const upperBand = [];
|
const upperBand = [];
|
||||||
const lowerBand = [];
|
const lowerBand = [];
|
||||||
|
|
||||||
// --- Calculate Upper Band ---
|
// Calculate Upper Band
|
||||||
for (let i = upperLength - 1; i < data.length; i++) {
|
for (let i = upperLength - 1; i < data.length; i++) {
|
||||||
const slice = data.slice(i - upperLength + 1, i + 1);
|
const slice = data.slice(i - upperLength + 1, i + 1);
|
||||||
const sma = slice.reduce((sum, candle) => sum + candle.close, 0) / upperLength;
|
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 stdDev = Math.sqrt(slice.reduce((sum, candle) => sum + Math.pow(candle.close - sma, 2), 0) / upperLength);
|
||||||
|
upperBand.push({
|
||||||
const newTime = slice[slice.length - 1].time;
|
time: slice[slice.length - 1].time,
|
||||||
const newValue = sma + (stdDev * upperStdDev);
|
value: 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 ---
|
// Calculate Lower Band
|
||||||
for (let i = lowerLength - 1; i < data.length; i++) {
|
for (let i = lowerLength - 1; i < data.length; i++) {
|
||||||
const slice = data.slice(i - lowerLength + 1, i + 1);
|
const slice = data.slice(i - lowerLength + 1, i + 1);
|
||||||
const sma = slice.reduce((sum, candle) => sum + candle.close, 0) / lowerLength;
|
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 stdDev = Math.sqrt(slice.reduce((sum, candle) => sum + Math.pow(candle.close - sma, 2), 0) / lowerLength);
|
||||||
|
lowerBand.push({
|
||||||
const newTime = slice[slice.length - 1].time;
|
time: slice[slice.length - 1].time,
|
||||||
const newValue = sma - (stdDev * lowerStdDev);
|
value: 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 };
|
return { upper: upperBand, lower: lowerBand };
|
||||||
|
|||||||
@ -6,25 +6,25 @@
|
|||||||
* @returns {Object} A manager object with public methods to control indicators.
|
* @returns {Object} A manager object with public methods to control indicators.
|
||||||
*/
|
*/
|
||||||
function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef) {
|
function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef) {
|
||||||
|
// --- FIX: --- Added `debounceTimerId` to each slot object to track pending updates.
|
||||||
const indicatorSlots = [
|
const indicatorSlots = [
|
||||||
{ id: 1, cellId: 'indicator-cell-1', 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 },
|
{ 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 },
|
{ 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 },
|
{ 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 = {
|
const colors = {
|
||||||
bb: {
|
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)',
|
bb2_upper: 'rgba(128, 25, 34, 0.75)',
|
||||||
bb3_upper: 'rgba(128, 25, 34, 1)', // Lowest opacity
|
bb3_upper: 'rgba(128, 25, 34, 1)',
|
||||||
bb1_lower: 'rgba(6, 95, 6, 0.5)', // Highest band, 50% opacity
|
bb1_lower: 'rgba(6, 95, 6, 0.5)',
|
||||||
bb2_lower: 'rgba(6, 95, 6, 0.75)',
|
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' },
|
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() {
|
function populateDropdowns() {
|
||||||
@ -53,12 +53,19 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
const slot = indicatorSlots.find(s => s.id === slotId);
|
const slot = indicatorSlots.find(s => s.id === slotId);
|
||||||
if (!slot) return;
|
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.forEach(s => chart.removeSeries(s));
|
||||||
slot.series = [];
|
slot.series = [];
|
||||||
slot.definition = null;
|
slot.definition = null;
|
||||||
slot.params = {};
|
slot.params = {};
|
||||||
slot.calculator = null;
|
slot.calculator = null;
|
||||||
|
|
||||||
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
||||||
controlsContainer.innerHTML = '';
|
controlsContainer.innerHTML = '';
|
||||||
|
|
||||||
@ -68,12 +75,12 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
if (!definition) return;
|
if (!definition) return;
|
||||||
|
|
||||||
slot.definition = definition;
|
slot.definition = definition;
|
||||||
|
|
||||||
definition.params.forEach(param => {
|
definition.params.forEach(param => {
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.textContent = param.label || param.name;
|
label.textContent = param.label || param.name;
|
||||||
label.style.fontSize = '12px';
|
label.style.fontSize = '12px';
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = param.type;
|
input.type = param.type;
|
||||||
input.value = param.defaultValue;
|
input.value = param.defaultValue;
|
||||||
@ -82,13 +89,14 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
input.className = 'input-field';
|
input.className = 'input-field';
|
||||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||||
|
|
||||||
let debounceTimer;
|
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
clearTimeout(debounceTimer);
|
// --- FIX: --- Use the slot's `debounceTimerId` property to manage the timeout.
|
||||||
debounceTimer = setTimeout(() => {
|
clearTimeout(slot.debounceTimerId);
|
||||||
|
slot.debounceTimerId = setTimeout(() => {
|
||||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||||
updateIndicator(slot.id, true);
|
updateIndicator(slot.id, true);
|
||||||
}, 500);
|
slot.debounceTimerId = null; // Clear the ID after the function has run.
|
||||||
|
}, 500);
|
||||||
});
|
});
|
||||||
const controlGroup = document.createElement('div');
|
const controlGroup = document.createElement('div');
|
||||||
controlGroup.style.display = 'flex';
|
controlGroup.style.display = 'flex';
|
||||||
@ -103,26 +111,28 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
|
|
||||||
function updateIndicator(slotId, isFullRecalculation = false) {
|
function updateIndicator(slotId, isFullRecalculation = false) {
|
||||||
const slot = indicatorSlots.find(s => s.id === slotId);
|
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) {
|
if (isFullRecalculation) {
|
||||||
slot.series.forEach(s => chart.removeSeries(s));
|
slot.series.forEach(s => chart.removeSeries(s));
|
||||||
slot.series = [];
|
slot.series = [];
|
||||||
|
|
||||||
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
|
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
|
||||||
|
|
||||||
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
||||||
Object.keys(indicatorResult).forEach(key => {
|
Object.keys(indicatorResult).forEach(key => {
|
||||||
const seriesData = indicatorResult[key];
|
const seriesData = indicatorResult[key];
|
||||||
const indicatorNameLower = slot.definition.name.toLowerCase();
|
const indicatorNameLower = slot.definition.name.toLowerCase();
|
||||||
const series = chart.addLineSeries({
|
const series = chart.addLineSeries({
|
||||||
color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
|
color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
|
||||||
lineWidth: 1, // **FIX**: Set line width to 1px
|
lineWidth: 1,
|
||||||
title: '', // **FIX**: Remove title label
|
title: '',
|
||||||
lastValueVisible: false, // **FIX**: Remove price label on the right
|
lastValueVisible: false,
|
||||||
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
priceLineVisible: false,
|
||||||
});
|
});
|
||||||
series.setData(seriesData);
|
series.setData(seriesData);
|
||||||
slot.series.push(series);
|
slot.series.push(series);
|
||||||
@ -130,53 +140,69 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
} else {
|
} else {
|
||||||
const series = chart.addLineSeries({
|
const series = chart.addLineSeries({
|
||||||
color: colors.default[slot.id - 1],
|
color: colors.default[slot.id - 1],
|
||||||
lineWidth: 1, // **FIX**: Set line width to 1px
|
lineWidth: 1,
|
||||||
title: '', // **FIX**: Remove title label
|
title: '',
|
||||||
lastValueVisible: false, // **FIX**: Remove price label on the right
|
lastValueVisible: false,
|
||||||
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
priceLineVisible: false,
|
||||||
});
|
});
|
||||||
series.setData(indicatorResult);
|
series.setData(indicatorResult);
|
||||||
slot.series.push(series);
|
slot.series.push(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slot.definition.createRealtime) {
|
if (slot.definition.createRealtime) {
|
||||||
slot.calculator = slot.definition.createRealtime(slot.params);
|
slot.calculator = slot.definition.createRealtime(slot.params);
|
||||||
slot.calculator.prime(candleDataForCalc);
|
slot.calculator.prime(candleDataForCalc);
|
||||||
}
|
}
|
||||||
} else if (slot.calculator) {
|
} else if (slot.calculator) {
|
||||||
// **FIX**: This is the lightweight real-time update logic
|
|
||||||
const lastCandle = candleDataForCalc[candleDataForCalc.length - 1];
|
const lastCandle = candleDataForCalc[candleDataForCalc.length - 1];
|
||||||
if (!lastCandle) return;
|
if (!lastCandle) return;
|
||||||
|
|
||||||
const newPoint = slot.calculator.update(lastCandle);
|
const newPoint = slot.calculator.update(lastCandle);
|
||||||
|
|
||||||
if (newPoint && typeof newPoint === 'object') {
|
if (newPoint && typeof newPoint === 'object') {
|
||||||
if (slot.series.length > 1) { // Multi-line indicator
|
if (slot.series.length > 1) { // Multi-line indicator
|
||||||
Object.keys(newPoint).forEach((key, index) => {
|
Object.keys(newPoint).forEach((key, index) => {
|
||||||
if (slot.series[index] && newPoint[key]) {
|
if (slot.series[index] && newPoint[key]) {
|
||||||
slot.series[index].update(newPoint[key]);
|
slot.series[index].update(newPoint[key]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (slot.series.length === 1) { // Single-line indicator
|
} else if (slot.series.length === 1) { // Single-line indicator
|
||||||
slot.series[0].update(newPoint);
|
slot.series[0].update(newPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalculateAllAfterHistory(baseData, displayedData) {
|
function recalculateAllAfterHistory(baseData, displayedData) {
|
||||||
baseCandleDataRef = baseData;
|
baseCandleDataRef = baseData;
|
||||||
displayedCandleDataRef = displayedData;
|
displayedCandleDataRef = displayedData;
|
||||||
indicatorSlots.forEach(slot => {
|
|
||||||
if (slot.definition) updateIndicator(slot.id, true);
|
// --- 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() {
|
function updateAllOnNewCandle() {
|
||||||
indicatorSlots.forEach(slot => {
|
indicatorSlots.forEach(slot => {
|
||||||
if (slot.definition) {
|
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 {
|
return {
|
||||||
populateDropdowns,
|
populateDropdowns,
|
||||||
recalculateAllAfterHistory,
|
recalculateAllAfterHistory,
|
||||||
updateAllOnNewCandle, // Expose the new function
|
updateAllOnNewCandle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,12 @@
|
|||||||
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||||
|
|
||||||
<!-- Indicator & Aggregation Scripts -->
|
<!-- NOTE: These 'url_for' will not work in a static HTML file. -->
|
||||||
|
<!-- They are placeholders for a Flask environment. For a standalone file, you would link directly to the JS files. -->
|
||||||
<script src="{{ url_for('static', filename='candle-aggregator.js') }}"></script>
|
<script src="{{ url_for('static', filename='candle-aggregator.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='sma.js') }}"></script>
|
<script src="{{ url_for('static', filename='sma.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='ema.js') }}"></script>
|
<script src="{{ url_for('static', filename='ema.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='bb.js') }}"></script>
|
<script src="{{ url_for('static',filename='bb.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='hurst.js') }}"></script>
|
<script src="{{ url_for('static', filename='hurst.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='indicators.js') }}"></script>
|
<script src="{{ url_for('static', filename='indicators.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='indicator-manager.js') }}"></script>
|
<script src="{{ url_for('static', filename='indicator-manager.js') }}"></script>
|
||||||
@ -58,7 +59,7 @@
|
|||||||
.action-button:hover, .control-cell select:hover { background-color: var(--button-hover-bg); }
|
.action-button:hover, .control-cell select:hover { background-color: var(--button-hover-bg); }
|
||||||
.input-field { width: 60px; }
|
.input-field { width: 60px; }
|
||||||
#candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); }
|
#candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); }
|
||||||
#timeframe-select { margin-top: 10px; }
|
#timeframe-display { margin-top: 10px; min-width: 60px; }
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
width: 80%; height: 4px; background-color: var(--button-bg);
|
width: 80%; height: 4px; background-color: var(--button-bg);
|
||||||
border-radius: 2px; margin-top: 10px; overflow: hidden;
|
border-radius: 2px; margin-top: 10px; overflow: hidden;
|
||||||
@ -70,37 +71,41 @@
|
|||||||
|
|
||||||
/* --- Styles for Measure Tool --- */
|
/* --- Styles for Measure Tool --- */
|
||||||
#measure-tool {
|
#measure-tool {
|
||||||
position: absolute;
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
top: 0;
|
pointer-events: none; overflow: hidden; z-index: 10;
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
#measure-box {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
#measure-svg {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
#measure-box { position: absolute; }
|
||||||
|
#measure-svg { position: absolute; width: 100%; height: 100%; top: 0; left: 0; }
|
||||||
#measure-tooltip {
|
#measure-tooltip {
|
||||||
position: absolute;
|
position: absolute; color: var(--measure-tool-text); padding: 4px 8px;
|
||||||
color: var(--measure-tool-text);
|
border-radius: 4px; font-size: 11px; line-height: 1.2;
|
||||||
padding: 4px 8px;
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.2;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Styles for Timeframe Modal --- */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
justify-content: center; align-items: center; z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background-color: var(--container-dark); padding: 25px; border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color); text-align: center;
|
||||||
|
width: 300px; box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.modal h2 {
|
||||||
|
margin-top: 0; margin-bottom: 20px; font-size: 18px; color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.modal .input-field {
|
||||||
|
width: 100%; box-sizing: border-box; font-size: 24px;
|
||||||
|
padding: 10px; margin-bottom: 10px; text-align: center;
|
||||||
|
}
|
||||||
|
.modal #timeframe-preview-text {
|
||||||
|
color: var(--text-secondary); font-size: 14px; margin-top: 0;
|
||||||
|
margin-bottom: 20px; min-height: 20px;
|
||||||
|
}
|
||||||
|
.modal .action-button { width: 100%; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -121,14 +126,8 @@
|
|||||||
<div class="control-cell">
|
<div class="control-cell">
|
||||||
<h3>Candle Closes In</h3>
|
<h3>Candle Closes In</h3>
|
||||||
<div id="candle-timer">--:--</div>
|
<div id="candle-timer">--:--</div>
|
||||||
<select id="timeframe-select">
|
<div id="timeframe-display" class="action-button">1m</div>
|
||||||
<option value="1">1m</option>
|
<div id="progress-container" class="progress-bar-container">
|
||||||
<option value="2">2m</option>
|
|
||||||
<option value="3">3m</option>
|
|
||||||
<option value="4">4m</option>
|
|
||||||
<option value="5">5m</option>
|
|
||||||
</select>
|
|
||||||
<div id="progress-container" class="progress-bar-container">
|
|
||||||
<div class="progress-bar"></div>
|
<div class="progress-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,6 +137,16 @@
|
|||||||
<div class="control-cell" id="indicator-cell-4"></div>
|
<div class="control-cell" id="indicator-cell-4"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeframe Modal -->
|
||||||
|
<div id="timeframe-modal-overlay" class="modal-overlay">
|
||||||
|
<div id="timeframe-modal" class="modal">
|
||||||
|
<h2>Change interval</h2>
|
||||||
|
<input type="number" id="timeframe-input" class="input-field" min="1" placeholder="Enter minutes"/>
|
||||||
|
<p id="timeframe-preview-text"></p>
|
||||||
|
<button id="timeframe-confirm-btn" class="action-button">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
const chartElement = document.getElementById('chart');
|
const chartElement = document.getElementById('chart');
|
||||||
@ -145,19 +154,27 @@
|
|||||||
width: chartElement.clientWidth, height: 500,
|
width: chartElement.clientWidth, height: 500,
|
||||||
layout: { background: { type: 'solid', color: '#1E222D' }, textColor: '#D1D4DC' },
|
layout: { background: { type: 'solid', color: '#1E222D' }, textColor: '#D1D4DC' },
|
||||||
grid: { vertLines: { color: '#2A2E39' }, horzLines: { color: '#2A2E39' } },
|
grid: { vertLines: { color: '#2A2E39' }, horzLines: { color: '#2A2E39' } },
|
||||||
timeScale: { timeVisible: true, secondsVisible: true }
|
timeScale: { timeVisible: true, secondsVisible: true },
|
||||||
|
crosshair: {
|
||||||
|
mode: LightweightCharts.CrosshairMode.Normal,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const candlestickSeries = chart.addCandlestickSeries({
|
const candlestickSeries = chart.addCandlestickSeries({
|
||||||
|
|
||||||
upColor: 'rgba(255, 152, 0, 1.0)', downColor: 'rgba(255, 152, 0, 0.66)', borderDownColor: 'rgba(255, 152, 0, 0.66)',
|
upColor: 'rgba(255, 152, 0, 1.0)', downColor: 'rgba(255, 152, 0, 0.66)', borderDownColor: 'rgba(255, 152, 0, 0.66)',
|
||||||
borderUpColor: 'rgba(255, 152, 0, 1.0)', wickDownColor: 'rgba(255, 152, 0, 0.66)', wickUpColor: 'rgba(255, 152, 0, 1.0)'
|
borderUpColor: 'rgba(255, 152, 0, 1.0)', wickDownColor: 'rgba(255, 152, 0, 0.66)', wickUpColor: 'rgba(255, 152, 0, 1.0)'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const originalAddLineSeries = chart.addLineSeries;
|
||||||
|
chart.addLineSeries = function(options) {
|
||||||
|
const newOptions = { ...options, crosshairMarkerVisible: false, };
|
||||||
|
return originalAddLineSeries.call(this, newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
let baseCandleData1m = [];
|
let baseCandleData1m = [];
|
||||||
let displayedCandleData = [];
|
let displayedCandleData = [];
|
||||||
let manager;
|
let manager;
|
||||||
|
let currentTimeframeMinutes = 1;
|
||||||
|
|
||||||
const timeframeSelect = document.getElementById('timeframe-select');
|
|
||||||
const candleTimerDiv = document.getElementById('candle-timer');
|
const candleTimerDiv = document.getElementById('candle-timer');
|
||||||
const chartTitle = document.getElementById('chart-title');
|
const chartTitle = document.getElementById('chart-title');
|
||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
@ -170,6 +187,61 @@
|
|||||||
let measureState = { active: false, finished: false, startPoint: null, endPoint: null };
|
let measureState = { active: false, finished: false, startPoint: null, endPoint: null };
|
||||||
let isRedrawScheduled = false;
|
let isRedrawScheduled = false;
|
||||||
|
|
||||||
|
const timeframeDisplay = document.getElementById('timeframe-display');
|
||||||
|
const modalOverlay = document.getElementById('timeframe-modal-overlay');
|
||||||
|
const modalInput = document.getElementById('timeframe-input');
|
||||||
|
const modalPreviewText = document.getElementById('timeframe-preview-text');
|
||||||
|
const modalConfirmBtn = document.getElementById('timeframe-confirm-btn');
|
||||||
|
|
||||||
|
function openModal(initialValue = '') {
|
||||||
|
modalOverlay.style.display = 'flex';
|
||||||
|
modalInput.value = initialValue;
|
||||||
|
updatePreviewText();
|
||||||
|
modalInput.focus();
|
||||||
|
modalInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modalOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreviewText() {
|
||||||
|
const value = modalInput.value;
|
||||||
|
if (value && parseInt(value) > 0) {
|
||||||
|
modalPreviewText.textContent = `${value} minute${parseInt(value) > 1 ? 's' : ''}`;
|
||||||
|
} else {
|
||||||
|
modalPreviewText.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmTimeframe() {
|
||||||
|
const newTimeframe = parseInt(modalInput.value);
|
||||||
|
if (newTimeframe && newTimeframe > 0) {
|
||||||
|
currentTimeframeMinutes = newTimeframe;
|
||||||
|
timeframeDisplay.textContent = `${newTimeframe}m`;
|
||||||
|
updateChartForTimeframe(true);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeframeDisplay.addEventListener('click', () => openModal(currentTimeframeMinutes));
|
||||||
|
modalConfirmBtn.addEventListener('click', confirmTimeframe);
|
||||||
|
modalInput.addEventListener('input', updatePreviewText);
|
||||||
|
modalInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') confirmTimeframe(); });
|
||||||
|
modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeModal(); });
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (modalOverlay.style.display === 'flex' && e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
else if (modalOverlay.style.display !== 'flex' && !isNaN(parseInt(e.key)) && !e.ctrlKey && !e.metaKey) {
|
||||||
|
const activeEl = document.activeElement;
|
||||||
|
if (activeEl.tagName !== 'INPUT' && activeEl.tagName !== 'TEXTAREA') {
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(e.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
manager = createIndicatorManager(chart, baseCandleData1m, displayedCandleData);
|
manager = createIndicatorManager(chart, baseCandleData1m, displayedCandleData);
|
||||||
manager.populateDropdowns();
|
manager.populateDropdowns();
|
||||||
|
|
||||||
@ -191,37 +263,142 @@
|
|||||||
setTimeout(() => { progressContainer.style.display = 'none'; }, 500);
|
setTimeout(() => { progressContainer.style.display = 'none'; }, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('candle_update', (candle) => candlestickSeries.update(candle));
|
// --- MODIFICATION START: Rewritten candle update and creation logic ---
|
||||||
|
function handleLiveUpdate(update) {
|
||||||
|
if (baseCandleData1m.length === 0 || displayedCandleData.length === 0) return;
|
||||||
|
|
||||||
|
// First, ensure the base 1m data is up-to-date.
|
||||||
|
const lastBaseCandle = baseCandleData1m[baseCandleData1m.length - 1];
|
||||||
|
if (update.time > lastBaseCandle.time) {
|
||||||
|
baseCandleData1m.push(update);
|
||||||
|
} else if (update.time === lastBaseCandle.time) {
|
||||||
|
baseCandleData1m[baseCandleData1m.length - 1] = update;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candleDurationSeconds = currentTimeframeMinutes * 60;
|
||||||
|
let lastDisplayedCandle = displayedCandleData[displayedCandleData.length - 1];
|
||||||
|
|
||||||
|
// Check if the update belongs to the currently forming displayed candle
|
||||||
|
if (update.time >= lastDisplayedCandle.time && update.time < lastDisplayedCandle.time + candleDurationSeconds) {
|
||||||
|
// It does, so just update the High, Low, and Close prices
|
||||||
|
lastDisplayedCandle.high = Math.max(lastDisplayedCandle.high, update.high);
|
||||||
|
lastDisplayedCandle.low = Math.min(lastDisplayedCandle.low, update.low);
|
||||||
|
lastDisplayedCandle.close = update.close;
|
||||||
|
candlestickSeries.update(lastDisplayedCandle);
|
||||||
|
} else if (update.time >= lastDisplayedCandle.time + candleDurationSeconds) {
|
||||||
|
// This update is for a NEW candle.
|
||||||
|
const newCandleTime = Math.floor(update.time / candleDurationSeconds) * candleDurationSeconds;
|
||||||
|
|
||||||
|
// Create the new candle. Its O,H,L,C are all from this first tick.
|
||||||
|
const newCandle = {
|
||||||
|
time: newCandleTime,
|
||||||
|
open: update.open,
|
||||||
|
high: update.high,
|
||||||
|
low: update.low,
|
||||||
|
close: update.close,
|
||||||
|
};
|
||||||
|
|
||||||
|
displayedCandleData.push(newCandle);
|
||||||
|
candlestickSeries.update(newCandle);
|
||||||
|
|
||||||
|
// Since a new candle has been formed, we should recalculate indicators
|
||||||
|
manager.recalculateAllAfterHistory(baseCandleData1m, displayedCandleData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestCandleUpdate = null;
|
||||||
|
let isUpdateScheduled = false;
|
||||||
|
|
||||||
|
function processLatestUpdate() {
|
||||||
|
if (latestCandleUpdate) {
|
||||||
|
handleLiveUpdate(latestCandleUpdate);
|
||||||
|
latestCandleUpdate = null;
|
||||||
|
}
|
||||||
|
isUpdateScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('candle_update', (update) => {
|
||||||
|
latestCandleUpdate = update;
|
||||||
|
if (!isUpdateScheduled) {
|
||||||
|
isUpdateScheduled = true;
|
||||||
|
requestAnimationFrame(processLatestUpdate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('candle_closed', (closedCandle) => {
|
socket.on('candle_closed', (closedCandle) => {
|
||||||
const lastBaseCandle = baseCandleData1m.length > 0 ? baseCandleData1m[baseCandleData1m.length - 1] : null;
|
// This handler's primary job is to ensure data integrity by using the final, closed 1m candle.
|
||||||
if (lastBaseCandle && lastBaseCandle.time === closedCandle.time) {
|
|
||||||
baseCandleData1m[baseCandleData1m.length - 1] = closedCandle;
|
// 1. Update the master 1-minute data array with the final version of the candle.
|
||||||
|
const candleIndex = baseCandleData1m.findIndex(c => c.time === closedCandle.time);
|
||||||
|
if (candleIndex !== -1) {
|
||||||
|
baseCandleData1m[candleIndex] = closedCandle;
|
||||||
} else {
|
} else {
|
||||||
|
// This case might happen if connection was lost and we missed updates for this candle
|
||||||
baseCandleData1m.push(closedCandle);
|
baseCandleData1m.push(closedCandle);
|
||||||
|
baseCandleData1m.sort((a, b) => a.time - b.time); // Keep it sorted just in case
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayedCandleData.length === 0) return;
|
||||||
|
|
||||||
|
// 2. Determine which displayed candle this closed 1m candle belongs to.
|
||||||
|
const candleDurationSeconds = currentTimeframeMinutes * 60;
|
||||||
|
const bucketTime = Math.floor(closedCandle.time / candleDurationSeconds) * candleDurationSeconds;
|
||||||
|
|
||||||
|
// 3. Find the displayed candle that needs to be corrected with final data.
|
||||||
|
const displayedCandleToUpdate = displayedCandleData.find(c => c.time === bucketTime);
|
||||||
|
if (!displayedCandleToUpdate) {
|
||||||
|
console.warn("Could not find a displayed candle to update for closed 1m candle at", new Date(closedCandle.time * 1000).toISOString());
|
||||||
|
// As a fallback, a full redraw can fix inconsistencies.
|
||||||
|
// updateChartForTimeframe(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Find all 1m source candles for this bucket.
|
||||||
|
const sourceCandles = baseCandleData1m.filter(c =>
|
||||||
|
c.time >= bucketTime && c.time < bucketTime + candleDurationSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. If we have source candles, aggregate them to get the CORRECT final data.
|
||||||
|
if (sourceCandles.length > 0) {
|
||||||
|
const finalCandle = {
|
||||||
|
time: bucketTime,
|
||||||
|
open: sourceCandles[0].open,
|
||||||
|
high: Math.max(...sourceCandles.map(c => c.high)),
|
||||||
|
low: Math.min(...sourceCandles.map(c => c.low)),
|
||||||
|
close: sourceCandles[sourceCandles.length - 1].close
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. Update the specific candle in the displayed data array
|
||||||
|
const displayedIndex = displayedCandleData.findIndex(c => c.time === bucketTime);
|
||||||
|
if (displayedIndex !== -1) {
|
||||||
|
displayedCandleData[displayedIndex] = finalCandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Update the series on the chart and recalculate indicators for accuracy.
|
||||||
|
candlestickSeries.update(finalCandle);
|
||||||
|
manager.recalculateAllAfterHistory(baseCandleData1m, displayedCandleData);
|
||||||
}
|
}
|
||||||
updateChartForTimeframe(false);
|
|
||||||
});
|
});
|
||||||
|
// --- MODIFICATION END ---
|
||||||
|
|
||||||
function updateChartForTimeframe(isFullReset = false) {
|
function updateChartForTimeframe(isFullReset = false) {
|
||||||
const selectedIntervalMinutes = parseInt(timeframeSelect.value, 10);
|
|
||||||
if (baseCandleData1m.length === 0) return;
|
if (baseCandleData1m.length === 0) return;
|
||||||
const visibleRange = isFullReset ? null : chart.timeScale().getVisibleLogicalRange();
|
const visibleTimeRange = isFullReset ? null : chart.timeScale().getVisibleTimeRange();
|
||||||
const newCandleData = aggregateCandles(baseCandleData1m, selectedIntervalMinutes);
|
const newCandleData = aggregateCandles(baseCandleData1m, currentTimeframeMinutes);
|
||||||
|
|
||||||
if (newCandleData.length > 0) {
|
if (newCandleData.length > 0) {
|
||||||
displayedCandleData = newCandleData;
|
displayedCandleData = newCandleData;
|
||||||
candlestickSeries.setData(displayedCandleData);
|
candlestickSeries.setData(displayedCandleData);
|
||||||
chartTitle.textContent = `{{ symbol }} Chart (${selectedIntervalMinutes}m)`;
|
chartTitle.textContent = `{{ symbol }} Chart (${currentTimeframeMinutes}m)`;
|
||||||
manager.recalculateAllAfterHistory(baseCandleData1m, displayedCandleData);
|
manager.recalculateAllAfterHistory(baseCandleData1m, displayedCandleData);
|
||||||
if (visibleRange) chart.timeScale().setVisibleLogicalRange(visibleRange);
|
if (visibleTimeRange) {
|
||||||
else chart.timeScale().fitContent();
|
chart.timeScale().setVisibleRange(visibleTimeRange);
|
||||||
|
} else {
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeframeSelect.addEventListener('change', () => updateChartForTimeframe(true));
|
|
||||||
|
|
||||||
// --- Measure Tool Logic ---
|
|
||||||
function clearMeasureTool() {
|
function clearMeasureTool() {
|
||||||
measureState = { active: false, finished: false, startPoint: null, endPoint: null };
|
measureState = { active: false, finished: false, startPoint: null, endPoint: null };
|
||||||
measureToolEl.style.display = 'none';
|
measureToolEl.style.display = 'none';
|
||||||
@ -241,7 +418,6 @@
|
|||||||
|
|
||||||
function drawMeasureTool() {
|
function drawMeasureTool() {
|
||||||
if (!measureState.startPoint || !measureState.endPoint) return;
|
if (!measureState.startPoint || !measureState.endPoint) return;
|
||||||
|
|
||||||
const startCoord = {
|
const startCoord = {
|
||||||
x: chart.timeScale().timeToCoordinate(measureState.startPoint.time),
|
x: chart.timeScale().timeToCoordinate(measureState.startPoint.time),
|
||||||
y: candlestickSeries.priceToCoordinate(measureState.startPoint.price)
|
y: candlestickSeries.priceToCoordinate(measureState.startPoint.price)
|
||||||
@ -250,72 +426,52 @@
|
|||||||
x: chart.timeScale().timeToCoordinate(measureState.endPoint.time),
|
x: chart.timeScale().timeToCoordinate(measureState.endPoint.time),
|
||||||
y: candlestickSeries.priceToCoordinate(measureState.endPoint.price)
|
y: candlestickSeries.priceToCoordinate(measureState.endPoint.price)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (startCoord.x === null || startCoord.y === null || endCoord.x === null || endCoord.y === null) {
|
if (startCoord.x === null || startCoord.y === null || endCoord.x === null || endCoord.y === null) {
|
||||||
measureToolEl.style.display = 'none';
|
measureToolEl.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
measureToolEl.style.display = 'block';
|
measureToolEl.style.display = 'block';
|
||||||
|
|
||||||
const isUp = measureState.endPoint.price >= measureState.startPoint.price;
|
const isUp = measureState.endPoint.price >= measureState.startPoint.price;
|
||||||
const boxColor = isUp ? 'var(--measure-tool-up-bg)' : 'var(--measure-tool-down-bg)';
|
const boxColor = isUp ? 'var(--measure-tool-up-bg)' : 'var(--measure-tool-down-bg)';
|
||||||
const borderColor = isUp ? 'var(--measure-tool-up-border)' : 'var(--measure-tool-down-border)';
|
const borderColor = isUp ? 'var(--measure-tool-up-border)' : 'var(--measure-tool-down-border)';
|
||||||
|
|
||||||
measureBoxEl.style.backgroundColor = boxColor;
|
measureBoxEl.style.backgroundColor = boxColor;
|
||||||
measureBoxEl.style.borderColor = borderColor;
|
measureBoxEl.style.borderColor = borderColor;
|
||||||
measureTooltipEl.style.backgroundColor = borderColor;
|
measureTooltipEl.style.backgroundColor = borderColor;
|
||||||
|
|
||||||
const minX = Math.min(startCoord.x, endCoord.x);
|
const minX = Math.min(startCoord.x, endCoord.x);
|
||||||
const maxX = Math.max(startCoord.x, endCoord.x);
|
const maxX = Math.max(startCoord.x, endCoord.x);
|
||||||
const minY = Math.min(startCoord.y, endCoord.y);
|
const minY = Math.min(startCoord.y, endCoord.y);
|
||||||
const maxY = Math.max(startCoord.y, endCoord.y);
|
const maxY = Math.max(startCoord.y, endCoord.y);
|
||||||
|
|
||||||
measureBoxEl.style.left = `${minX}px`;
|
measureBoxEl.style.left = `${minX}px`;
|
||||||
measureBoxEl.style.top = `${minY}px`;
|
measureBoxEl.style.top = `${minY}px`;
|
||||||
measureBoxEl.style.width = `${maxX - minX}px`;
|
measureBoxEl.style.width = `${maxX - minX}px`;
|
||||||
measureBoxEl.style.height = `${maxY - minY}px`;
|
measureBoxEl.style.height = `${maxY - minY}px`;
|
||||||
|
|
||||||
const midX = minX + (maxX - minX) / 2;
|
const midX = minX + (maxX - minX) / 2;
|
||||||
const midY = minY + (maxY - minY) / 2;
|
const midY = minY + (maxY - minY) / 2;
|
||||||
|
|
||||||
const arrowSize = 5;
|
const arrowSize = 5;
|
||||||
const isTimeGoingForward = measureState.endPoint.time >= measureState.startPoint.time;
|
const isTimeGoingForward = measureState.endPoint.time >= measureState.startPoint.time;
|
||||||
|
let hArrowPoints = isTimeGoingForward
|
||||||
let hArrowPoints;
|
? `${maxX - arrowSize},${midY - arrowSize} ${maxX},${midY} ${maxX - arrowSize},${midY + arrowSize}`
|
||||||
if (isTimeGoingForward) {
|
: `${minX + arrowSize},${midY - arrowSize} ${minX},${midY} ${minX + arrowSize},${midY + arrowSize}`;
|
||||||
hArrowPoints = `${maxX - arrowSize},${midY - arrowSize} ${maxX},${midY} ${maxX - arrowSize},${midY + arrowSize}`;
|
let vArrowPoints = isUp
|
||||||
} else {
|
? `${midX - arrowSize},${minY + arrowSize} ${midX},${minY} ${midX + arrowSize},${minY + arrowSize}`
|
||||||
hArrowPoints = `${minX + arrowSize},${midY - arrowSize} ${minX},${midY} ${minX + arrowSize},${midY + arrowSize}`;
|
: `${midX - arrowSize},${maxY - arrowSize} ${midX},${maxY} ${midX + arrowSize},${maxY - arrowSize}`;
|
||||||
}
|
|
||||||
|
|
||||||
let vArrowPoints;
|
|
||||||
if (isUp) {
|
|
||||||
vArrowPoints = `${midX - arrowSize},${minY + arrowSize} ${midX},${minY} ${midX + arrowSize},${minY + arrowSize}`;
|
|
||||||
} else {
|
|
||||||
vArrowPoints = `${midX - arrowSize},${maxY - arrowSize} ${midX},${maxY} ${midX + arrowSize},${maxY - arrowSize}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
measureSvgEl.innerHTML = `
|
measureSvgEl.innerHTML = `
|
||||||
<line x1="${minX}" y1="${midY}" x2="${maxX}" y2="${midY}" stroke="${borderColor}" stroke-width="1"/>
|
<line x1="${minX}" y1="${midY}" x2="${maxX}" y2="${midY}" stroke="${borderColor}" stroke-width="1"/>
|
||||||
<polygon points="${hArrowPoints}" fill="${borderColor}" />
|
<polygon points="${hArrowPoints}" fill="${borderColor}" />
|
||||||
<line x1="${midX}" y1="${minY}" x2="${midX}" y2="${maxY}" stroke="${borderColor}" stroke-width="1"/>
|
<line x1="${midX}" y1="${minY}" x2="${midX}" y2="${maxY}" stroke="${borderColor}" stroke-width="1"/>
|
||||||
<polygon points="${vArrowPoints}" fill="${borderColor}" />
|
<polygon points="${vArrowPoints}" fill="${borderColor}" />
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const priceChange = measureState.endPoint.price - measureState.startPoint.price;
|
const priceChange = measureState.endPoint.price - measureState.startPoint.price;
|
||||||
const pctChange = (priceChange / measureState.startPoint.price) * 100;
|
const pctChange = (priceChange / measureState.startPoint.price) * 100;
|
||||||
const timeDifference = measureState.endPoint.time - measureState.startPoint.time;
|
const timeDifference = measureState.endPoint.time - measureState.startPoint.time;
|
||||||
const bars = Math.round(timeDifference / (parseInt(timeframeSelect.value, 10) * 60));
|
const bars = Math.round(timeDifference / (currentTimeframeMinutes * 60));
|
||||||
const duration = formatDuration(Math.abs(timeDifference));
|
const duration = formatDuration(Math.abs(timeDifference));
|
||||||
|
|
||||||
measureTooltipEl.innerHTML = `
|
measureTooltipEl.innerHTML = `
|
||||||
<div>${priceChange.toFixed(2)} (${pctChange.toFixed(2)}%)</div>
|
<div>${priceChange.toFixed(2)} (${pctChange.toFixed(2)}%)</div>
|
||||||
<div>${bars} bars, ${duration}</div>
|
<div>${bars} bars, ${duration}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
measureTooltipEl.style.left = `${midX}px`;
|
measureTooltipEl.style.left = `${midX}px`;
|
||||||
const tooltipMargin = 8;
|
const tooltipMargin = 8;
|
||||||
|
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
measureTooltipEl.style.top = `${minY}px`;
|
measureTooltipEl.style.top = `${minY}px`;
|
||||||
measureTooltipEl.style.transform = `translate(-50%, calc(-100% - ${tooltipMargin}px))`;
|
measureTooltipEl.style.transform = `translate(-50%, calc(-100% - ${tooltipMargin}px))`;
|
||||||
@ -339,32 +495,26 @@
|
|||||||
chart.unsubscribeCrosshairMove(onMeasureMove);
|
chart.unsubscribeCrosshairMove(onMeasureMove);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- High-performance scaling logic for price axis drag ---
|
|
||||||
let priceScaleDragState = { isDragging: false };
|
let priceScaleDragState = { isDragging: false };
|
||||||
|
|
||||||
function priceScaleRedrawLoop() {
|
function priceScaleRedrawLoop() {
|
||||||
if (!priceScaleDragState.isDragging) return;
|
if (!priceScaleDragState.isDragging) return;
|
||||||
drawMeasureTool();
|
drawMeasureTool();
|
||||||
requestAnimationFrame(priceScaleRedrawLoop);
|
requestAnimationFrame(priceScaleRedrawLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
chartElement.addEventListener('mousedown', (e) => {
|
chartElement.addEventListener('mousedown', (e) => {
|
||||||
if (!measureState.finished) return;
|
if (!measureState.finished) return;
|
||||||
const rect = chartElement.getBoundingClientRect();
|
const rect = chartElement.getBoundingClientRect();
|
||||||
const priceScaleWidth = chart.priceScale('right').width();
|
const priceScaleWidth = chart.priceScale('right').width();
|
||||||
const chartWidth = rect.width;
|
if (e.clientX > rect.left + rect.width - priceScaleWidth) {
|
||||||
if (e.clientX > rect.left + chartWidth - priceScaleWidth) {
|
|
||||||
priceScaleDragState.isDragging = true;
|
priceScaleDragState.isDragging = true;
|
||||||
requestAnimationFrame(priceScaleRedrawLoop);
|
requestAnimationFrame(priceScaleRedrawLoop);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('mouseup', () => {
|
window.addEventListener('mouseup', () => {
|
||||||
if (priceScaleDragState.isDragging) {
|
if (priceScaleDragState.isDragging) {
|
||||||
priceScaleDragState.isDragging = false;
|
priceScaleDragState.isDragging = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// --- End high-performance scaling logic ---
|
|
||||||
|
|
||||||
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
||||||
if (measureState.finished) {
|
if (measureState.finished) {
|
||||||
@ -387,31 +537,25 @@
|
|||||||
clearMeasureTool();
|
clearMeasureTool();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!param.point || !param.sourceEvent.shiftKey) return;
|
if (!param.point || !param.sourceEvent.shiftKey) return;
|
||||||
|
|
||||||
if (measureState.active) {
|
if (measureState.active) {
|
||||||
onMeasureUp();
|
onMeasureUp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMeasureTool();
|
clearMeasureTool();
|
||||||
|
|
||||||
const time = chart.timeScale().coordinateToTime(param.point.x);
|
const time = chart.timeScale().coordinateToTime(param.point.x);
|
||||||
const price = candlestickSeries.coordinateToPrice(param.point.y);
|
const price = candlestickSeries.coordinateToPrice(param.point.y);
|
||||||
if (!time || !price) return;
|
if (!time || !price) return;
|
||||||
|
|
||||||
measureState.startPoint = { time, price };
|
measureState.startPoint = { time, price };
|
||||||
measureState.endPoint = { time, price };
|
measureState.endPoint = { time, price };
|
||||||
measureState.active = true;
|
measureState.active = true;
|
||||||
measureToolEl.style.display = 'block';
|
measureToolEl.style.display = 'block';
|
||||||
drawMeasureTool();
|
drawMeasureTool();
|
||||||
|
|
||||||
chart.subscribeCrosshairMove(onMeasureMove);
|
chart.subscribeCrosshairMove(onMeasureMove);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const selectedIntervalSeconds = parseInt(timeframeSelect.value, 10) * 60;
|
const selectedIntervalSeconds = currentTimeframeMinutes * 60;
|
||||||
const now = new Date().getTime() / 1000;
|
const now = new Date().getTime() / 1000;
|
||||||
const secondsRemaining = Math.floor(selectedIntervalSeconds - (now % selectedIntervalSeconds));
|
const secondsRemaining = Math.floor(selectedIntervalSeconds - (now % selectedIntervalSeconds));
|
||||||
const minutes = Math.floor(secondsRemaining / 60);
|
const minutes = Math.floor(secondsRemaining / 60);
|
||||||
|
|||||||
Reference in New Issue
Block a user