From d2c777f0d497744016b6db616293cb954fb2b00f Mon Sep 17 00:00:00 2001 From: ditus Date: Mon, 14 Jul 2025 10:33:53 +0000 Subject: [PATCH] Upload files to "static" init --- static/candle-aggregator.js | 50 +++++++++ static/ema.js | 32 ++++++ static/indicator-manager.js | 199 ++++++++++++++++++++++++++++++++++++ static/indicators.js | 13 +++ static/sma.js | 28 +++++ 5 files changed, 322 insertions(+) create mode 100644 static/candle-aggregator.js create mode 100644 static/ema.js create mode 100644 static/indicator-manager.js create mode 100644 static/indicators.js create mode 100644 static/sma.js diff --git a/static/candle-aggregator.js b/static/candle-aggregator.js new file mode 100644 index 0000000..ec74dd6 --- /dev/null +++ b/static/candle-aggregator.js @@ -0,0 +1,50 @@ +/** + * 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 => { + // Calculate the timestamp for the start of the interval bucket + const bucketTimestamp = candle.time - (candle.time % 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; +} diff --git a/static/ema.js b/static/ema.js new file mode 100644 index 0000000..080100d --- /dev/null +++ b/static/ema.js @@ -0,0 +1,32 @@ +/** + * Indicator Definition Object for EMA. + */ +const EMA_INDICATOR = { + name: 'EMA', + label: 'Exponential Moving Average', + usesBaseData: false, // This simple indicator uses the chart's currently displayed data + params: [ + { name: 'period', type: 'number', defaultValue: 20, min: 2 }, + ], + calculateFull: calculateFullEMA, +}; + +function calculateFullEMA(data, params) { + const period = params.period; + if (!data || data.length < period) return []; + let emaData = []; + const multiplier = 2 / (period + 1); + let sum = 0; + for (let i = 0; i < period; i++) { + sum += data[i].close; + } + let prevEma = sum / period; + emaData.push({ time: data[period - 1].time, value: prevEma }); + for (let i = period; i < data.length; i++) { + const close = data[i].close; + const ema = (close - prevEma) * multiplier + prevEma; + emaData.push({ time: data[i].time, value: ema }); + prevEma = ema; + } + return emaData; +} diff --git a/static/indicator-manager.js b/static/indicator-manager.js new file mode 100644 index 0000000..1d2ef1d --- /dev/null +++ b/static/indicator-manager.js @@ -0,0 +1,199 @@ +/** + * Creates and manages all indicator-related logic for the chart. + * @param {Object} chart - The Lightweight Charts instance. + * @param {Array} baseCandleData - A reference to the array holding the chart's BASE 1m candle data. + * @returns {Object} A manager object with public methods to control indicators. + */ +function createIndicatorManager(chart, baseCandleData) { + // This holds the candle data currently displayed on the chart (e.g., 5m, 10m) + let currentAggregatedData = []; + + // Defines the 4 slots available in the UI for indicators. + const indicatorSlots = [ + { id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {} }, + { id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} }, + { id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {} }, + { id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} }, + ]; + + // Pre-defined colors for the indicator lines. + const colors = { + bb1: { upper: '#FF9800', lower: '#FF9800' }, // Orange + bb2: { upper: '#2196F3', lower: '#2196F3' }, // Blue + bb3: { upper: '#9C27B0', lower: '#9C27B0' }, // Purple + default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] // Fallback colors for other indicators + }; + + /** + * Populates the dropdown menus in each indicator cell. + */ + function populateDropdowns() { + indicatorSlots.forEach(slot => { + const cell = document.getElementById(slot.cellId); + if (!cell) return; + + const select = document.createElement('select'); + select.innerHTML = ``; + AVAILABLE_INDICATORS.forEach(ind => { + select.innerHTML += ``; + }); + + const controlsContainer = document.createElement('div'); + controlsContainer.className = 'indicator-controls'; + + cell.innerHTML = ''; // Clear previous content + cell.appendChild(select); + cell.appendChild(controlsContainer); + + select.addEventListener('change', (e) => { + const indicatorName = e.target.value; + loadIndicator(slot.id, indicatorName); + }); + }); + } + + /** + * Loads a new indicator into a specified slot. + * @param {number} slotId - The ID of the slot (1-4). + * @param {string} indicatorName - The name of the indicator to load (e.g., 'SMA'). + */ + function loadIndicator(slotId, indicatorName) { + const slot = indicatorSlots.find(s => s.id === slotId); + if (!slot) return; + + // Clean up any previous indicator series in this slot + slot.series.forEach(s => chart.removeSeries(s)); + slot.series = []; + + slot.definition = null; + slot.params = {}; + + const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`); + controlsContainer.innerHTML = ''; + + if (!indicatorName) return; + + const definition = AVAILABLE_INDICATORS.find(ind => ind.name === indicatorName); + if (!definition) return; + + slot.definition = definition; + + // Create UI controls for the indicator's parameters + 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; + if (param.min !== undefined) input.min = param.min; + if (param.step !== undefined) input.step = param.step; + input.className = 'input-field'; + input.placeholder = param.name; + slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value; + + let debounceTimer; + input.addEventListener('input', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value; + updateIndicator(slot.id); + }, 500); + }); + const controlGroup = document.createElement('div'); + controlGroup.style.display = 'flex'; + controlGroup.style.flexDirection = 'column'; + controlGroup.appendChild(label); + controlGroup.appendChild(input); + controlsContainer.appendChild(controlGroup); + }); + + updateIndicator(slot.id); + } + + /** + * Recalculates and redraws the lines for a specific indicator. + * @param {number} slotId - The ID of the slot to update. + */ + function updateIndicator(slotId) { + const slot = indicatorSlots.find(s => s.id === slotId); + + const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : currentAggregatedData; + + if (!slot || !slot.definition || candleDataForCalc.length === 0) { + return; + } + + // Clean up previous series before creating new ones + slot.series.forEach(s => chart.removeSeries(s)); + slot.series = []; + + console.log(`Recalculating ${slot.definition.name} for slot ${slot.id} on ${candleDataForCalc.length} candles.`); + + const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params); + + // Handle multi-line indicators like Bollinger Bands + if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) { + Object.keys(indicatorResult).forEach(key => { + const seriesData = indicatorResult[key]; + const bandName = key.split('_')[0]; + const bandType = key.split('_')[1]; + + const series = chart.addLineSeries({ + color: colors[bandName] ? colors[bandName][bandType] : colors.default[slot.id - 1], + lineWidth: 2, + title: `${slot.definition.label} - ${key}`, + lastValueVisible: false, + priceLineVisible: false, + }); + series.setData(seriesData); + slot.series.push(series); + }); + } else { // Handle single-line indicators like SMA/EMA + const series = chart.addLineSeries({ + color: colors.default[slot.id - 1], + lineWidth: 2, + title: slot.definition.label, + }); + series.setData(indicatorResult); + slot.series.push(series); + } + } + + /** + * Internal function to recalculate all active indicators. + */ + function recalculateAllIndicators() { + indicatorSlots.forEach(slot => { + if (slot.definition) { + updateIndicator(slot.id); + } + }); + } + + /** + * Sets the candle data for indicators and triggers a full recalculation. + * @param {Array} aggregatedCandleData - The candle data for the currently selected timeframe. + */ + function recalculateAllAfterHistory(aggregatedCandleData) { + currentAggregatedData = aggregatedCandleData; + recalculateAllIndicators(); + } + + /** + * Updates all indicators in response to a new candle closing. + * @param {Array} aggregatedCandleData - The latest candle data for the currently selected timeframe. + */ + function updateIndicatorsOnNewCandle(aggregatedCandleData) { + currentAggregatedData = aggregatedCandleData; + recalculateAllIndicators(); + } + + // Public API for the manager + return { + populateDropdowns, + recalculateAllAfterHistory, + updateIndicatorsOnNewCandle + }; +} diff --git a/static/indicators.js b/static/indicators.js new file mode 100644 index 0000000..81c38a2 --- /dev/null +++ b/static/indicators.js @@ -0,0 +1,13 @@ +/** + * This file acts as a central registry for all available indicators. + * To add a new indicator to the chart UI: + * 1. Create the indicator's JS file (e.g., rsi.js) with its definition object. + * 2. Make sure that file is loaded in index.html BEFORE this one. + * 3. Add the indicator's definition object (e.g., RSI_INDICATOR) to this array. + */ +const AVAILABLE_INDICATORS = [ + SMA_INDICATOR, + EMA_INDICATOR, + BB_INDICATOR, // Added the new Bollinger Bands indicator + // Add other indicators here, e.g., RSI_INDICATOR +]; \ No newline at end of file diff --git a/static/sma.js b/static/sma.js new file mode 100644 index 0000000..28a1c25 --- /dev/null +++ b/static/sma.js @@ -0,0 +1,28 @@ +/** + * Indicator Definition Object for SMA. + */ +const SMA_INDICATOR = { + name: 'SMA', + label: 'Simple Moving Average', + usesBaseData: false, // This simple indicator uses the chart's currently displayed data + params: [ + { name: 'period', type: 'number', defaultValue: 20, min: 2 }, + ], + calculateFull: calculateFullSMA, +}; + +function calculateFullSMA(data, params) { + const period = params.period; + if (!data || data.length < period) return []; + let smaData = []; + let sum = 0; + for (let i = 0; i < period; i++) { + sum += data[i].close; + } + smaData.push({ time: data[period - 1].time, value: sum / period }); + for (let i = period; i < data.length; i++) { + sum = sum - data[i - period].close + data[i].close; + smaData.push({ time: data[i].time, value: sum / period }); + } + return smaData; +}