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 // v5 API: seriesPrices renamed to seriesData, returns full data item chart.subscribeCrosshairMove((param) => { const data = param.seriesData.get(lineSeries); // Extract value from data item (could be {value} or {close} etc) const priceValue = data ? (data.value !== undefined ? data.value : data.close) : undefined; this.set_legend_text(priceValue, 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') { // v5 API: use addSeries with HistogramSeries this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, { color: color, priceFormat: { type: 'price', }, priceScaleId: 'volume_ps', }); // v5: scaleMargins must be set on the price scale, not series options // Volume should only take up bottom 30% of the chart chart.priceScale('volume_ps').applyOptions({ scaleMargins: { top: 0.7, bottom: 0, }, }); } addLine(name, chart, color, lineWidth) { // v5 API: use addSeries with LineSeries this.lines[name] = chart.addSeries(LightweightCharts.LineSeries, { 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) { 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) { // Try the old element format first (legacy chart-based display) 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 { // Try the new card-based display element const cardElement = document.getElementById(`indicator_card_value_${this.name}`); if (cardElement) { let displayValue = '--'; if (typeof priceValue === 'object' && priceValue !== null) { // For object values, get the first numeric value const values = Object.values(priceValue).filter(v => typeof v === 'number' && !isNaN(v)); if (values.length > 0) { displayValue = this._formatDisplayValue(values[0]); } } else if (typeof priceValue === 'number' && !isNaN(priceValue)) { displayValue = this._formatDisplayValue(priceValue); } cardElement.textContent = displayValue; } // Silently ignore if neither element exists (may be during initialization) } } /** * Formats a numeric value for display in cards */ _formatDisplayValue(value) { if (value === null || value === undefined || isNaN(value)) return '--'; if (Math.abs(value) >= 1000) { return value.toFixed(0); } else if (Math.abs(value) >= 100) { return value.toFixed(1); } else { return value.toFixed(2); } } setHist(name, data) { this.hist[name].setData(data); } updateLine(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'); } 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); // Candlestick Pattern Indicator class // Displays pattern signals as histogram bars in a dedicated patterns chart class CandlestickPattern extends Indicator { constructor(name, charts, bullish_color, bearish_color) { super(name); // Create patterns chart if it doesn't exist if (!charts.hasOwnProperty('chart5') || !charts.chart5) { charts.create_patterns_chart(); } let chart = charts.chart5; // Store colors for pattern signals (hex format for color picker compatibility) this.bullish_color = bullish_color || '#00C853'; this.bearish_color = bearish_color || '#FF5252'; // Add histogram series for this pattern this.addPatternHist(name, chart); this.outputs = ['value']; } static getIndicatorConfig() { return { args: ['name', 'charts', 'bullish_color', 'bearish_color'], class: this }; } addPatternHist(name, chart) { // v5 API: use addSeries with HistogramSeries this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, { priceFormat: { type: 'price', }, priceScaleId: 'right', }); // Create legend for the pattern iOutput.create_legend(`${this.name}_pattern`, chart, this.hist[name]); } init(data) { // Transform data to add colors based on signal value // Keep ALL data points (including zeros) to maintain proper time scale alignment // Zero values will have no visible bar (height = 0) but the timestamp is needed const histData = data.map(d => ({ time: d.time, value: d.value, // Use bullish/bearish colors for signals, gray for no signal (won't be visible anyway) color: d.value > 0 ? this.bullish_color : d.value < 0 ? this.bearish_color : '#808080' })); this.setHist(this.name, histData); // Update display with latest signal const latestValue = data.at(-1).value; let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None'; this.updateDisplay(this.name, displayText, 'value'); } update(data) { // Transform data to add colors based on signal value // Keep ALL data points to maintain time scale alignment with other charts const histData = data.map(d => ({ time: d.time, value: d.value, color: d.value > 0 ? this.bullish_color : d.value < 0 ? this.bearish_color : '#808080' })); this.setHist(this.name, histData); // Update display with latest signal const latestValue = data.at(-1).value; let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None'; this.updateDisplay(this.name, displayText, 'value'); } updateDisplay(name, value, value_name) { // Override to handle text display for patterns let element = document.getElementById(this.name + '_' + value_name); if (element) { element.value = value; } } } // Register all candlestick pattern types to use the same CandlestickPattern class // Single candle patterns indicatorMap.set("CDL_DOJI", CandlestickPattern); indicatorMap.set("CDL_HAMMER", CandlestickPattern); indicatorMap.set("CDL_INVERTEDHAMMER", CandlestickPattern); indicatorMap.set("CDL_SHOOTINGSTAR", CandlestickPattern); indicatorMap.set("CDL_SPINNINGTOP", CandlestickPattern); indicatorMap.set("CDL_MARUBOZU", CandlestickPattern); indicatorMap.set("CDL_DRAGONFLYDOJI", CandlestickPattern); indicatorMap.set("CDL_GRAVESTONEDOJI", CandlestickPattern); indicatorMap.set("CDL_HANGINGMAN", CandlestickPattern); indicatorMap.set("CDL_HIGHWAVE", CandlestickPattern); indicatorMap.set("CDL_LONGLEGGEDDOJI", CandlestickPattern); indicatorMap.set("CDL_LONGLINE", CandlestickPattern); indicatorMap.set("CDL_SHORTLINE", CandlestickPattern); indicatorMap.set("CDL_RICKSHAWMAN", CandlestickPattern); indicatorMap.set("CDL_TAKURI", CandlestickPattern); indicatorMap.set("CDL_BELTHOLD", CandlestickPattern); indicatorMap.set("CDL_CLOSINGMARUBOZU", CandlestickPattern); // Two candle patterns indicatorMap.set("CDL_ENGULFING", CandlestickPattern); indicatorMap.set("CDL_HARAMI", CandlestickPattern); indicatorMap.set("CDL_HARAMICROSS", CandlestickPattern); indicatorMap.set("CDL_PIERCING", CandlestickPattern); indicatorMap.set("CDL_DARKCLOUDCOVER", CandlestickPattern); indicatorMap.set("CDL_COUNTERATTACK", CandlestickPattern); indicatorMap.set("CDL_DOJISTAR", CandlestickPattern); indicatorMap.set("CDL_HOMINGPIGEON", CandlestickPattern); indicatorMap.set("CDL_KICKING", CandlestickPattern); indicatorMap.set("CDL_KICKINGBYLENGTH", CandlestickPattern); indicatorMap.set("CDL_MATCHINGLOW", CandlestickPattern); indicatorMap.set("CDL_INNECK", CandlestickPattern); indicatorMap.set("CDL_ONNECK", CandlestickPattern); indicatorMap.set("CDL_THRUSTING", CandlestickPattern); indicatorMap.set("CDL_SEPARATINGLINES", CandlestickPattern); indicatorMap.set("CDL_STICKSANDWICH", CandlestickPattern); indicatorMap.set("CDL_GAPSIDESIDEWHITE", CandlestickPattern); indicatorMap.set("CDL_2CROWS", CandlestickPattern); // Three+ candle patterns indicatorMap.set("CDL_MORNINGSTAR", CandlestickPattern); indicatorMap.set("CDL_EVENINGSTAR", CandlestickPattern); indicatorMap.set("CDL_MORNINGDOJISTAR", CandlestickPattern); indicatorMap.set("CDL_EVENINGDOJISTAR", CandlestickPattern); indicatorMap.set("CDL_3WHITESOLDIERS", CandlestickPattern); indicatorMap.set("CDL_3BLACKCROWS", CandlestickPattern); indicatorMap.set("CDL_3INSIDE", CandlestickPattern); indicatorMap.set("CDL_3OUTSIDE", CandlestickPattern); indicatorMap.set("CDL_3LINESTRIKE", CandlestickPattern); indicatorMap.set("CDL_3STARSINSOUTH", CandlestickPattern); indicatorMap.set("CDL_ABANDONEDBABY", CandlestickPattern); indicatorMap.set("CDL_ADVANCEBLOCK", CandlestickPattern); indicatorMap.set("CDL_BREAKAWAY", CandlestickPattern); indicatorMap.set("CDL_CONCEALBABYSWALL", CandlestickPattern); indicatorMap.set("CDL_IDENTICAL3CROWS", CandlestickPattern); indicatorMap.set("CDL_LADDERBOTTOM", CandlestickPattern); indicatorMap.set("CDL_MATHOLD", CandlestickPattern); indicatorMap.set("CDL_RISEFALL3METHODS", CandlestickPattern); indicatorMap.set("CDL_STALLEDPATTERN", CandlestickPattern); indicatorMap.set("CDL_TASUKIGAP", CandlestickPattern); indicatorMap.set("CDL_TRISTAR", CandlestickPattern); indicatorMap.set("CDL_UNIQUE3RIVER", CandlestickPattern); indicatorMap.set("CDL_UPSIDEGAP2CROWS", CandlestickPattern); indicatorMap.set("CDL_XSIDEGAP3METHODS", CandlestickPattern); indicatorMap.set("CDL_HIKKAKE", CandlestickPattern); indicatorMap.set("CDL_HIKKAKEMOD", CandlestickPattern); class Indicators { constructor(comms) { // Contains instantiated indicators. this.i_objs = {}; this.comms = comms; // Store indicator configurations for editing this.indicators = {}; // Store indicator data (values) for card display this.indicator_data = {}; } 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'; if (arg === 'bullish_color') return indicators[name].bullish_color || '#00C853'; if (arg === 'bearish_color') return indicators[name].bearish_color || '#FF5252'; }); 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 } } // Include external indicators (historical API data) const externalIndicators = window.UI?.signals?.externalIndicators || []; for (const extInd of externalIndicators) { // External indicators have a single 'value' output indicatorOutputs[extInd.name] = ['value']; } return indicatorOutputs; } addToCharts(charts, idata){ /* Receives indicator data, creates and stores the indicator objects, then inserts the data into the charts. */ // Always set up the visibility form event handler this._setupIndicatorForm(); // Store indicator configurations for card rendering and editing if (idata.indicators) { this.indicators = idata.indicators; } // Store indicator data (values) for card display if (idata.indicator_data) { this.indicator_data = idata.indicator_data; } 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.'); } // Render indicator cards in the UI (after init so values are available) this.renderIndicators(); } _setupIndicatorForm(){ // Set up the visibility form event handler const form = document.getElementById('indicator-form'); if (form && !form._hasSubmitListener) { form.addEventListener('submit', this.handleFormSubmit.bind(this)); form._hasSubmitListener = true; // Prevent duplicate listeners } } 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]); } } // 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]); // Update stored data and card display this.indicator_data[name] = updates[name]; this._updateCardValue(name, updates[name]); } else { console.warn(`Indicator "${name}" not found in i_objs, skipping update`); } } } /** * Updates a single indicator card's displayed value */ _updateCardValue(name, data) { const valueEl = document.getElementById(`indicator_card_value_${name}`); if (!valueEl) return; let displayValue = '--'; if (data && Array.isArray(data) && data.length > 0) { const lastPoint = data[data.length - 1]; if (lastPoint.value !== undefined && lastPoint.value !== null) { displayValue = this._formatValue(lastPoint.value); } else if (lastPoint.macd !== undefined) { displayValue = this._formatValue(lastPoint.macd); } else if (lastPoint.middle !== undefined) { displayValue = this._formatValue(lastPoint.middle); } else if (lastPoint.upper !== undefined) { displayValue = this._formatValue(lastPoint.upper); } } valueEl.textContent = displayValue; } 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 const indicatorType = this.i_objs[indicator].constructor.name; 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 if (indicatorType === 'CandlestickPattern') { chart = window.UI.charts.chart5; // Candlestick patterns on chart5 } 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
contains the name const indicatorName = nameDiv.innerText.trim(); // Get the indicator name // Initialize formObj with the name of the indicator const formObj = { name: indicatorName, visible: false, // Default value for visible (will be updated based on the checkbox input) source: {}, // Initialize the source object directly at the top level properties: {} // Initialize the properties object }; // Define an exclusion list for properties that should not be parsed as numbers const exclusionFields = ['custom_field_name']; // Add field names that should NOT be parsed as numbers // Function to check if a value contains mixed data (e.g., 'abc123') const isMixedData = (value) => /\D/.test(value) && /\d/.test(value); // Iterate over each input (text, checkbox, select) and add its name and value to formObj inputs.forEach(input => { let value = input.value; // Handle the 'visible' checkbox separately if (input.name === 'visible') { formObj.visible = input.checked; } else if (['market', 'timeframe', 'exchange'].includes(input.name)) { // Directly map inputs to source object fields formObj.source[input.name] = input.value; } else { // Check if the value should be parsed as a number if (!exclusionFields.includes(input.name) && !isMixedData(value)) { const parsedValue = parseFloat(value); value = isNaN(parsedValue) ? value : parsedValue; } else if (input.type === 'checkbox') { value = input.checked; } // Add the processed value to the properties object formObj.properties[input.name] = value; } }); // Call comms to send data to the server this.comms.updateIndicator(formObj).then(response => { if (response.success) { window.location.reload(); // This triggers a full page refresh } else { alert('Failed to update the indicator.'); } }).catch(error => { console.error('Error updating indicator:', error); alert('An unexpected error occurred while updating the indicator.'); }); } add_to_list(){ // Adds user input to a list and displays it in a HTML element. // called from html button click add property // Collect the property name and value input by the user let n = document.getElementById("new_prop_name").value.trim(); let v = document.getElementById("new_prop_value").value.trim(); // Ensure the property name and value are not empty if (!n || !v) { alert("Property name and value are required."); return; } // Converts css color name to hex if (n === 'color'){ // list of valid css colors let colours = { "aliceblue":"#f0f8ff", "antiquewhite":"#faebd7", "aqua":"#00ffff", "aquamarine":"#7fffd4", "azure":"#f0ffff", "beige":"#f5f5dc", "bisque":"#ffe4c4", "black":"#000000", "blanchedalmond":"#ffebcd", "blue":"#0000ff", "blueviolet":"#8a2be2", "brown":"#a52a2a", "burlywood":"#deb887", "cadetblue":"#5f9ea0", "chartreuse":"#7fff00", "chocolate":"#d2691e", "coral":"#ff7f50", "cornflowerblue":"#6495ed", "cornsilk":"#fff8dc", "crimson":"#dc143c", "cyan":"#00ffff", "darkblue":"#00008b", "darkcyan":"#008b8b", "darkgoldenrod":"#b8860b", "darkgray":"#a9a9a9", "darkgreen":"#006400", "darkkhaki":"#bdb76b", "darkmagenta":"#8b008b", "darkolivegreen":"#556b2f", "darkorange":"#ff8c00", "darkorchid":"#9932cc", "darkred":"#8b0000", "darksalmon":"#e9967a", "darkseagreen":"#8fbc8f", "darkslateblue":"#483d8b", "darkslategray":"#2f4f4f", "darkturquoise":"#00ced1", "darkviolet":"#9400d3", "deeppink":"#ff1493", "deepskyblue":"#00bfff", "dimgray":"#696969", "dodgerblue":"#1e90ff", "firebrick":"#b22222", "floralwhite":"#fffaf0", "forestgreen":"#228b22", "fuchsia":"#ff00ff", "gainsboro":"#dcdcdc", "ghostwhite":"#f8f8ff", "gold":"#ffd700", "goldenrod":"#daa520", "gray":"#808080", "green":"#008000", "greenyellow":"#adff2f", "honeydew":"#f0fff0", "hotpink":"#ff69b4", "indianred ":"#cd5c5c", "indigo":"#4b0082", "ivory":"#fffff0", "khaki":"#f0e68c", "lavender":"#e6e6fa", "lavenderblush":"#fff0f5", "lawngreen":"#7cfc00", "lemonchiffon":"#fffacd", "lightblue":"#add8e6", "lightcoral":"#f08080", "lightcyan":"#e0ffff", "lightgoldenrodyellow":"#fafad2", "lightgrey":"#d3d3d3", "lightgreen":"#90ee90", "lightpink":"#ffb6c1", "lightsalmon":"#ffa07a", "lightseagreen":"#20b2aa", "lightskyblue":"#87cefa", "lightslategray":"#778899", "lightsteelblue":"#b0c4de", "lightyellow":"#ffffe0", "lime":"#00ff00", "limegreen":"#32cd32", "linen":"#faf0e6", "magenta":"#ff00ff", "maroon":"#800000", "mediumaquamarine":"#66cdaa", "mediumblue":"#0000cd", "mediumorchid":"#ba55d3", "mediumpurple":"#9370d8", "mediumseagreen":"#3cb371", "mediumslateblue":"#7b68ee", "mediumspringgreen":"#00fa9a", "mediumturquoise":"#48d1cc", "mediumvioletred":"#c71585", "midnightblue":"#191970", "mintcream":"#f5fffa", "mistyrose":"#ffe4e1", "moccasin":"#ffe4b5", "navajowhite":"#ffdead", "navy":"#000080", "oldlace":"#fdf5e6", "olive":"#808000", "olivedrab":"#6b8e23", "orange":"#ffa500", "orangered":"#ff4500", "orchid":"#da70d6", "palegoldenrod":"#eee8aa", "palegreen":"#98fb98", "paleturquoise":"#afeeee", "palevioletred":"#d87093", "papayawhip":"#ffefd5", "peachpuff":"#ffdab9", "peru":"#cd853f", "pink":"#ffc0cb", "plum":"#dda0dd", "powderblue":"#b0e0e6", "purple":"#800080", "rebeccapurple":"#663399", "red":"#ff0000", "rosybrown":"#bc8f8f", "royalblue":"#4169e1", "saddlebrown":"#8b4513", "salmon":"#fa8072", "sandybrown":"#f4a460", "seagreen":"#2e8b57", "seashell":"#fff5ee", "sienna":"#a0522d", "silver":"#c0c0c0", "skyblue":"#87ceeb", "slateblue":"#6a5acd", "slategray":"#708090", "snow":"#fffafa", "springgreen":"#00ff7f", "steelblue":"#4682b4", "tan":"#d2b48c", "teal":"#008080", "thistle":"#d8bfd8", "tomato":"#ff6347", "turquoise":"#40e0d0", "violet":"#ee82ee", "wheat":"#f5deb3", "white":"#ffffff", "whitesmoke":"#f5f5f5", "yellow":"#ffff00", "yellowgreen":"#9acd32" }; // if the value is in the list of colors convert it. if (v.toLowerCase() in colours) { v = colours[v.toLowerCase()]; } } // Create a new property row with a clear button const newPropHTML = `
${n}: ${v}
`; // Insert the new property into the property list document.getElementById("new_prop_list").insertAdjacentHTML('beforeend', newPropHTML); // Update the hidden property object with the new property const propObj = JSON.parse(document.getElementById("new_prop_obj").value || '{}'); propObj[n] = v; document.getElementById("new_prop_obj").value = JSON.stringify(propObj); // Clear the input fields document.getElementById("new_prop_name").value = ''; document.getElementById("new_prop_value").value = ''; } // Function to remove a property from the list remove_prop(buttonElement) { const propertyDiv = buttonElement.parentElement; const propertyText = propertyDiv.querySelector('span').textContent; const [propName] = propertyText.split(':'); // Remove the property div from the DOM propertyDiv.remove(); // Remove the property from the hidden input object const propObj = JSON.parse(document.getElementById("new_prop_obj").value || '{}'); delete propObj[propName.trim()]; document.getElementById("new_prop_obj").value = JSON.stringify(propObj); } // Call to display Create new signal dialog. open_form() { // Show the form document.getElementById("new_ind_form").style.display = "grid"; // Prefill the form fields with the current chart data // Always use the current chart view values to ensure indicator is created // for the currently viewed exchange/symbol/timeframe const marketField = document.getElementById('ei_symbol'); const timeframeField = document.getElementById('ei_timeframe'); const exchangeField = document.getElementById('ei_exchange_name'); // Always set to current chart view values if (window.UI.data.trading_pair) { marketField.value = window.UI.data.trading_pair; } if (window.UI.data.interval) { timeframeField.value = window.UI.data.interval; } if (window.UI.data.exchange) { exchangeField.value = window.UI.data.exchange; } } // Call to hide Create new signal dialog. close_form() { document.getElementById("new_ind_form").style.display = "none"; } submit_new_i() { /* Populates a hidden with a value from another element then submits the form Used in the create indicator panel.*/ // Perform validation const name = document.getElementById('newi_name').value; const type = document.getElementById('newi_type').value; let market = document.getElementById('ei_symbol').value; const timeframe = document.getElementById('ei_timeframe').value; const exchange = document.getElementById('ei_exchange_name').value; let errorMsg = ''; if (!name) { errorMsg += 'Indicator name is required.\n'; } if (!type) { errorMsg += 'Indicator type is required.\n'; } if (!market) { errorMsg += 'Indicator market is required.\n'; } if (!timeframe) { errorMsg += 'Timeframe is required.\n'; } if (!exchange) { errorMsg += 'Exchange name is required.\n'; } if (errorMsg) { alert(errorMsg); // Display the error messages return; // Stop form submission if there are errors } // Collect and update properties const propObj = {} // Optionally add name, type, market, timeframe, and exchange to the properties if needed propObj["name"] = name; propObj["type"] = type; propObj["source"] = {'market':market,'timeframe': timeframe,'exchange':exchange}; propObj["properties"] = document.getElementById("new_prop_obj").value; // Call comms to send data to the server this.comms.submitIndicator(propObj).then(response => { if (response.success) { window.location.reload(); // This triggers a full page refresh } else { alert(response.message || 'Failed to create a new Indicator.'); } }); } // Method to handle form submission handleFormSubmit(event) { event.preventDefault(); // Prevent default form submission behavior // Get the form element const form = event.target; // Get all the checked checkboxes (indicators) const checkboxes = form.querySelectorAll('input[name="indicator"]:checked'); let selectedIndicators = []; checkboxes.forEach(function (checkbox) { selectedIndicators.push(checkbox.value); }); // Prepare the form data const formData = new FormData(form); formData.delete('indicator'); // Remove the single value (from original HTML behavior) // Append all selected indicators as a single array-like structure formData.append('indicator', JSON.stringify(selectedIndicators)); // Hide the popup immediately const popup = document.getElementById('indicators'); if (popup) { popup.style.display = 'none'; } // Show a loading overlay on the page const overlay = document.createElement('div'); overlay.id = 'loading-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;justify-content:center;align-items:center;z-index:9999;'; overlay.innerHTML = '
Updating indicators...
'; document.body.appendChild(overlay); // Send form data via AJAX (fetch) fetch(form.action, { method: form.method, body: formData }).then(response => { if (response.ok) { // Handle success (you can reload the page or update the UI) window.location.reload(); } else { // Remove overlay on error const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.remove(); alert('Failed to update indicators.'); } }).catch(error => { console.error('Error:', error); const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.remove(); alert('An error occurred while updating the indicators.'); }); } // ==================== Card Display Methods ==================== /** * Renders all indicators as icon cards in the indicators_list container */ renderIndicators() { const container = document.getElementById('indicators_list'); if (!container) return; container.innerHTML = ''; for (const [name, indicator] of Object.entries(this.indicators)) { const item = document.createElement('div'); const isVisible = indicator.visible !== false; item.className = `indicator-item ${isVisible ? 'visible' : 'hidden'}`; item.setAttribute('data-indicator-name', name); // Get indicator type symbol const symbol = this.getIndicatorSymbol(indicator.type); // Get current value from indicator_data let displayValue = '--'; const data = this.indicator_data[name]; if (data && Array.isArray(data) && data.length > 0) { const lastPoint = data[data.length - 1]; // Handle different indicator data formats if (lastPoint.value !== undefined && lastPoint.value !== null) { // Simple indicators (RSI, SMA, EMA, etc.) displayValue = this._formatValue(lastPoint.value); } else if (lastPoint.macd !== undefined) { // MACD - show the MACD value displayValue = this._formatValue(lastPoint.macd); } else if (lastPoint.middle !== undefined) { // Bollinger Bands - show middle value displayValue = this._formatValue(lastPoint.middle); } else if (lastPoint.upper !== undefined) { // Other band indicators displayValue = this._formatValue(lastPoint.upper); } } const indicatorType = indicator.type || 'Unknown'; item.innerHTML = `
${symbol}
${name}
${indicatorType}
${displayValue}
`; // Click to edit item.addEventListener('click', () => this.openEditDialog(name)); container.appendChild(item); } } /** * Formats a numeric value for display */ _formatValue(value) { if (value === null || value === undefined) return '--'; if (typeof value === 'number') { // For large numbers, show fewer decimals if (Math.abs(value) >= 1000) { return value.toFixed(0); } else if (Math.abs(value) >= 100) { return value.toFixed(1); } else { return value.toFixed(2); } } return String(value); } /** * Returns an emoji/symbol for the given indicator type */ getIndicatorSymbol(type) { // All keys are uppercase for consistent matching const symbols = { 'RSI': '📊', 'MACD': '📈', 'SMA': '〰️', 'EMA': '〰️', 'LREG': '➡️', 'BOLBANDS': '📉', 'BOL%B': '📉', 'ATR': '📏', 'VOLUME': '📊', }; if (!type) return '📈'; const upperType = type.toUpperCase(); // Check for candlestick patterns (they start with CDL_) if (upperType.startsWith('CDL_')) { return '🕯️'; } return symbols[upperType] || '📈'; // default chart emoji } /** * Opens the edit dialog for an indicator */ openEditDialog(indicatorName) { const indicator = this.indicators[indicatorName]; if (!indicator) return; const form = document.getElementById('edit_indicator_form'); if (!form) { console.error('Edit indicator form not found'); return; } // Populate form fields document.getElementById('edit_indicator_name').value = indicatorName; document.getElementById('edit_ind_display_name').value = indicatorName; document.getElementById('edit_ind_type').value = indicator.type || 'Unknown'; document.getElementById('edit_ind_visible').checked = indicator.visible !== false; // Source fields const source = indicator.source || {}; document.getElementById('edit_ind_market').value = source.market || ''; document.getElementById('edit_ind_timeframe').value = source.timeframe || ''; document.getElementById('edit_ind_exchange').value = source.exchange || ''; // Color document.getElementById('edit_ind_color').value = indicator.color || '#667eea'; // Render dynamic properties const propsContainer = document.getElementById('edit_ind_properties'); propsContainer.innerHTML = ''; // Properties to exclude from dynamic rendering const excludeProps = ['type', 'value', 'color', 'visible', 'source', 'name', 'color_1', 'color_2', 'color_3', 'bullish_color', 'bearish_color']; for (const [key, value] of Object.entries(indicator)) { if (!excludeProps.includes(key)) { const div = document.createElement('div'); div.innerHTML = ` `; propsContainer.appendChild(div); } } // Show and position the form form.style.display = 'block'; form.style.left = '50%'; form.style.top = '50%'; form.style.transform = 'translate(-50%, -50%)'; form.style.position = 'fixed'; form.style.zIndex = '1000'; } /** * Closes the edit dialog */ closeEditDialog() { const form = document.getElementById('edit_indicator_form'); if (form) { form.style.display = 'none'; } } /** * Saves changes from the edit dialog */ saveEditDialog() { const indicatorName = document.getElementById('edit_indicator_name').value; const formObj = { name: indicatorName, visible: document.getElementById('edit_ind_visible').checked, color: document.getElementById('edit_ind_color').value, source: { market: document.getElementById('edit_ind_market').value, timeframe: document.getElementById('edit_ind_timeframe').value, exchange: document.getElementById('edit_ind_exchange').value }, properties: {} }; // Gather dynamic properties const propsContainer = document.getElementById('edit_ind_properties'); propsContainer.querySelectorAll('input').forEach(input => { const value = parseFloat(input.value); formObj.properties[input.name] = isNaN(value) ? input.value : value; }); this.comms.updateIndicator(formObj).then(response => { if (response.success) { this.closeEditDialog(); // Refresh page to reload indicators with new settings window.location.reload(); } else { alert('Failed to update indicator: ' + (response.message || 'Unknown error')); } }).catch(error => { console.error('Error updating indicator:', error); alert('An error occurred while updating the indicator.'); }); } /** * Deletes an indicator card with confirmation */ deleteIndicatorCard(indicatorName) { if (!confirm(`Delete indicator "${indicatorName}"?`)) return; this.comms.deleteIndicator(indicatorName).then(response => { if (response.success) { // Remove from chart if (this.i_objs[indicatorName]) { // Determine which chart the indicator is on let chart; const indicator = this.i_objs[indicatorName]; const indicatorType = indicator.constructor.name; if (indicatorName.includes('RSI') || indicatorType === 'RSI') { chart = window.UI.charts.chart2; } else if (indicatorName.includes('MACD') || indicatorType === 'MACD') { chart = window.UI.charts.chart3; } else if (indicatorName.includes('%B') || indicatorType === 'BollingerPercentB') { chart = window.UI.charts.chart4; } else if (indicatorType === 'CandlestickPattern') { chart = window.UI.charts.chart5; } else { chart = window.UI.charts.chart_1; } indicator.removeFromChart(chart); delete this.i_objs[indicatorName]; iOutput.clear_legend(indicatorName); } // Remove from indicators object delete this.indicators[indicatorName]; // Re-render the cards this.renderIndicators(); } else { alert('Failed to delete indicator: ' + (response.message || 'Unknown error')); } }).catch(error => { console.error('Error deleting indicator:', error); alert('An error occurred while deleting the indicator.'); }); } /** * Toggles visibility of an indicator on the chart */ toggleVisibility(indicatorName) { const indicator = this.indicators[indicatorName]; if (!indicator) return; // Toggle the visibility indicator.visible = !indicator.visible; // Save to server and reload to apply chart changes this.comms.updateIndicator({ name: indicatorName, visible: indicator.visible }).then(response => { if (response.success) { // Refresh page to properly update chart display window.location.reload(); } else { // Revert the toggle if save failed indicator.visible = !indicator.visible; alert('Failed to update visibility'); } }).catch(error => { console.error('Error updating visibility:', error); indicator.visible = !indicator.visible; }); } /** * Makes a dialog draggable by its header */ _makeDialogDraggable(dialog, handle) { if (!dialog || !handle) return; let offsetX = 0, offsetY = 0, isDragging = false; handle.style.cursor = 'move'; handle.onmousedown = (e) => { if (e.target.tagName === 'BUTTON') return; // Don't drag when clicking buttons isDragging = true; offsetX = e.clientX - dialog.offsetLeft; offsetY = e.clientY - dialog.offsetTop; document.onmousemove = onMouseMove; document.onmouseup = onMouseUp; }; function onMouseMove(e) { if (!isDragging) return; dialog.style.left = (e.clientX - offsetX) + 'px'; dialog.style.top = (e.clientY - offsetY) + 'px'; dialog.style.transform = 'none'; } function onMouseUp() { isDragging = false; document.onmousemove = null; document.onmouseup = null; } } }