From 4eda0b6f81e5a48e7e87ef900116f7c03151dd22 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 10 Oct 2024 17:05:07 -0300 Subject: [PATCH] The strategies class is broken into smaller classes. I believe the ui is mostly functional at this point. It is time to get the tests working. --- src/BrighterTrades.py | 30 +- src/DataCache_v3.py | 113 ++++- src/Database.py | 17 +- src/Strategies.py | 19 +- src/indicators.py | 135 ++--- src/static/Strategies.js | 686 +++++++++++++++----------- src/static/indicators.js | 156 ++++-- src/templates/indicator_popup.html | 5 +- src/templates/new_strategy_popup.html | 4 +- src/templates/strategies_hud.html | 2 +- 10 files changed, 762 insertions(+), 405 deletions(-) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 5863e7e..53b6e63 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -1,3 +1,4 @@ +import json from typing import Any from Users import Users @@ -319,7 +320,7 @@ class BrighterTrades: self.config.set_setting('signals_list', self.signals.get_signals('dict')) return data - def received_new_strategy(self, data: dict) -> str | dict: + def received_new_strategy(self, data: dict) -> dict: """ Handles the creation of a new strategy based on the provided data. @@ -426,7 +427,7 @@ class BrighterTrades: return {"success": False, "message": "strategy_name not found"} self.strategies.delete_strategy(user_id=user_id, name=strategy_name) - return {"success": True, "message": "Strategy {strategy_name} deleted"} + return {"success": True, "message": "Strategy deleted", "strategy_name": strategy_name} def delete_signal(self, signal_name: str) -> None: """ @@ -450,13 +451,13 @@ class BrighterTrades: """ return self.signals.get_signals('json') - def get_strategies_json(self, user_id) -> str: + def get_strategies_json(self, user_id) -> list: """ - Retrieve all the strategies from the strategies instance and return them as a JSON object. + Retrieve all the strategies from the strategies instance and return them as a list of dictionaries. - :return: str - A JSON object containing all the strategies. + :return: list - A list of dictionaries, each representing a strategy. """ - return self.strategies.get_all_strategies(user_id, 'json') + return self.strategies.get_all_strategies(user_id, 'dict') def connect_or_config_exchange(self, user_name: str, exchange_name: str, api_keys: dict = None) -> dict: """ @@ -626,7 +627,12 @@ class BrighterTrades: user_name=user_name, default_market=market) elif setting == 'toggle_indicator': - indicators_to_toggle = params.getlist('indicator') + # Parse the indicator field as a JSON array + try: + indicators_to_toggle = json.loads(params.get('indicator', '[]')) + except json.JSONDecodeError: + indicators_to_toggle = [] + user_id = self.get_user_info(user_name=user_name, info='User_id') self.indicators.toggle_indicators(user_id=user_id, indicator_names=indicators_to_toggle) @@ -643,9 +649,6 @@ class BrighterTrades: print(f'ERROR SETTING VALUE') print(f'The string received by the server was: /n{params}') - # Todo this doesn't seem necessary anymore, because the cache now updates per request. - # Now that the state is changed reload price history. - # self.candles.set_cache(user_name=user_name) return def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None: @@ -692,7 +695,12 @@ class BrighterTrades: self.delete_signal(msg_data) if msg_type == 'delete_strategy': - self.delete_strategy(msg_data) + result = self.delete_strategy(msg_data) + if result.get('success'): + return standard_reply("strategy_deleted", + {"message": result.get('message'), "strategy_name": result.get('strategy_name')}) + else: + return standard_reply("strategy_error", {"message": result.get('message')}) if msg_type == 'close_trade': self.close_trade(msg_data) diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index f12e30e..5d38c17 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -734,6 +734,7 @@ class DatabaseInteractions(SnapshotDataCache): :param key: Optional key to filter by 'tbl_key'. :param cache_name: The key used to identify the cache (also the name of the database table). :param filter_vals: A list of tuples, each containing a column name and the value(s) to filter by. + If a value is a list, it will use the SQL 'IN' clause. :return: A DataFrame containing the requested rows, or None if no matching rows are found. :raises ValueError: If the cache is not a DataFrame or does not contain DataFrames in the 'data' column. """ @@ -748,17 +749,62 @@ class DatabaseInteractions(SnapshotDataCache): if key: filter_vals.insert(0, ('tbl_key', key)) + # Convert filter values that are lists to 'IN' clauses for cache filtering result = self.get_rows_from_cache(cache_name, filter_vals) + + # Fallback to database if no result found in cache if result.empty: - # Fallback: Fetch from the database and cache the result if necessary result = self._fetch_from_database(cache_name, filter_vals) + # Only use _fetch_from_database_with_list_support if any filter values are lists + if result.empty and any(isinstance(val, list) for _, val in filter_vals): + result = self._fetch_from_database_with_list_support(cache_name, filter_vals) + # Remove 'tbl_key' unless include_tbl_key is True if not include_tbl_key: result = result.drop(columns=['tbl_key'], errors='ignore') return result + def _fetch_from_database_with_list_support(self, cache_name: str, + filter_vals: List[tuple[str, Any]]) -> pd.DataFrame: + """ + Fetch rows from the database, supporting list values that require SQL 'IN' clauses. + + :param cache_name: The name of the table or key used to store/retrieve data. + :param filter_vals: A list of tuples with the filter column and value, supporting lists for 'IN' clause. + :return: A DataFrame with the fetched rows, or None if no data is found. + """ + where_clauses = [] + params = [] + + for col, val in filter_vals: + if isinstance(val, list): + placeholders = ', '.join('?' for _ in val) + where_clauses.append(f"{col} IN ({placeholders})") + params.extend(val) + else: + where_clauses.append(f"{col} = ?") + params.append(val) + + where_clause = " AND ".join(where_clauses) + sql_query = f"SELECT * FROM {cache_name} WHERE {where_clause}" + + # Execute the SQL query with the prepared parameters + rows = self.db.get_rows_where(sql_query, params) + + # Cache the result (either row or table based) + if rows is not None and not rows.empty: + cache = self.get_cache(cache_name) + + if isinstance(cache, RowBasedCache): + for _, row in rows.iterrows(): + cache.add_entry(key=row['tbl_key'], data=row) + else: + cache.add_table(rows, overwrite='tbl_key') + + return rows + def _fetch_from_database(self, cache_name: str, filter_vals: List[tuple[str, Any]]) -> pd.DataFrame: """ Fetch rows from the database and cache the result. @@ -929,6 +975,71 @@ class DatabaseInteractions(SnapshotDataCache): # Execute the SQL update to modify the database self.db.execute_sql(sql_update, params) + def modify_multiple_datacache_items(self, cache_name: str, filter_vals: List[Tuple[str, any]], + field_names: Tuple[str, ...], new_values: Tuple[Any, ...], + key: str = None, overwrite: str = None) -> None: + """ + Modifies specific fields in multiple rows within the cache and updates the database accordingly. + + :param cache_name: The name used to identify the cache. + :param filter_vals: A list of tuples containing column names and values to filter by. + If a filter value is a list, it will be used with the 'IN' clause. + :param field_names: A tuple of field names to be updated. + :param new_values: A tuple of new values corresponding to field_names. + :param key: Optional key to identify the entry. + :param overwrite: Column name(s) to use for overwriting in the cache. + :raises ValueError: If no rows are found. + """ + if key: + filter_vals.insert(0, ('tbl_key', key)) + + # Prepare the SQL query + where_clauses = [] + query_params = [] + + for col, val in filter_vals: + if isinstance(val, list): + # Use the 'IN' clause if the value is a list + placeholders = ', '.join('?' for _ in val) + where_clauses.append(f"{col} IN ({placeholders})") + query_params.extend(val) + else: + where_clauses.append(f"{col} = ?") + query_params.append(val) + + # Build the SQL query string + where_clause = " AND ".join(where_clauses) + set_clause = ", ".join([f"{field} = ?" for field in field_names]) + sql_update = f"UPDATE {cache_name} SET {set_clause} WHERE {where_clause}" + + # Add the new values to the parameters list + query_params = list(new_values) + query_params + + # Execute the SQL update to modify the database + self.db.execute_sql(sql_update, query_params) + + # Retrieve the rows from the cache to update the cache + rows = self.get_rows_from_datacache(cache_name=cache_name, filter_vals=filter_vals) + + if rows is None or rows.empty: + raise ValueError(f"Rows not found in cache or database for {filter_vals}") + + # Update the cache with the new values + for field_name, new_value in zip(field_names, new_values): + rows[field_name] = new_value + + # Get the cache instance + cache = self.get_cache(cache_name) + + if isinstance(cache, RowBasedCache): + for _, row in rows.iterrows(): + key_value = row['tbl_key'] + cache.add_entry(key=key_value, data=row) + elif isinstance(cache, TableBasedCache): + cache.add_table(rows, overwrite=overwrite) + else: + raise ValueError(f"Unsupported cache type for {cache_name}") + def serialized_datacache_insert(self, cache_name: str, data: Any, key: str = None, do_not_overwrite: bool = False): """ diff --git a/src/Database.py b/src/Database.py index 0e7c0f5..a4112c1 100644 --- a/src/Database.py +++ b/src/Database.py @@ -129,11 +129,22 @@ class Database: """ try: with SQLite(self.db_file) as con: - # Construct the WHERE clause with multiple conditions - where_clause = " AND ".join([f"{col} = ?" for col, _ in filter_vals]) - params = [val for _, val in filter_vals] + where_clauses = [] + params = [] + + # Construct the WHERE clause, handling lists for 'IN' conditions + for col, val in filter_vals: + if isinstance(val, list): + # If the value is a list, use the 'IN' clause + placeholders = ', '.join('?' for _ in val) + where_clauses.append(f"{col} IN ({placeholders})") + params.extend(val) # Extend the parameters with the list values + else: + where_clauses.append(f"{col} = ?") + params.append(val) # Prepare and execute the query with the constructed WHERE clause + where_clause = " AND ".join(where_clauses) qry = f"SELECT * FROM {table} WHERE {where_clause}" result = pd.read_sql(qry, con, params=params) diff --git a/src/Strategies.py b/src/Strategies.py index 41d8a7f..161174e 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -230,9 +230,24 @@ class Strategies: tbl_key ) ) - + # Construct the saved strategy data to return + saved_strategy = { + "id": tbl_key, # Assuming tbl_key is used as a unique identifier + "creator": data.get('creator'), + "name": data['name'], + "workspace": data['workspace'], # Original workspace data + "code": data['code'], + "stats": data.get('stats', {}), + "public": data.get('public', False), + "fee": data.get('fee', 0) + } # If everything is successful, return a success message - return {"success": True, "message": "Strategy created and saved successfully"} + # along with the saved strategy data + return { + "success": True, + "message": "Strategy created and saved successfully", + "strategy": saved_strategy # Include the strategy data + } except Exception as e: # Catch any exceptions and return a failure message diff --git a/src/indicators.py b/src/indicators.py index 10af0ae..8fa6dac 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -362,7 +362,7 @@ class Indicators: return # Set visibility for all indicators off - self.cache_manager.modify_datacache_item( + self.cache_manager.modify_multiple_datacache_items( cache_name='indicators', filter_vals=[('creator', str(user_id))], field_names=('visible',), @@ -371,13 +371,14 @@ class Indicators: ) # Set visibility for the specified indicators on - self.cache_manager.modify_datacache_item( - cache_name='indicators', - filter_vals=[('creator', str(user_id)), ('name', indicator_names)], - field_names=('visible',), - new_values=(True,), - overwrite='name' - ) + if indicator_names: + self.cache_manager.modify_multiple_datacache_items( + cache_name='indicators', + filter_vals=[('creator', str(user_id)), ('name', indicator_names)], + field_names=('visible',), + new_values=(True,), + overwrite='name' + ) def edit_indicator(self, user_name: str, params: dict): """ @@ -385,64 +386,74 @@ class Indicators: :param user_name: The name of the user. :param params: The updated properties of the indicator. """ - indicator_name = params.get('name') - if not indicator_name: - raise ValueError("Indicator name is required for editing.") + try: + indicator_name = params.get('name') + if not indicator_name: + raise ValueError("Indicator name is required for editing.") - # Get the indicator from the user's indicator list - user_id = self.users.get_id(user_name) - indicator = self.cache_manager.get_rows_from_datacache('indicators', - [('name', indicator_name), ('creator', str(user_id))]) - - if indicator.empty: - raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_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 - - # Compare the strings directly - if existing_properties_str != new_properties_str: - self.cache_manager.modify_datacache_item( - 'indicators', - [('creator', str(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', str(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( + # Get user ID and retrieve the indicator + user_id = self.users.get_id(user_name) + indicator = self.cache_manager.get_rows_from_datacache( 'indicators', - [('creator', str(user_id)), ('name', indicator_name)], - field_name=('visible',), - new_data=(new_visible,), - overwrite='name' + [('name', indicator_name), ('creator', str(user_id))] ) + if indicator.empty: + raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.") + + # Validate and update 'properties' + new_properties = params.get('properties') + if new_properties is not None: + if not isinstance(new_properties, dict): + raise ValueError("'properties' must be a dictionary.") + # Serialize the dictionary to a JSON string + new_properties_json = json.dumps(new_properties, sort_keys=True) + # Retrieve existing properties as JSON string + existing_properties_str = indicator['properties'].iloc[0] + + if existing_properties_str != new_properties_json: + self.cache_manager.modify_datacache_item( + 'indicators', + [('creator', str(user_id)), ('name', indicator_name)], + ('properties',), # field_names as a single-element tuple + (new_properties_json,), # new_values as a single-element tuple + overwrite='name' # overwrite remains as a keyword argument + ) + + # Validate and update 'source' + new_source = params.get('source') + if new_source is not None: + if not isinstance(new_source, dict): + raise ValueError("'source' must be a dictionary.") + # Serialize the dictionary to a JSON string + new_source_json = json.dumps(new_source, sort_keys=True) + # Retrieve existing source as JSON string + existing_source_str = indicator['source'].iloc[0] if 'source' in indicator else None + + if existing_source_str != new_source_json and new_source_json is not None: + self.cache_manager.modify_datacache_item( + 'indicators', + [('creator', str(user_id)), ('name', indicator_name)], + ('source',), # field_names as a single-element tuple + (new_source_json,), # new_values as a single-element tuple + overwrite='name' # overwrite remains as a keyword argument + ) + + # Validate and update 'visible' + 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', str(user_id)), ('name', indicator_name)], + ('visible',), # field_names as a single-element tuple + (new_visible,), # new_values as a single-element tuple + overwrite='name' # overwrite remains as a keyword argument + ) + except Exception as e: + raise e + def new_indicator(self, user_name: str, params) -> None: """ Appends a new indicator to a user-specific collection of Indicator definitions. diff --git a/src/static/Strategies.js b/src/static/Strategies.js index e9579c0..b9e020b 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -1,85 +1,215 @@ -class Strategies { +class StratUIManager { constructor() { - // The HTML element that displays the strategies. - this.target_el = null; - // The HTML element that displays the creation form. + this.targetEl = null; this.formElement = null; - // The class responsible for handling server communications. - this.comms = null; - // The class responsible for keeping user data. - this.data = null; - // The list of strategies. - this.strategies = []; - // The Blockly workspace. - this.workspace = null; - // Flag to indicate if the instance is initialized. - this._initialized = false; } /** - * Initializes the Strategies instance with necessary dependencies. - * @param {string} target_id - The ID of the HTML element where strategies will be displayed. + * Initializes the UI elements with provided IDs. + * @param {string} targetId - The ID of the HTML element where strategies will be displayed. * @param {string} formElId - The ID of the HTML element for the strategy creation form. - * @param {Object} data - An object containing user data and communication instances. */ - initialize(target_id, formElId, data) { - try { - // Get the target element for displaying strategies - this.target_el = document.getElementById(target_id); - if (!this.target_el) { - throw new Error(`Element for displaying strategies "${target_id}" not found.`); - } + initUI(targetId, formElId) { + // Get the target element for displaying strategies + this.targetEl = document.getElementById(targetId); + if (!this.targetEl) { + throw new Error(`Element for displaying strategies "${targetId}" not found.`); + } - // Get the form element for strategy creation - this.formElement = document.getElementById(formElId); - if (!this.formElement) { - throw new Error(`Strategies form element "${formElId}" not found.`); - } - - if (!data || typeof data !== 'object') { - throw new Error("Invalid data object provided for initialization."); - } - this.data = data; - - if (!this.data.user_name || typeof this.data.user_name !== 'string') { - throw new Error("Invalid user_name provided in data object."); - } - - this.comms = this.data?.comms; - if (!this.comms) { - throw new Error('Communications instance not provided in data.'); - } - - // Register handlers with Comms for specific message types - this.comms.on('strategy_created', this.handleStrategyCreated.bind(this)); - this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this)); - this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this)); - this.comms.on('updates', this.handleUpdates.bind(this)); - - // Fetch saved strategies from the server - this.fetchSavedStrategies(); - this._initialized = true; - } catch (error) { - console.error("Error initializing Strategies instance:", error.message); + // Get the form element for strategy creation + this.formElement = document.getElementById(formElId); + if (!this.formElement) { + throw new Error(`Strategies form element "${formElId}" not found.`); } } + + /** + * Displays the form for creating or editing a strategy. + * @param {string} action - The action to perform ('new' or 'edit'). + * @param {string|null} strategyData - The data of the strategy to edit (only applicable for 'edit' action). + */ + displayForm(action, strategyData = null) { + console.log(`Opening form for action: ${action}, strategy: ${strategyData?.name}`); + if (this.formElement) { + const headerTitle = this.formElement.querySelector("#draggable_header h1"); + const submitCreateBtn = this.formElement.querySelector("#submit-create"); + const submitEditBtn = this.formElement.querySelector("#submit-edit"); + const nameBox = this.formElement.querySelector('#name_box'); + const publicCheckbox = this.formElement.querySelector('#public_checkbox'); + const feeBox = this.formElement.querySelector('#fee_box'); + + if (!headerTitle || !submitCreateBtn || !submitEditBtn || !nameBox || !publicCheckbox || !feeBox) { + console.error('One or more form elements were not found.'); + return; + } + + // Update form based on action + if (action === 'new') { + headerTitle.textContent = "Create New Strategy"; + submitCreateBtn.style.display = "inline-block"; + submitEditBtn.style.display = "none"; + nameBox.value = ''; + publicCheckbox.checked = false; + feeBox.value = 0; + } else if (action === 'edit' && strategyData) { + headerTitle.textContent = "Edit Strategy"; + submitCreateBtn.style.display = "none"; + submitEditBtn.style.display = "inline-block"; + nameBox.value = strategyData.name; + publicCheckbox.checked = strategyData.public === 1; + feeBox.value = strategyData.fee || 0; + } + + // Display the form + this.formElement.style.display = "grid"; + + // Call the workspace manager to initialize the Blockly workspace after the form becomes visible + if (UI.strats && UI.strats.workspaceManager) { + setTimeout(() => { + UI.strats.workspaceManager.initWorkspace(); + }, 100); // Delay slightly to allow the form to render properly + } else { + console.error("Workspace manager is not initialized or is unavailable."); + } + } else { + console.error(`Form element "${this.formElement.id}" not found.`); + } + } + + + /** + * Hides the "Create New Strategy" form by adding a 'hidden' class. + */ + hideForm() { + if (this.formElement) { + this.formElement.style.display = 'none'; // Hide the form + } + } + + + /** + * Updates the HTML representation of the strategies. + * @param {Object[]} strategies - The list of strategies to display. + */ + updateStrategiesHtml(strategies) { + if (this.targetEl) { + // Clear existing content + while (this.targetEl.firstChild) { + this.targetEl.removeChild(this.targetEl.firstChild); + } + + // Create and append new elements for all strategies + for (let strat of strategies) { + const strategyItem = document.createElement('div'); + strategyItem.className = 'strategy-item'; + + // Delete button + const deleteButton = document.createElement('button'); + deleteButton.className = 'delete-button'; + deleteButton.innerHTML = '✘'; + deleteButton.addEventListener('click', () => { + if (this.onDeleteStrategy) { + this.onDeleteStrategy(strat.name); // Call the callback set by Strategies + } else { + console.error("Delete strategy callback is not set."); + } + }); + strategyItem.appendChild(deleteButton); + + // Strategy icon + const strategyIcon = document.createElement('div'); + strategyIcon.className = 'strategy-icon'; + strategyIcon.addEventListener('click', () => { + this.displayForm('edit', strat); // Open the form with strategy data when clicked + }); + + // Strategy name + const strategyName = document.createElement('div'); + strategyName.className = 'strategy-name'; + strategyName.textContent = strat.name || 'Unnamed Strategy'; // Fallback for undefined + strategyIcon.appendChild(strategyName); + strategyItem.appendChild(strategyIcon); + + // Strategy hover details + const strategyHover = document.createElement('div'); + strategyHover.className = 'strategy-hover'; + strategyHover.innerHTML = `${strat.name || 'Unnamed Strategy'}
Stats: ${JSON.stringify(strat.stats, null, 2)}`; + strategyItem.appendChild(strategyHover); + + // Append to target element + this.targetEl.appendChild(strategyItem); + } + } else { + console.error("Target element for updating strategies is not set."); + } + } + + /** + * Toggles the fee input field based on the state of the public checkbox. + */ + toggleFeeInput() { + const publicCheckbox = document.getElementById('public_checkbox'); + const feeBox = document.getElementById('fee_box'); + + if (publicCheckbox && feeBox) { + feeBox.disabled = !publicCheckbox.checked; + } + } + + /** + * Sets the callback function for deleting a strategy. + * @param {Function} callback - The callback function to call when deleting a strategy. + */ + registerDeleteStrategyCallback(callback) { + this.onDeleteStrategy = callback; + } +} + +class StratDataManager { + constructor() { + this.strategies = []; + } + + /** + * Fetches the saved strategies from the server. + * @param {Object} comms - The communications instance to interact with the server. + * @param {Object} data - An object containing user data. + */ + fetchSavedStrategies(comms, data) { + if (comms) { + try { + const requestData = { + request: 'strategies', + user_name: data?.user_name + }; + comms.sendToApp('request', requestData); + } catch (error) { + console.error("Error fetching saved strategies:", error.message); + alert('Unable to connect to the server. Please check your connection or try reinitializing the application.'); + } + } else { + throw new Error('Communications instance not available.'); + } + } + /** * Handles the creation of a new strategy. * @param {Object} data - The data for the newly created strategy. */ - handleStrategyCreated(data) { - console.log("New strategy created:", data); - // Add the new strategy to the list without fetching from the server again + addNewStrategy(data) { + console.log("Adding new strategy. Data:", data); + if (!data.name) { + console.error("Strategy data missing 'name' field:", data); + } this.strategies.push(data); - // Update the UI - this.updateHtml(); } + /** * Handles updates to the strategy itself (e.g., configuration changes). * @param {Object} data - The updated strategy data. */ - handleStrategyUpdated(data) { + updateStrategyData(data) { console.log("Strategy updated:", data); const index = this.strategies.findIndex(strategy => strategy.id === data.id); if (index !== -1) { @@ -87,22 +217,16 @@ class Strategies { } else { this.strategies.push(data); // Add if not found } - this.updateHtml(); } /** * Handles the deletion of a strategy. - * @param {Object} data - The data for the deleted strategy. + * @param {Object} data - The name for the deleted strategy. */ - handleStrategyDeleted(data) { + removeStrategy(name) { try { - console.log("Strategy deleted:", data); - - // Remove the strategy from the local array - this.strategies = this.strategies.filter(strat => strat.name !== data.strategy_name); - - // Update the UI - this.updateHtml(); + console.log("Strategy deleted:", name); + this.strategies = this.strategies.filter(strat => strat.name !== name); } catch (error) { console.error("Error handling strategy deletion:", error.message); } @@ -112,10 +236,10 @@ class Strategies { * Handles batch updates for strategies, such as multiple configuration or performance updates. * @param {Object} data - The data containing batch updates for strategies. */ - handleUpdates(data) { + applyBatchUpdates(data) { const { stg_updts } = data; if (stg_updts) { - stg_updts.forEach(strategy => this.handleStrategyUpdated(strategy)); + stg_updts.forEach(strategy => this.updateStrategyData(strategy)); } } @@ -123,55 +247,66 @@ class Strategies { * Returns all available strategies. * @returns {Object[]} - The list of available strategies. */ - getAvailableStrategies() { + getAllStrategies() { return this.strategies; } +} + +class StratWorkspaceManager { + constructor() { + this.workspace = null; + this.blocksDefined = false; + } /** - * Creates the Blockly workspace with custom blocks and generators. + * Initializes the Blockly workspace with custom blocks and generators. * Ensures required elements are present in the DOM and initializes the workspace. * @async * @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found. */ - async createWorkspace() { - // Ensure 'blocklyDiv' exists in the DOM + initWorkspace() { if (!document.getElementById('blocklyDiv')) { console.error("blocklyDiv is not loaded."); return; } - // Dispose of the existing workspace if it exists if (this.workspace) { this.workspace.dispose(); } - try { - // Load all modules concurrently to reduce loading time - const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([ - import('./custom_blocks.js'), - import('./indicator_blocks.js'), - import('./python_generators.js'), - import('./json_generators.js') - ]); + // Initialize custom blocks and Blockly workspace + this._loadModulesAndInitWorkspace(); + } - // Define custom blocks - customBlocksModule.defineCustomBlocks(); - indicatorBlocksModule.defineIndicatorBlocks(); - pythonGeneratorsModule.definePythonGenerators(); - jsonGeneratorsModule.defineJsonGenerators(); - } catch (error) { - console.error("Error loading Blockly modules: ", error); - return; + async _loadModulesAndInitWorkspace() { + if (!this.blocksDefined) { + try { + // Load all modules concurrently to reduce loading time + const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([ + import('./custom_blocks.js'), + import('./indicator_blocks.js'), + import('./python_generators.js'), + import('./json_generators.js') + ]); + + // Define custom blocks + customBlocksModule.defineCustomBlocks(); + indicatorBlocksModule.defineIndicatorBlocks(); + pythonGeneratorsModule.definePythonGenerators(); + jsonGeneratorsModule.defineJsonGenerators(); + } catch (error) { + console.error("Error loading Blockly modules: ", error); + return; + } + this.blocksDefined = true; } - // Ensure 'toolbox' exists in the DOM const toolboxElement = document.getElementById('toolbox'); if (!toolboxElement) { console.error("toolbox is not loaded."); return; } - // Initialize the Blockly workspace this.workspace = Blockly.inject('blocklyDiv', { toolbox: toolboxElement, scrollbars: true, @@ -193,8 +328,10 @@ class Strategies { }); } - // Resize the Blockly workspace - resizeWorkspace() { + /** + * Adjusts the Blockly workspace dimensions to fit within the container. + */ + adjustWorkspace() { const blocklyDiv = document.getElementById('blocklyDiv'); if (blocklyDiv && this.workspace) { Blockly.svgResize(this.workspace); @@ -207,7 +344,7 @@ class Strategies { * Generates the strategy data including Python code, JSON representation, and workspace XML. * @returns {string} - A JSON string containing the strategy data. */ - generateStrategyJson() { + compileStrategyJson() { if (!this.workspace) { console.error("Workspace is not available."); return; @@ -230,7 +367,6 @@ class Strategies { const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); - // Compile and return all information as a JSON string return JSON.stringify({ name: strategyName, code: pythonCode, @@ -239,7 +375,6 @@ class Strategies { }); } - /** * Generates a JSON representation of the strategy from the workspace. * @private @@ -247,8 +382,7 @@ class Strategies { */ _generateStrategyJsonFromWorkspace() { const topBlocks = this.workspace.getTopBlocks(true); - const strategyJson = topBlocks.map(block => this._blockToJson(block)); - return strategyJson; + return topBlocks.map(block => this._blockToJson(block)); } /** @@ -265,7 +399,6 @@ class Strategies { statements: {} }; - // Get field values block.inputList.forEach(input => { if (input.fieldRow) { input.fieldRow.forEach(field => { @@ -282,13 +415,11 @@ class Strategies { } else if (input.type === Blockly.NEXT_STATEMENT) { json.statements[input.name] = this._blockToJson(targetBlock); } - } }); - // Handle next blocks (in statements) if (block.getNextBlock()) { - json.next = this.blockToJson(block.getNextBlock()); + json.next = this._blockToJson(block.getNextBlock()); } return json; @@ -298,30 +429,23 @@ class Strategies { * Restores the Blockly workspace from an XML string. * @param {string} workspaceXmlText - The XML text representing the workspace. */ - _restoreWorkspaceFromXml(workspaceXmlText) { + loadWorkspaceFromXml(workspaceXmlText) { try { if (!this.workspace) { console.error("Cannot restore workspace: Blockly workspace is not initialized."); return; } - // Parse the XML text into an XML DOM object const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText); - - // Validate that the XML is not empty and has child nodes if (!workspaceXml || !workspaceXml.hasChildNodes()) { console.error('Invalid workspace XML provided.'); alert('The provided workspace data is invalid and cannot be loaded.'); return; } - // Clear the current workspace this.workspace.clear(); - - // Load the XML DOM into the workspace Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); } catch (error) { - // Handle specific errors if possible if (error instanceof SyntaxError) { console.error('Syntax error in workspace XML:', error.message); alert('There was a syntax error in the workspace data. Please check the data and try again.'); @@ -331,48 +455,173 @@ class Strategies { } } } +} + +class Strategies { + constructor() { + this.uiManager = new StratUIManager(); + this.dataManager = new StratDataManager(); + this.workspaceManager = new StratWorkspaceManager(); + this.comms = null; + this.data = null; + this._initialized = false; + + // Set the delete callback + this.uiManager.registerDeleteStrategyCallback(this.deleteStrategy.bind(this)); + } /** - * Fetches the saved strategies from the server. + * Initializes the Strategies instance with necessary dependencies. + * @param {string} targetId - The ID of the HTML element where strategies will be displayed. + * @param {string} formElId - The ID of the HTML element for the strategy creation form. + * @param {Object} data - An object containing user data and communication instances. */ - fetchSavedStrategies() { - if (this.comms) { - try { - // Prepare request data, including user name if available - const requestData = { - request: 'strategies', - user_name: this.data?.user_name - }; - // Send request to application server - this.comms.sendToApp('request', requestData); - } catch (error) { - console.error("Error fetching saved strategies:", error.message); - alert('Unable to connect to the server. Please check your connection or try reinitializing the application.'); + initialize(targetId, formElId, data) { + try { + // Initialize UI Manager + this.uiManager.initUI(targetId, formElId); + + if (!data || typeof data !== 'object') { + throw new Error("Invalid data object provided for initialization."); } + this.data = data; + + if (!this.data.user_name || typeof this.data.user_name !== 'string') { + throw new Error("Invalid user_name provided in data object."); + } + + this.comms = this.data?.comms; + if (!this.comms) { + throw new Error('Communications instance not provided in data.'); + } + + // Register handlers with Comms for specific message types + this.comms.on('strategy_created', this.handleStrategyCreated.bind(this)); + this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this)); + this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this)); + this.comms.on('updates', this.handleUpdates.bind(this)); + this.comms.on('strategies', this.handleStrategies.bind(this)); + // Register the handler for 'strategy_error' reply + this.comms.on('strategy_error', this.handleStrategyError.bind(this)); + + // Fetch saved strategies using DataManager + this.dataManager.fetchSavedStrategies(this.comms, this.data); + + this._initialized = true; + } catch (error) { + console.error("Error initializing Strategies instance:", error.message); + } + } + + /** + * Handles strategy-related error messages from the server. + * @param {Object} errorData - The data containing error details. + */ + handleStrategyError(errorData) { + console.error("Strategy Error:", errorData.message); + alert(`Error: ${errorData.message}`); + } + + /** + * Handles the reception of existing strategies from the server. + * @param {Array} data - The data containing the list of existing strategies. + */ + handleStrategies(data) { + console.log("Received strategies data:", data); + + if (Array.isArray(data)) { + console.log(`Number of strategies received: ${data.length}`); + // Update the DataManager with the received strategies + this.dataManager.strategies = data; + + // Update the UI to display the strategies + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + console.log("Successfully loaded strategies."); } else { - throw new Error('Communications instance not available.'); + console.error("Failed to load strategies: Invalid data format"); + alert("Failed to load strategies: Invalid data format"); } } + /** - * Updates the strategies with the data received from the server and refreshes the UI. - * @param {string|Array} data - The strategies data as a JSON string or an array of strategy objects. + * Handles the creation of a new strategy. + * @param {Object} data - The data for the newly created strategy. */ - set_data(data) { - if (typeof data === 'string') { - data = JSON.parse(data); + handleStrategyCreated(data) { + console.log("handleStrategyCreated received data:", data); + + if (data.success && data.strategy) { + this.dataManager.addNewStrategy(data.strategy); + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + } else { + console.error("Failed to create strategy:", data.message); + alert(`Strategy creation failed: ${data.message}`); } - this.strategies = data; - this.updateHtml(); // Refresh the strategies display + } + + + /** + * Handles updates to the strategy itself (e.g., configuration changes). + * @param {Object} data - The updated strategy data. + */ + handleStrategyUpdated(data) { + this.dataManager.updateStrategyData(data); + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); } /** - * Hides the "Create New Strategy" form by adding a 'hidden' class. + * Handles the deletion of a strategy. + * @param {Object} data - The data for the deleted strategy. */ - close_form() { - if (this.formElement) { - this.formElement.classList.add('hidden'); - } + handleStrategyDeleted(data) { + const strategyName = data.strategy_name; // Extract the strategy name + + // Remove the strategy using the provided strategy_name + this.dataManager.removeStrategy(strategyName); + + // Update the UI to reflect the deletion + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + } + + /** + * Handles batch updates for strategies, such as multiple configuration or performance updates. + * @param {Object} data - The data containing batch updates for strategies. + */ + handleUpdates(data) { + this.dataManager.applyBatchUpdates(data); + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + } + + /** + * Creates the Blockly workspace using StratWorkspaceManager. + * @async + */ + async createWorkspace() { + await this.workspaceManager.initializeWorkspace(); + } + + /** + * Resizes the Blockly workspace using StratWorkspaceManager. + */ + resizeWorkspace() { + this.workspaceManager.adjustWorkspace(); + } + + /** + * Generates the strategy data including Python code, JSON representation, and workspace XML. + * @returns {string} - A JSON string containing the strategy data. + */ + generateStrategyJson() { + return this.workspaceManager.compileStrategyJson(); + } + + /** + * Restores the Blockly workspace from an XML string using StratWorkspaceManager. + * @param {string} workspaceXmlText - The XML text representing the workspace. + */ + restoreWorkspaceFromXml(workspaceXmlText) { + this.workspaceManager.loadWorkspaceFromXml(workspaceXmlText); } /** @@ -384,19 +633,9 @@ class Strategies { const nameBox = document.getElementById('name_box'); const publicCheckbox = document.getElementById('public_checkbox'); - if (!feeBox) { - console.error("fee_box element not found."); - alert("An error occurred: fee input element is missing."); - return; - } - if (!nameBox) { - console.error("name_box element not found."); - alert("An error occurred: name input element is missing."); - return; - } - if (!publicCheckbox) { - console.error("public_checkbox element not found."); - alert("An error occurred: public checkbox element is missing."); + if (!feeBox || !nameBox || !publicCheckbox) { + console.error("One or more form elements are missing."); + alert("An error occurred: Required form elements are missing."); return; } @@ -435,147 +674,32 @@ class Strategies { // Determine if this is a new strategy or an edit const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy'; - // Format the message and send it using the existing sendToApp function if (this.comms) { this.comms.sendToApp(messageType, strategyData); - this.close_form(); + this.uiManager.hideForm(); } else { console.error("Comms instance not available or invalid action type."); } } - /** - * Toggles the fee input field based on the state of the public checkbox. - * Disables the fee input if the public checkbox is unchecked. - */ - toggleFeeBox() { - const publicCheckbox = document.getElementById('public_checkbox'); - const feeBox = document.getElementById('fee_box'); - if (publicCheckbox && feeBox) { - feeBox.disabled = !publicCheckbox.checked; - } - } - - /** - * Updates the HTML representation of the strategies. - */ - updateHtml() { - // Logic to update the UI with the current list of strategies - if (this.target_el) { - // Clear existing event listeners - while (this.target_el.firstChild) { - this.target_el.removeChild(this.target_el.firstChild); - } - - // Create and append new elements for all strategies - for (let strat of this.strategies) { - const strategyItem = document.createElement('div'); - strategyItem.className = 'strategy-item'; - - // Delete button - const deleteButton = document.createElement('button'); - deleteButton.className = 'delete-button'; - deleteButton.innerHTML = '✘'; - deleteButton.addEventListener('click', () => this.del(strat.name)); - strategyItem.appendChild(deleteButton); - - // Strategy icon - const strategyIcon = document.createElement('div'); - strategyIcon.className = 'strategy-icon'; - strategyIcon.addEventListener('click', () => this.openForm('edit', strat.name)); - - // Strategy name - const strategyName = document.createElement('div'); - strategyName.className = 'strategy-name'; - strategyName.textContent = strat.name; - strategyIcon.appendChild(strategyName); - strategyItem.appendChild(strategyIcon); - - // Strategy hover details - const strategyHover = document.createElement('div'); - strategyHover.className = 'strategy-hover'; - strategyHover.innerHTML = `${strat.name}
Stats: ${JSON.stringify(strat.stats, null, 2)}`; - strategyItem.appendChild(strategyHover); - - // Append to target element - this.target_el.appendChild(strategyItem); - } - } - } - /** - * Opens the form for creating or editing a strategy. - * @param {string} action - The action to perform ('new' or 'edit'). - * @param {string|null} strategyName - The name of the strategy to edit (only applicable for 'edit' action). - */ - openForm(action, strategyName = null) { - console.log(`Opening form for action: ${action}, strategy: ${strategyName}`); - if (this.formElement) { - const headerTitle = this.formElement.querySelector("#draggable_header h1"); - const submitCreateBtn = this.formElement.querySelector("#submit-create"); - const submitEditBtn = this.formElement.querySelector("#submit-edit"); - const nameBox = this.formElement.querySelector('#name_box'); - const publicCheckbox = this.formElement.querySelector('#public_checkbox'); - const feeBox = this.formElement.querySelector('#fee_box'); - - if (!headerTitle || !submitCreateBtn || !submitEditBtn || !nameBox || !publicCheckbox || !feeBox) { - console.error('One or more form elements were not found.'); - return; - } - - if (action === 'new') { - headerTitle.textContent = "Create New Strategy"; - submitCreateBtn.style.display = "inline-block"; - submitEditBtn.style.display = "none"; - nameBox.value = ''; - publicCheckbox.checked = false; - feeBox.value = 0; - - // Create a fresh workspace - this.createWorkspace(); - requestAnimationFrame(() => this.resizeWorkspace()); - - } else if (action === 'edit' && strategyName) { - if (!this.workspace) { - this.createWorkspace(); - } - - const strategyData = this.strategies.find(s => s.name === strategyName); - if (strategyData) { - headerTitle.textContent = "Edit Strategy"; - submitCreateBtn.style.display = "none"; - submitEditBtn.style.display = "inline-block"; - - // Populate the form with strategy data - nameBox.value = strategyData.name; - publicCheckbox.checked = strategyData.public === 1; - feeBox.value = strategyData.fee || 0; - - // Restore workspace from saved XML - this._restoreWorkspaceFromXml(strategyData.workspace); - } else { - console.error(`Strategy "${strategyName}" not found.`); - } - } - - // Display the form - this.formElement.style.display = "grid"; - } else { - console.error(`Form element "${this.formElement.id}" not found.`); - } - } /** * Deletes a strategy by its name. * @param {string} name - The name of the strategy to be deleted. */ - del(name) { + deleteStrategy(name) { console.log(`Deleting strategy: ${name}`); + + // Prepare data for server request const deleteData = { - user_name: this.data.user_name, // Include the user_name - strategy_name: name // Strategy name to be deleted + user_name: this.data.user_name, + strategy_name: name }; // Send delete request to the server - this.comms.sendToApp('delete_strategy', deleteData); + if (this.comms) { + this.comms.sendToApp('delete_strategy', deleteData); + } else { + console.error("Comms instance not available."); + } } - } diff --git a/src/static/indicators.js b/src/static/indicators.js index 4ce6792..1f73eb1 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -20,6 +20,11 @@ class Indicator_Output { } 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) { @@ -564,15 +569,20 @@ class Indicators { Receives indicator data, creates and stores the indicator objects, then inserts the data into the charts. */ - this.create_indicators(idata.indicators, charts); - // Initialize each indicator with the data directly - if (idata.indicator_data) { - this.init_indicators(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.error('Indicator data is not available.'); + console.log('No indicators defined for this user.'); } } + init_indicators(data){ // Loop through all the indicators. for (name in data){ @@ -583,6 +593,11 @@ class Indicators { } 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 @@ -627,49 +642,72 @@ class Indicators { } // This updates a specific indicator -updateIndicator(event) { - const row = event.target.closest('.indicator-row'); - const inputs = row.querySelectorAll('input, select'); + updateIndicator(event) { + event.preventDefault(); // Prevent default form submission behavior - // 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 + const row = event.target.closest('.indicator-row'); + const inputs = row.querySelectorAll('input, select'); - // 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 - }; + // 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 - // 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; - } - }); + // 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.'); + }); + } - // 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. + // 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(); @@ -802,5 +840,43 @@ updateIndicator(event) { }); } + // 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.'); + }); + } } diff --git a/src/templates/indicator_popup.html b/src/templates/indicator_popup.html index 7ca2ce9..76482a4 100644 --- a/src/templates/indicator_popup.html +++ b/src/templates/indicator_popup.html @@ -1,10 +1,11 @@
-
+ {% for indicator in indicator_list %} -
+
{% endfor %}
+
diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html index 9806c26..e776919 100644 --- a/src/templates/new_strategy_popup.html +++ b/src/templates/new_strategy_popup.html @@ -21,7 +21,7 @@
- +
@@ -32,7 +32,7 @@
- + diff --git a/src/templates/strategies_hud.html b/src/templates/strategies_hud.html index 46ec524..92e79b7 100644 --- a/src/templates/strategies_hud.html +++ b/src/templates/strategies_hud.html @@ -1,5 +1,5 @@
- +

Strategies