From 29f30cb358da41acab1880c50e053998c5a5187a Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 19 Sep 2024 17:10:26 -0300 Subject: [PATCH] Mostly working now --- src/DataCache_v3.py | 10 +- src/indicators.py | 121 ++++++++++------ src/static/communication.js | 22 +++ src/static/indicators.js | 164 +++++++++++++-------- src/templates/indicators_hud.html | 190 ++++++++++++++++++------- src/templates/new_indicator_popup.html | 133 +++++++++-------- 6 files changed, 425 insertions(+), 215 deletions(-) diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 680ff23..6de2e63 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -895,8 +895,14 @@ class DatabaseInteractions(SnapshotDataCache): if len(rows) > 1: raise ValueError(f"Multiple rows found for {filter_vals}. Please provide a more specific filter.") - # Modify the specified field - if isinstance(new_data, str): + # Ensure consistency when storing boolean data like 'visible' + if isinstance(new_data, bool): + # If storing in a system that supports booleans, you can store directly as boolean + updated_value = new_data # For cache systems that support booleans + + # If your cache or database only supports strings, convert boolean to string + # updated_value = 'true' if new_data else 'false' + elif isinstance(new_data, str): updated_value = new_data else: updated_value = json.dumps(new_data) # Convert non-string data to JSON string if necessary diff --git a/src/indicators.py b/src/indicators.py index fb321e2..481b585 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -309,7 +309,7 @@ class Indicators: :param only_enabled: bool - If True, return only indicators marked as visible. :return: dict - A dictionary of indicator names as keys and their attributes as values. """ - user_id = str(self.users.get_id(username)) + user_id = self.users.get_id(username) if not user_id: raise ValueError(f"Invalid user_name: {username}") @@ -317,7 +317,7 @@ class Indicators: # Fetch indicators based on visibility status if only_enabled: indicators_df = self.cache_manager.get_rows_from_datacache('indicators', - [('creator', user_id), ('visible', str(1))]) + [('creator', user_id), ('visible', True)]) else: indicators_df = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id)]) @@ -333,10 +333,15 @@ class Indicators: properties = row.get('properties', {}) properties = json.loads(properties) if isinstance(properties, str) else properties + # Deserialize the 'source' field if it is a JSON string + source = row.get('source', {}) + source = json.loads(source) if isinstance(source, str) else source + # Construct the result dictionary for each indicator result[row['name']] = { 'type': row['kind'], - 'visible': row['visible'], + 'visible': bool(row['visible']), + 'source': source, **properties # Merge in all properties from the properties field } @@ -344,24 +349,24 @@ class Indicators: def toggle_indicators(self, user_id: int, indicator_names: list) -> None: """ - Set the visibility of indicators for a user. + Set the visibility of indicators for a user. - :param user_id: The id of the user. - :param indicator_names: List of indicator names to set as visible. - :return: None + :param user_id: The id of the user. + :param indicator_names: List of indicator names to set as visible. + :return: None """ indicators = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id)]) - # Validate inputs + if indicators.empty: return # Set visibility for all indicators off self.cache_manager.modify_datacache_item('indicators', [('creator', user_id)], - field_name='visible', new_data=0, overwrite='name') + field_name='visible', new_data=False, overwrite='name') # Set visibility for the specified indicators on self.cache_manager.modify_datacache_item('indicators', [('creator', user_id), ('name', indicator_names)], - field_name='visible', new_data=1, overwrite='name') + field_name='visible', new_data=True, overwrite='name') def edit_indicator(self, user_name: str, params: dict): """ @@ -374,27 +379,58 @@ class Indicators: raise ValueError("Indicator name is required for editing.") # Get the indicator from the user's indicator list - user_id = str(self.users.get_id(user_name)) + user_id = self.users.get_id(user_name) indicator = self.cache_manager.get_rows_from_datacache('indicators', [('name', indicator_name), ('creator', user_id)]) if indicator.empty: raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.") - # Modify indicator. - self.cache_manager.modify_datacache_item('indicators', - [('creator', user_id), ('name', indicator_name)], - field_name='properties', new_data=params.get('properties'), - overwrite='name') + # Compare existing_properties and new_properties as strings + new_properties = params.get('properties') + if new_properties is not None: + new_properties_str = json.dumps(new_properties, sort_keys=True) # Convert new properties to a JSON string + existing_properties_str = indicator['properties'].iloc[0] # Existing properties are already a JSON string - new_visible = params.get('visible') - current_visible = indicator['visible'].iloc[0] + # Compare the strings directly + if existing_properties_str != new_properties_str: + self.cache_manager.modify_datacache_item( + 'indicators', + [('creator', user_id), ('name', indicator_name)], + field_name='properties', + new_data=new_properties, + overwrite='name' + ) + + # Compare existing_source and new_source as strings + new_source = params.get('source') + if new_source is not None: + new_source_str = json.dumps(new_source, sort_keys=True) # Convert new source to a JSON string + existing_source_str = indicator['source'].iloc[ + 0] if 'source' in indicator else None # Existing source as JSON string + + # Compare the strings directly + if existing_source_str != new_source_str and new_source_str is not None: + self.cache_manager.modify_datacache_item( + 'indicators', + [('creator', user_id), ('name', indicator_name)], + field_name='source', + new_data=new_source, + overwrite='name' + ) + + # Convert current_visible to boolean and check if it has changed + current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes'] + new_visible = bool(params.get('visible')) if current_visible != new_visible: - self.cache_manager.modify_datacache_item('indicators', - [('creator', user_id), ('name', indicator_name)], - field_name='visible', new_data=new_visible, - overwrite='name') + self.cache_manager.modify_datacache_item( + 'indicators', + [('creator', user_id), ('name', indicator_name)], + field_name='visible', + new_data=new_visible, + overwrite='name' + ) def new_indicator(self, user_name: str, params) -> None: """ @@ -404,26 +440,22 @@ class Indicators: :param params: The request parameters containing indicator information. :return: None """ - indcr = params['newi_name'] - indtyp = params['newi_type'] + indcr = params['name'] + indtyp = params['type'] # Validate indicator name and type if not indcr: raise ValueError("Indicator name is required.") - if indtyp not in ['SMA', 'EMA', 'RSI', 'LREG', 'ATR', 'BOLBands', 'MACD', 'Volume']: + if indtyp not in indicators_registry: raise ValueError("Unsupported indicator type.") # Create a dictionary of properties from the values in request form. - source = { - 'symbol': params['ei_symbol'], - 'timeframe': params['ei_timeframe'], - 'exchange_name': params['ei_exchange_name'] - } + source = params['source'] # Validate properties (assuming properties is a dictionary) properties = {} - if params['new_prop_obj']: - properties = json.loads(params['new_prop_obj']) + if params['properties']: + properties = json.loads(params['properties']) # Create indicator. self.create_indicator(creator=user_name, name=indcr, kind=indtyp, source=source, properties=properties) @@ -438,7 +470,7 @@ class Indicators: """ username = self.users.get_username(indicator.creator) src = indicator.source - symbol, timeframe, exchange_name = src['symbol'], src['timeframe'], src['exchange_name'] + symbol, timeframe, exchange_name = src['market'], src['timeframe'], src['exchange'] # Retrieve necessary details to instantiate the indicator name = indicator.name @@ -502,11 +534,11 @@ class Indicators: # Extract fields from indicators['source'] and compare directly mask = (indicators['source'].apply(lambda s: s.get('timeframe')) == source_timeframe) & \ - (indicators['source'].apply(lambda s: s.get('exchange_name')) == source_exchange) & \ - (indicators['source'].apply(lambda s: s.get('symbol')) == source_symbol) + (indicators['source'].apply(lambda s: s.get('exchange')) == source_exchange) & \ + (indicators['source'].apply(lambda s: s.get('market')) == source_symbol) # Filter the DataFrame using the mask - filtered_indicators = indicators[mask] + indicators = indicators[mask] # If no indicators match the filtered source, return None. if indicators.empty: @@ -515,7 +547,7 @@ class Indicators: # Process each indicator, convert DataFrame to JSON-serializable format, and collect the results json_ready_results = {} - for indicator in filtered_indicators.itertuples(index=False): + for indicator in indicators.itertuples(index=False): indicator_results = self.process_indicator(indicator=indicator, num_results=num_results) # Convert DataFrame to list of dictionaries if necessary @@ -569,15 +601,22 @@ class Indicators: if kind not in self.indicator_registry: raise ValueError(f"Requested an unsupported type of indicator: ({kind})") + # Instantiate the indicator class from the registry + IndicatorClass = indicators_registry[kind] + indicator_instance = IndicatorClass(name=name, indicator_type=kind, properties=properties) + + # Sanitize source and properties by converting them to JSON strings + sanitized_source = json.dumps(source) # Converts the dictionary to a JSON string + sanitized_properties = json.dumps(indicator_instance.properties) # Same for properties + # Add the new indicator to a pandas dataframe. - creator_id = self.users.get_id(creator) row_data = pd.DataFrame([{ 'creator': creator_id, 'name': name, 'kind': kind, - 'visible': visible, - 'source': source, - 'properties': properties + 'visible': bool(visible), + 'source': sanitized_source, + 'properties': sanitized_properties }]) self.cache_manager.insert_df_into_datacache(df=row_data, cache_name="indicators", skip_cache=False) diff --git a/src/static/communication.js b/src/static/communication.js index be4af7a..543d0ba 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -147,6 +147,28 @@ class Comms { } } + /** + * Sends a request to update an indicator's properties. + * @param {Object} indicatorData - An object containing the updated properties of the indicator. + * @returns {Promise} - The response from the server. + */ + async submitIndicator(indicatorData) { + try { + const response = await fetch('/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + setting: 'new_indicator', + indicator: indicatorData + }) + }); + return await response.json(); + } catch (error) { + console.error('Error updating indicator:', error); + return { success: false }; + } + } + /** * Sends a message to the application server. * @param {string} messageType - The type of the message. diff --git a/src/static/indicators.js b/src/static/indicators.js index e2fc8d0..5f226c6 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -460,51 +460,61 @@ class Indicators { } // This updates a specific indicator - updateIndicator(event) { - const row = event.target.closest('.indicator-row'); - const inputs = row.querySelectorAll('input, select'); +updateIndicator(event) { + 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 + // 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 - // Gather input data - // 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) - properties: {} - }; + // 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 + }; - // Iterate over each input (text, checkbox, select) and add its name and value to formObj - inputs.forEach(input => { - if (input.name === 'visible') { - // Handle the visible checkbox separately - formObj.visible = input.checked; - } else { - // Add all other inputs (type, period, color) to the properties object - formObj.properties[input.name] = input.type === 'checkbox' ? input.checked : input.value; - } - }); + // Iterate over each input (text, checkbox, select) and add its name and value to formObj + inputs.forEach(input => { + if (input.name === 'visible') { + // Handle the visible checkbox separately + formObj.visible = input.checked; + } else if (input.name === 'market' || input.name === 'timeframe' || input.name === 'exchange') { + // Directly map inputs to source object fields + formObj.source[input.name] = input.value; + } else { + // Add all other inputs (type, period, color) to the properties object + formObj.properties[input.name] = input.type === 'checkbox' ? input.checked : input.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.'); - } - }); - } + // 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.'); + } + }); +} add_to_list(){ // Adds user input to a list and displays it in a HTML element. - // Used in the Create indicator panel (!)Called from inline html. - let n = document.getElementById("new_prop_name").value; - let v = document.getElementById("new_prop_value").value; + // 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'){ + 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", @@ -512,28 +522,52 @@ class Indicators { "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 (typeof colours[v.toLowerCase()] != 'undefined') + 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); - let p={}; - p[n] = v; - if (document.getElementById("new_prop_list").innerHTML ==""){ - document.getElementById("new_prop_list").insertAdjacentHTML('beforeend', JSON.stringify(p)); - }else{ - document.getElementById("new_prop_list").insertAdjacentHTML('beforeend', ',' + JSON.stringify(p)); } + // 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 (if available) - const marketField = document.querySelector('[name="ei_symbol"]'); - const timeframeField = document.querySelector('[name="ei_timeframe"]'); - const exchangeField = document.querySelector('[name="ei_exchange_name"]'); + const marketField = document.getElementById('ei_symbol'); + const timeframeField = document.getElementById('ei_timeframe'); + const exchangeField = document.getElementById('ei_exchange_name'); // Set default values if fields are empty if (!marketField.value) { @@ -553,11 +587,11 @@ class Indicators { Used in the create indicator panel.*/ // Perform validation - const name = document.querySelector('[name="newi_name"]').value; - const type = document.querySelector('[name="newi_type"]').value; - let market = document.querySelector('[name="ei_symbol"]').value; - const timeframe = document.querySelector('[name="ei_timeframe"]').value; - const exchange = document.querySelector('[name="ei_exchange_name"]').value; + 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 = ''; @@ -568,8 +602,7 @@ class Indicators { errorMsg += 'Indicator type is required.\n'; } if (!market) { - market = window.UI.data.trading_pair; - document.querySelector('[name="ei_symbol"]').value = market; // Set the form field + errorMsg += 'Indicator market is required.\n'; } if (!timeframe) { errorMsg += 'Timeframe is required.\n'; @@ -583,13 +616,24 @@ class Indicators { return; // Stop form submission if there are errors } - // If validation passes, proceed with form submission - let pl = document.getElementById("new_prop_list").innerHTML; - if (pl) { - pl = '[' + pl + ']'; - } - document.getElementById("new_prop_obj").value = pl; - document.getElementById("new_i_form").submit(); + // 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('Failed to create a new Indicator.'); + } + }); + } } diff --git a/src/templates/indicators_hud.html b/src/templates/indicators_hud.html index 6d3846c..1967802 100644 --- a/src/templates/indicators_hud.html +++ b/src/templates/indicators_hud.html @@ -6,55 +6,41 @@
-
- -
- -
+ +
+

