brighter-trading/src/static/indicators.js

920 lines
35 KiB
JavaScript

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} <span style="color:rgba(4, 111, 232, 1)">${val}</span>`;
// 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 <div> 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 = `
<div class="property-item" style="display:flex;align-items:center;">
<button style="color:darkred;margin-right:5px;" type="button" onclick="UI.indicators.remove_prop(this)">&#10008;</button>
<span>${n}: ${v}</span>
</div>`;
// 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 <input> 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.');
});
}
}