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