class Indicator_Output { constructor(name) { this.legend = {}; } create_legend(name, chart, lineSeries) { // Create legend div and append it to the output element let target_div = document.getElementById('indicator_output'); this.legend[name] = document.createElement('div'); this.legend[name].className = 'legend'; this.legend[name].style.opacity = 0.1; // Initially mostly transparent this.legend[name].style.transition = 'opacity 1s ease-out'; // Smooth transition for fade-out target_div.appendChild(this.legend[name]); this.legend[name].style.display = 'block'; this.legend[name].style.left = 3 + 'px'; this.legend[name].style.top = 3 + 'px'; // subscribe set legend text to crosshair moves chart.subscribeCrosshairMove((param) => { this.set_legend_text(param.seriesPrices.get(lineSeries), name); }); } set_legend_text(priceValue, name) { // Ensure the legend for the name exists if (!this.legend[name]) { console.warn(`Legend for ${name} not found, skipping legend update.`); return; } // Callback assigned to fire on crosshair movements. let val = 'n/a'; if (priceValue !== undefined) { val = (Math.round(priceValue * 100) / 100).toFixed(2); } // Update legend text this.legend[name].innerHTML = `${name} ${val}`; // Make legend fully visible this.legend[name].style.opacity = 1; this.legend[name].style.display = 'block'; // Set a timeout to fade out the legend after 3 seconds clearTimeout(this.legend[name].fadeTimeout); // Clear any previous timeout to prevent conflicts this.legend[name].fadeTimeout = setTimeout(() => { this.legend[name].style.opacity = 0.1; // Gradually fade out // Set another timeout to hide the element after the fade-out transition setTimeout(() => { this.legend[name].style.display = 'none'; }, 1000); // Wait for the fade-out transition to complete (1s) }, 1000); } clear_legend(name) { // Remove the legend div from the DOM for (const key in this.legend) { if (key.startsWith(name)) { this.legend[key].remove(); // Remove the legend from the DOM delete this.legend[key]; // Remove the reference from the object } } } } iOutput = new Indicator_Output(); // Create a global map to store the mappings const indicatorMap = new Map(); class Indicator { constructor(name) { // The name of the indicator. this.name = name; this.lines = []; this.hist = []; this.outputs = []; } static getIndicatorConfig() { return { args: ['name'], class: this }; } init(data) { console.log(this.name + ': init() unimplemented.'); } update(data) { console.log(this.name + ': update() unimplemented.'); } addHist(name, chart, color = '#26a69a') { this.hist[name] = chart.addHistogramSeries({ color: color, priceFormat: { type: 'price', }, priceScaleId: 'volume_ps', scaleMargins: { top: 0, bottom: 0, }, }); } addLine(name, chart, color, lineWidth) { this.lines[name] = chart.addLineSeries({ color: color, lineWidth: lineWidth }); // Initialise the crosshair legend for the charts with a unique name for each line. iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]); } setLine(lineName, data, value_name) { console.log('indicators[68]: setLine takes:(lineName, data, value_name)'); console.log(lineName, data, value_name); let priceValue; // Check if the data is a multi-value object if (typeof data === 'object' && data !== null && value_name in data) { // Multi-value indicator: Extract the array for the specific key const processedData = data[value_name]; // Set the data for the line this.lines[lineName].setData(processedData); // Isolate the last value provided and round to 2 decimal places priceValue = processedData.at(-1).value; // Update the display and legend for multi-value indicators this.updateDisplay(lineName, { [value_name]: priceValue }, 'value'); } else { // Single-value indicator: Initialize the data directly this.lines[lineName].setData(data); // Isolate the last value provided and round to 2 decimal places priceValue = data.at(-1).value; // Update the display and legend for single-value indicators this.updateDisplay(lineName, priceValue, value_name); } iOutput.set_legend_text(priceValue, `${this.name}_${lineName}`); } updateDisplay(name, priceValue, value_name) { let element = document.getElementById(this.name + '_' + value_name); if (element) { if (typeof priceValue === 'object' && priceValue !== null) { // Handle multiple values by joining them into a single string with labels let currentValues = element.value ? element.value.split(', ').reduce((acc, pair) => { let [key, val] = pair.split(': '); if (!isNaN(parseFloat(val))) { acc[key] = parseFloat(val); } return acc; }, {}) : {}; // Update current values with the new key-value pairs Object.assign(currentValues, priceValue); // Set the updated values back to the element element.value = Object.entries(currentValues) .filter(([key, value]) => !isNaN(value)) // Skip NaN values .map(([key, value]) => `${key}: ${(Math.round(value * 100) / 100).toFixed(2)}`) .join(', '); // Use comma for formatting } else { // Handle simple values as before element.value = (Math.round(priceValue * 100) / 100).toFixed(2); } // Adjust the element styling dynamically for wrapping and height element.style.height = 'auto'; // Reset height element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content } else { console.warn(`Element with ID ${this.name}_${value_name} not found.`); } } setHist(name, data) { this.hist[name].setData(data); } updateLine(name, data, value_name) { console.log('indicators[68]: updateLine takes:(name, data, value_name)'); console.log(name, data, value_name); // Check if the data is a multi-value object if (typeof data === 'object' && data !== null && value_name in data) { // Multi-value indicator: Extract the array for the specific key const processedData = data[value_name]; // Update the line-set data in the chart this.lines[name].update(processedData); // Isolate the last value provided and round to 2 decimal places const priceValue = processedData.at(-1).value; // Update the display and legend for multi-value indicators this.updateDisplay(name, { [value_name]: priceValue }, 'value'); iOutput.set_legend_text(priceValue, `${this.name}_${name}`); } else { // Single-value indicator: Initialize the data directly this.lines[name].update(data); // Isolate the last value provided and round to 2 decimal places const priceValue = data.at(-1).value; // Update the display and legend for single-value indicators this.updateDisplay(name, priceValue, value_name); iOutput.set_legend_text(priceValue, `${this.name}_${name}`); } } updateHist(name, data) { this.hist[name].update(data); } removeFromChart(chart) { // Ensure the chart object is passed if (!chart) { console.error("Chart object is missing."); return; } // Remove all line series associated with this indicator for (let lineName in this.lines) { if (this.lines[lineName]) { chart.removeSeries(this.lines[lineName]); delete this.lines[lineName]; } } // Remove all histogram series associated with this indicator for (let histName in this.hist) { if (this.hist[histName]) { chart.removeSeries(this.hist[histName]); delete this.hist[histName]; } } // Remove the legend from the crosshair (if any) if (iOutput.legend[this.name]) { iOutput.legend[this.name].remove(); delete iOutput.legend[this.name]; } } } class SMA extends Indicator { constructor(name, chart, color, lineWidth = 2) { super(name); this.addLine('line', chart, color, lineWidth); this.outputs = ['value']; } static getIndicatorConfig() { return { args: ['name', 'chart_1', 'color'], class: this }; } init(data) { this.setLine('line', data, 'value'); console.log('line data', data); } update(data) { this.updateLine('line', data[0], 'value'); } } // Register SMA in the map indicatorMap.set("SMA", SMA); class Linear_Regression extends SMA { // Inherits getIndicatorConfig from SMA } indicatorMap.set("LREG", Linear_Regression); class EMA extends SMA { // Inherits getIndicatorConfig from SMA } indicatorMap.set("EMA", EMA); class RSI extends Indicator { constructor(name, charts, color, lineWidth = 2) { super(name); if (!charts.hasOwnProperty('chart2')) { charts.create_RSI_chart(); } let chart = charts.chart2; this.addLine('line', chart, color, lineWidth); this.outputs = ['value']; } static getIndicatorConfig() { return { args: ['name', 'charts', 'color'], class: this }; } init(data) { this.setLine('line', data, 'value'); } update(data) { this.updateLine('line', data[0], 'value'); } } indicatorMap.set("RSI", RSI); class BollingerPercentB extends Indicator { constructor(name, charts, color, lineWidth = 2) { super(name); if (!charts.hasOwnProperty('chart4')) { charts.create_PercentB_chart(); } let chart = charts.chart4; this.addLine('line', chart, color, lineWidth); this.outputs = ['value']; } static getIndicatorConfig() { return { args: ['name', 'charts', 'color'], class: this }; } init(data) { this.setLine('line', data, 'value'); } update(data) { this.updateLine('line', data[0], 'value'); } } indicatorMap.set("BOL%B", BollingerPercentB); class MACD extends Indicator { constructor(name, charts, color_1, color_2, lineWidth = 2) { super(name); if (!charts.hasOwnProperty('chart3')) { charts.create_MACD_chart(); } let chart = charts.chart3; this.addLine('line_m', chart, color_1, lineWidth); this.addLine('line_s', chart, color_2, lineWidth); this.addHist(name, chart); this.outputs = ['macd', 'signal', 'hist']; } static getIndicatorConfig() { return { args: ['name', 'charts', 'color_1', 'color_2'], class: this }; } init(data) { // Filter out rows where macd, signal, or hist are null const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null); if (filteredData.length > 0) { // Prepare the filtered data for the MACD line const line_m = filteredData.map(row => ({ time: row.time, value: row.macd })); // Set the 'line_m' for the MACD line this.setLine('line_m', { macd: line_m }, 'macd'); // Prepare the filtered data for the signal line const line_s = filteredData.map(row => ({ time: row.time, value: row.signal })); // Set the 'line_s' for the signal line this.setLine('line_s', { signal: line_s }, 'signal'); // Set the histogram data this.setHist(this.name, filteredData.map(row => ({ time: row.time, value: row.hist }))); } else { console.error('No valid MACD data found.'); } } update(data) { // Filter out rows where macd, signal, or hist are null const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null); if (filteredData.length > 0) { // Update the 'macd' line const line_m = filteredData.map(row => ({ time: row.time, value: row.macd })); this.updateLine('line_m', { macd: line_m }, 'macd'); // Update the 'signal' line const line_s = filteredData.map(row => ({ time: row.time, value: row.signal })); this.updateLine('line_s', { signal: line_s }, 'signal'); // Update the 'hist' (histogram) bar const hist_data = filteredData.map(row => ({ time: row.time, value: row.hist })); this.updateHist('hist', hist_data); } else { console.error('No valid MACD data found for update.'); } } } indicatorMap.set("MACD", MACD); class ATR extends Indicator { // Inherits getIndicatorConfig from Indicator init(data) { this.updateDisplay(this.name, data.at(-1).value, 'value'); this.outputs = ['value']; } update(data) { this.updateDisplay(this.name, data[0].value, 'value'); } } indicatorMap.set("ATR", ATR); class Volume extends Indicator { constructor(name, chart) { super(name); this.addHist(name, chart); this.hist[name].applyOptions({ scaleMargins: { top: 0.95, bottom: 0.0 } }); this.outputs = ['value']; } static getIndicatorConfig() { return { args: ['name', 'chart_1'], class: this }; } init(data) { this.setHist(this.name, data); } update(data) { this.updateHist(this.name, data[0]); } } indicatorMap.set("Volume", Volume); class Bolenger extends Indicator { constructor(name, chart, color_1, color_2, color_3, lineWidth = 2) { super(name); this.addLine('line_u', chart, color_1, lineWidth); this.addLine('line_m', chart, color_2, lineWidth); this.addLine('line_l', chart, color_3, lineWidth); this.outputs = ['upper', 'middle', 'lower']; } static getIndicatorConfig() { return { args: ['name', 'chart_1', 'color_1', 'color_2', 'color_3'], class: this }; } init(data) { // Filter out rows where upper, middle, or lower are null const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null); if (filteredData.length > 0) { // Set the 'line_u' for the upper line const line_u = filteredData.map(row => ({ time: row.time, value: row.upper })); this.setLine('line_u', { upper: line_u }, 'upper'); // Set the 'line_m' for the middle line const line_m = filteredData.map(row => ({ time: row.time, value: row.middle })); this.setLine('line_m', { middle: line_m }, 'middle'); // Set the 'line_l' for the lower line const line_l = filteredData.map(row => ({ time: row.time, value: row.lower })); this.setLine('line_l', { lower: line_l }, 'lower'); } else { console.error('No valid data found for init.'); } } update(data) { // Filter out rows where upper, middle, or lower are null const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null); if (filteredData.length > 0) { // Update the 'upper' line const line_u = filteredData.map(row => ({ time: row.time, value: row.upper })); this.updateLine('line_u', { upper: line_u }, 'upper'); // Update the 'middle' line const line_m = filteredData.map(row => ({ time: row.time, value: row.middle })); this.updateLine('line_m', { middle: line_m }, 'middle'); // Update the 'lower' line const line_l = filteredData.map(row => ({ time: row.time, value: row.lower })); this.updateLine('line_l', { lower: line_l }, 'lower'); } else { console.error('No valid data found for update.'); } } } indicatorMap.set("BOLBands", Bolenger); class Indicators { constructor(comms) { // Contains instantiated indicators. this.i_objs = {}; this.comms = comms; } create_indicators(indicators, charts) { for (let name in indicators) { if (!indicators[name].visible) continue; let i_type = indicators[name].type; let IndicatorClass = indicatorMap.get(i_type); if (IndicatorClass) { let { args, class: IndicatorConstructor } = IndicatorClass.getIndicatorConfig(); let preparedArgs = args.map(arg => { if (arg === 'name') return name; if (arg === 'charts') return charts; if (arg === 'chart_1') return charts.chart_1; if (arg === 'color') return indicators[name].color; if (arg === 'color_1') return indicators[name].color_1 || 'red'; if (arg === 'color_2') return indicators[name].color_2 || 'white'; if (arg === 'color_3') return indicators[name].color_3 || 'blue'; }); this.i_objs[name] = new IndicatorConstructor(...preparedArgs); } else { console.error(`Unknown indicator type: ${i_type}`); } } } // Method to retrieve outputs for each indicator getIndicatorOutputs() { let indicatorOutputs = {}; for (let name in this.i_objs) { if (this.i_objs[name].outputs) { indicatorOutputs[name] = this.i_objs[name].outputs; } else { indicatorOutputs[name] = ['value']; // Default output if not defined } } return indicatorOutputs; } addToCharts(charts, idata){ /* Receives indicator data, creates and stores the indicator objects, then inserts the data into the charts. */ if (idata.indicators && Object.keys(idata.indicators).length > 0) { this.create_indicators(idata.indicators, charts); // Initialize each indicator with the data directly if (idata.indicator_data && Object.keys(idata.indicator_data).length > 0) { this.init_indicators(idata.indicator_data); } else { console.warn('Indicator data is empty. No indicators to initialize.'); } } else { console.log('No indicators defined for this user.'); } } init_indicators(data){ // Loop through all the indicators. for (name in data){ // Call the initialization function for each indicator. if (!this.i_objs[name]){ console.log('could not load:', name); continue; } this.i_objs[name].init(data[name]); } // Set up the visibility form event handler const form = document.getElementById('indicator-form'); // Add a submit event listener to the form form.addEventListener('submit', this.handleFormSubmit.bind(this)); } // This updates all the indicator data (re-initializes with full dataset) update(updates){ for (let name in updates){ if (window.UI.indicators.i_objs[name]) { // Use init() to refresh with full data on candle close window.UI.indicators.i_objs[name].init(updates[name]); } else { console.warn(`Indicator "${name}" not found in i_objs, skipping update`); } } } deleteIndicator(indicator, event) { this.comms.deleteIndicator(indicator).then(response => { if (response.success) { const indicatorElement = event.target.closest('.indicator-row'); indicatorElement.remove(); // Remove from DOM // Remove the indicator from the chart and legend if (this.i_objs[indicator]) { let chart; // Determine which chart the indicator is on, based on its type if (indicator.includes('RSI')) { chart = window.UI.charts.chart2; // Assume RSI is on chart2 } else if (indicator.includes('MACD')) { chart = window.UI.charts.chart3; // Assume MACD is on chart3 } else if (indicator.includes('%B') || indicator.includes('PercentB')) { chart = window.UI.charts.chart4; // %B is on chart4 } else { chart = window.UI.charts.chart_1; // Default to the main chart } // Pass the correct chart object when removing the indicator this.i_objs[indicator].removeFromChart(chart); // Remove the indicator object from i_objs delete this.i_objs[indicator]; // Optionally: Clear the legend entry iOutput.clear_legend(indicator); } } else { alert('Failed to delete the indicator.'); } }); } // This updates a specific indicator updateIndicator(event) { event.preventDefault(); // Prevent default form submission behavior const row = event.target.closest('.indicator-row'); const inputs = row.querySelectorAll('input, select'); // Gather the indicator name from the row const nameDiv = row.querySelector('div:nth-child(2)'); // Second