Remove or Edit

Name

-

Type

Value

+

Type

+

Source

Visible

Period

Color

Properties

-
+
{% for indicator in indicator_list %} -
+
- +
- +
- +
{{indicator}}
- -
- {% if 'type' in indicator_list[indicator] %} - - {% else %} - - - {% endif %} -
- - +
{% if 'value' in indicator_list[indicator] %} @@ -63,16 +49,49 @@ {% endif %}
- +
- {% if 'visible' in indicator_list[indicator] %} - + {% if 'type' in indicator_list[indicator] %} + {{indicator_list[indicator]['type']}} {% else %} - {% endif %}
- + +
+ + + {% for symbol in symbols %} + + {% endfor %} + + + + + {% for timeframe in intervals %} + + {% endfor %} + + + + + {% for exchange in exchanges %} + + {% endfor %} + +
+ + +
+ {% if 'visible' in indicator_list[indicator] %} + + {% else %} + - + {% endif %} +
+ +
{% if 'period' in indicator_list[indicator] %} @@ -81,7 +100,7 @@ {% endif %}
- +
{% if 'color' in indicator_list[indicator] %} @@ -90,17 +109,15 @@ {% endif %}
- +
{% for property, value in indicator_list[indicator].items() %} - {% if property not in ['type', 'value', 'color', 'period', 'visible'] %} + {% if property not in ['type', 'value', 'color', 'period', 'visible', 'source'] %}
{% if 'color' in property %} - {% else %} - {% endif %}
@@ -111,29 +128,68 @@
{% endfor %}
-
+ + + + diff --git a/src/templates/new_indicator_popup.html b/src/templates/new_indicator_popup.html index 3a6a051..7ac304b 100644 --- a/src/templates/new_indicator_popup.html +++ b/src/templates/new_indicator_popup.html @@ -1,66 +1,81 @@ -
-
- -
+
+
+ +
- -

Create New Indicator

+ +

Create New Indicator

- - -
- - - -

- Properties: - + +
+ + + +

+ Properties: -

-
- - -
- - - - +

+
+ + + + +
+ + + + +
+
-
-
- - - - - {% for symbol in symbols %} - - {% endfor %} - - - - - - -
+ + + + + {% for symbol in symbols %} + + {% endfor %} + - -
- - -
+ + + + + + + +
+ + +
+ + +
-
-
+ +
+