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
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)); // 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 { alert('Failed to update indicators.'); } }).catch(error => { console.error('Error:', error); alert('An error occurred while updating the indicators.'); }); } }