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.

This commit is contained in:
Rob 2024-10-10 17:05:07 -03:00
parent 89e0f8b849
commit 4eda0b6f81
10 changed files with 762 additions and 405 deletions

View File

@ -1,3 +1,4 @@
import json
from typing import Any from typing import Any
from Users import Users from Users import Users
@ -319,7 +320,7 @@ class BrighterTrades:
self.config.set_setting('signals_list', self.signals.get_signals('dict')) self.config.set_setting('signals_list', self.signals.get_signals('dict'))
return data 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. 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"} return {"success": False, "message": "strategy_name not found"}
self.strategies.delete_strategy(user_id=user_id, name=strategy_name) 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: def delete_signal(self, signal_name: str) -> None:
""" """
@ -450,13 +451,13 @@ class BrighterTrades:
""" """
return self.signals.get_signals('json') 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: 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) user_name=user_name, default_market=market)
elif setting == 'toggle_indicator': 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') 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) 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'ERROR SETTING VALUE')
print(f'The string received by the server was: /n{params}') 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 return
def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None: 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) self.delete_signal(msg_data)
if msg_type == 'delete_strategy': 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': if msg_type == 'close_trade':
self.close_trade(msg_data) self.close_trade(msg_data)

View File

@ -734,6 +734,7 @@ class DatabaseInteractions(SnapshotDataCache):
:param key: Optional key to filter by 'tbl_key'. :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 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. :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. :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. :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: if key:
filter_vals.insert(0, ('tbl_key', 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) result = self.get_rows_from_cache(cache_name, filter_vals)
# Fallback to database if no result found in cache
if result.empty: if result.empty:
# Fallback: Fetch from the database and cache the result if necessary
result = self._fetch_from_database(cache_name, filter_vals) 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 # Remove 'tbl_key' unless include_tbl_key is True
if not include_tbl_key: if not include_tbl_key:
result = result.drop(columns=['tbl_key'], errors='ignore') result = result.drop(columns=['tbl_key'], errors='ignore')
return result 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: 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. Fetch rows from the database and cache the result.
@ -929,6 +975,71 @@ class DatabaseInteractions(SnapshotDataCache):
# Execute the SQL update to modify the database # Execute the SQL update to modify the database
self.db.execute_sql(sql_update, params) 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, def serialized_datacache_insert(self, cache_name: str, data: Any, key: str = None,
do_not_overwrite: bool = False): do_not_overwrite: bool = False):
""" """

View File

@ -129,11 +129,22 @@ class Database:
""" """
try: try:
with SQLite(self.db_file) as con: with SQLite(self.db_file) as con:
# Construct the WHERE clause with multiple conditions where_clauses = []
where_clause = " AND ".join([f"{col} = ?" for col, _ in filter_vals]) params = []
params = [val for _, val in filter_vals]
# 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 # Prepare and execute the query with the constructed WHERE clause
where_clause = " AND ".join(where_clauses)
qry = f"SELECT * FROM {table} WHERE {where_clause}" qry = f"SELECT * FROM {table} WHERE {where_clause}"
result = pd.read_sql(qry, con, params=params) result = pd.read_sql(qry, con, params=params)

View File

@ -230,9 +230,24 @@ class Strategies:
tbl_key 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 # 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: except Exception as e:
# Catch any exceptions and return a failure message # Catch any exceptions and return a failure message

View File

@ -362,7 +362,7 @@ class Indicators:
return return
# Set visibility for all indicators off # Set visibility for all indicators off
self.cache_manager.modify_datacache_item( self.cache_manager.modify_multiple_datacache_items(
cache_name='indicators', cache_name='indicators',
filter_vals=[('creator', str(user_id))], filter_vals=[('creator', str(user_id))],
field_names=('visible',), field_names=('visible',),
@ -371,13 +371,14 @@ class Indicators:
) )
# Set visibility for the specified indicators on # Set visibility for the specified indicators on
self.cache_manager.modify_datacache_item( if indicator_names:
cache_name='indicators', self.cache_manager.modify_multiple_datacache_items(
filter_vals=[('creator', str(user_id)), ('name', indicator_names)], cache_name='indicators',
field_names=('visible',), filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
new_values=(True,), field_names=('visible',),
overwrite='name' new_values=(True,),
) overwrite='name'
)
def edit_indicator(self, user_name: str, params: dict): def edit_indicator(self, user_name: str, params: dict):
""" """
@ -385,64 +386,74 @@ class Indicators:
:param user_name: The name of the user. :param user_name: The name of the user.
:param params: The updated properties of the indicator. :param params: The updated properties of the indicator.
""" """
indicator_name = params.get('name') try:
if not indicator_name: indicator_name = params.get('name')
raise ValueError("Indicator name is required for editing.") if not indicator_name:
raise ValueError("Indicator name is required for editing.")
# Get the indicator from the user's indicator list # Get user ID and retrieve the indicator
user_id = self.users.get_id(user_name) user_id = self.users.get_id(user_name)
indicator = self.cache_manager.get_rows_from_datacache('indicators', indicator = self.cache_manager.get_rows_from_datacache(
[('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(
'indicators', 'indicators',
[('creator', str(user_id)), ('name', indicator_name)], [('name', indicator_name), ('creator', str(user_id))]
field_name=('visible',),
new_data=(new_visible,),
overwrite='name'
) )
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: def new_indicator(self, user_name: str, params) -> None:
""" """
Appends a new indicator to a user-specific collection of Indicator definitions. Appends a new indicator to a user-specific collection of Indicator definitions.

View File

@ -1,85 +1,215 @@
class Strategies { class StratUIManager {
constructor() { constructor() {
// The HTML element that displays the strategies. this.targetEl = null;
this.target_el = null;
// The HTML element that displays the creation form.
this.formElement = 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. * Initializes the UI elements with provided IDs.
* @param {string} target_id - The ID of the HTML element where strategies will be displayed. * @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 {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) { initUI(targetId, formElId) {
try { // Get the target element for displaying strategies
// Get the target element for displaying strategies this.targetEl = document.getElementById(targetId);
this.target_el = document.getElementById(target_id); if (!this.targetEl) {
if (!this.target_el) { throw new Error(`Element for displaying strategies "${targetId}" not found.`);
throw new Error(`Element for displaying strategies "${target_id}" not found.`); }
}
// Get the form element for strategy creation // Get the form element for strategy creation
this.formElement = document.getElementById(formElId); this.formElement = document.getElementById(formElId);
if (!this.formElement) { if (!this.formElement) {
throw new Error(`Strategies form element "${formElId}" not found.`); 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);
} }
} }
/**
* 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 = `<strong>${strat.name || 'Unnamed Strategy'}</strong><br>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. * Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy. * @param {Object} data - The data for the newly created strategy.
*/ */
handleStrategyCreated(data) { addNewStrategy(data) {
console.log("New strategy created:", data); console.log("Adding new strategy. Data:", data);
// Add the new strategy to the list without fetching from the server again if (!data.name) {
console.error("Strategy data missing 'name' field:", data);
}
this.strategies.push(data); this.strategies.push(data);
// Update the UI
this.updateHtml();
} }
/** /**
* Handles updates to the strategy itself (e.g., configuration changes). * Handles updates to the strategy itself (e.g., configuration changes).
* @param {Object} data - The updated strategy data. * @param {Object} data - The updated strategy data.
*/ */
handleStrategyUpdated(data) { updateStrategyData(data) {
console.log("Strategy updated:", data); console.log("Strategy updated:", data);
const index = this.strategies.findIndex(strategy => strategy.id === data.id); const index = this.strategies.findIndex(strategy => strategy.id === data.id);
if (index !== -1) { if (index !== -1) {
@ -87,22 +217,16 @@ class Strategies {
} else { } else {
this.strategies.push(data); // Add if not found this.strategies.push(data); // Add if not found
} }
this.updateHtml();
} }
/** /**
* Handles the deletion of a strategy. * 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 { try {
console.log("Strategy deleted:", data); console.log("Strategy deleted:", name);
this.strategies = this.strategies.filter(strat => strat.name !== name);
// Remove the strategy from the local array
this.strategies = this.strategies.filter(strat => strat.name !== data.strategy_name);
// Update the UI
this.updateHtml();
} catch (error) { } catch (error) {
console.error("Error handling strategy deletion:", error.message); 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. * Handles batch updates for strategies, such as multiple configuration or performance updates.
* @param {Object} data - The data containing batch updates for strategies. * @param {Object} data - The data containing batch updates for strategies.
*/ */
handleUpdates(data) { applyBatchUpdates(data) {
const { stg_updts } = data; const { stg_updts } = data;
if (stg_updts) { 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 all available strategies.
* @returns {Object[]} - The list of available strategies. * @returns {Object[]} - The list of available strategies.
*/ */
getAvailableStrategies() { getAllStrategies() {
return this.strategies; 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. * Ensures required elements are present in the DOM and initializes the workspace.
* @async * @async
* @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found. * @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found.
*/ */
async createWorkspace() { initWorkspace() {
// Ensure 'blocklyDiv' exists in the DOM
if (!document.getElementById('blocklyDiv')) { if (!document.getElementById('blocklyDiv')) {
console.error("blocklyDiv is not loaded."); console.error("blocklyDiv is not loaded.");
return; return;
} }
// Dispose of the existing workspace if it exists
if (this.workspace) { if (this.workspace) {
this.workspace.dispose(); this.workspace.dispose();
} }
try { // Initialize custom blocks and Blockly workspace
// Load all modules concurrently to reduce loading time this._loadModulesAndInitWorkspace();
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 async _loadModulesAndInitWorkspace() {
customBlocksModule.defineCustomBlocks(); if (!this.blocksDefined) {
indicatorBlocksModule.defineIndicatorBlocks(); try {
pythonGeneratorsModule.definePythonGenerators(); // Load all modules concurrently to reduce loading time
jsonGeneratorsModule.defineJsonGenerators(); const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
} catch (error) { import('./custom_blocks.js'),
console.error("Error loading Blockly modules: ", error); import('./indicator_blocks.js'),
return; 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'); const toolboxElement = document.getElementById('toolbox');
if (!toolboxElement) { if (!toolboxElement) {
console.error("toolbox is not loaded."); console.error("toolbox is not loaded.");
return; return;
} }
// Initialize the Blockly workspace
this.workspace = Blockly.inject('blocklyDiv', { this.workspace = Blockly.inject('blocklyDiv', {
toolbox: toolboxElement, toolbox: toolboxElement,
scrollbars: true, 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'); const blocklyDiv = document.getElementById('blocklyDiv');
if (blocklyDiv && this.workspace) { if (blocklyDiv && this.workspace) {
Blockly.svgResize(this.workspace); Blockly.svgResize(this.workspace);
@ -207,7 +344,7 @@ class Strategies {
* Generates the strategy data including Python code, JSON representation, and workspace XML. * Generates the strategy data including Python code, JSON representation, and workspace XML.
* @returns {string} - A JSON string containing the strategy data. * @returns {string} - A JSON string containing the strategy data.
*/ */
generateStrategyJson() { compileStrategyJson() {
if (!this.workspace) { if (!this.workspace) {
console.error("Workspace is not available."); console.error("Workspace is not available.");
return; return;
@ -230,7 +367,6 @@ class Strategies {
const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace);
const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml);
// Compile and return all information as a JSON string
return JSON.stringify({ return JSON.stringify({
name: strategyName, name: strategyName,
code: pythonCode, code: pythonCode,
@ -239,7 +375,6 @@ class Strategies {
}); });
} }
/** /**
* Generates a JSON representation of the strategy from the workspace. * Generates a JSON representation of the strategy from the workspace.
* @private * @private
@ -247,8 +382,7 @@ class Strategies {
*/ */
_generateStrategyJsonFromWorkspace() { _generateStrategyJsonFromWorkspace() {
const topBlocks = this.workspace.getTopBlocks(true); const topBlocks = this.workspace.getTopBlocks(true);
const strategyJson = topBlocks.map(block => this._blockToJson(block)); return topBlocks.map(block => this._blockToJson(block));
return strategyJson;
} }
/** /**
@ -265,7 +399,6 @@ class Strategies {
statements: {} statements: {}
}; };
// Get field values
block.inputList.forEach(input => { block.inputList.forEach(input => {
if (input.fieldRow) { if (input.fieldRow) {
input.fieldRow.forEach(field => { input.fieldRow.forEach(field => {
@ -282,13 +415,11 @@ class Strategies {
} else if (input.type === Blockly.NEXT_STATEMENT) { } else if (input.type === Blockly.NEXT_STATEMENT) {
json.statements[input.name] = this._blockToJson(targetBlock); json.statements[input.name] = this._blockToJson(targetBlock);
} }
} }
}); });
// Handle next blocks (in statements)
if (block.getNextBlock()) { if (block.getNextBlock()) {
json.next = this.blockToJson(block.getNextBlock()); json.next = this._blockToJson(block.getNextBlock());
} }
return json; return json;
@ -298,30 +429,23 @@ class Strategies {
* Restores the Blockly workspace from an XML string. * Restores the Blockly workspace from an XML string.
* @param {string} workspaceXmlText - The XML text representing the workspace. * @param {string} workspaceXmlText - The XML text representing the workspace.
*/ */
_restoreWorkspaceFromXml(workspaceXmlText) { loadWorkspaceFromXml(workspaceXmlText) {
try { try {
if (!this.workspace) { if (!this.workspace) {
console.error("Cannot restore workspace: Blockly workspace is not initialized."); console.error("Cannot restore workspace: Blockly workspace is not initialized.");
return; return;
} }
// Parse the XML text into an XML DOM object
const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText); const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText);
// Validate that the XML is not empty and has child nodes
if (!workspaceXml || !workspaceXml.hasChildNodes()) { if (!workspaceXml || !workspaceXml.hasChildNodes()) {
console.error('Invalid workspace XML provided.'); console.error('Invalid workspace XML provided.');
alert('The provided workspace data is invalid and cannot be loaded.'); alert('The provided workspace data is invalid and cannot be loaded.');
return; return;
} }
// Clear the current workspace
this.workspace.clear(); this.workspace.clear();
// Load the XML DOM into the workspace
Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); Blockly.Xml.domToWorkspace(workspaceXml, this.workspace);
} catch (error) { } catch (error) {
// Handle specific errors if possible
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
console.error('Syntax error in workspace XML:', error.message); 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.'); 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() { initialize(targetId, formElId, data) {
if (this.comms) { try {
try { // Initialize UI Manager
// Prepare request data, including user name if available this.uiManager.initUI(targetId, formElId);
const requestData = {
request: 'strategies', if (!data || typeof data !== 'object') {
user_name: this.data?.user_name throw new Error("Invalid data object provided for initialization.");
};
// 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.');
} }
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 { } 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. * Handles the creation of a new strategy.
* @param {string|Array} data - The strategies data as a JSON string or an array of strategy objects. * @param {Object} data - The data for the newly created strategy.
*/ */
set_data(data) { handleStrategyCreated(data) {
if (typeof data === 'string') { console.log("handleStrategyCreated received data:", data);
data = JSON.parse(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() { handleStrategyDeleted(data) {
if (this.formElement) { const strategyName = data.strategy_name; // Extract the strategy name
this.formElement.classList.add('hidden');
} // 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 nameBox = document.getElementById('name_box');
const publicCheckbox = document.getElementById('public_checkbox'); const publicCheckbox = document.getElementById('public_checkbox');
if (!feeBox) { if (!feeBox || !nameBox || !publicCheckbox) {
console.error("fee_box element not found."); console.error("One or more form elements are missing.");
alert("An error occurred: fee input element is missing."); alert("An error occurred: Required form elements are 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.");
return; return;
} }
@ -435,147 +674,32 @@ class Strategies {
// Determine if this is a new strategy or an edit // Determine if this is a new strategy or an edit
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy'; const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
// Format the message and send it using the existing sendToApp function
if (this.comms) { if (this.comms) {
this.comms.sendToApp(messageType, strategyData); this.comms.sendToApp(messageType, strategyData);
this.close_form(); this.uiManager.hideForm();
} else { } else {
console.error("Comms instance not available or invalid action type."); 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 = '&#10008;';
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 = `<strong>${strat.name}</strong><br>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. * Deletes a strategy by its name.
* @param {string} name - The name of the strategy to be deleted. * @param {string} name - The name of the strategy to be deleted.
*/ */
del(name) { deleteStrategy(name) {
console.log(`Deleting strategy: ${name}`); console.log(`Deleting strategy: ${name}`);
// Prepare data for server request
const deleteData = { const deleteData = {
user_name: this.data.user_name, // Include the user_name user_name: this.data.user_name,
strategy_name: name // Strategy name to be deleted strategy_name: name
}; };
// Send delete request to the server // 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.");
}
} }
} }

View File

@ -20,6 +20,11 @@ class Indicator_Output {
} }
set_legend_text(priceValue, name) { set_legend_text(priceValue, name) {
// Ensure the legend for the name exists
if (!this.legend[name]) {
console.warn(`Legend for ${name} not found, skipping legend update.`);
return;
}
// Callback assigned to fire on crosshair movements. // Callback assigned to fire on crosshair movements.
let val = 'n/a'; let val = 'n/a';
if (priceValue !== undefined) { if (priceValue !== undefined) {
@ -564,15 +569,20 @@ class Indicators {
Receives indicator data, creates and stores the indicator Receives indicator data, creates and stores the indicator
objects, then inserts the data into the charts. objects, then inserts the data into the charts.
*/ */
this.create_indicators(idata.indicators, charts); if (idata.indicators && Object.keys(idata.indicators).length > 0) {
// Initialize each indicator with the data directly this.create_indicators(idata.indicators, charts);
if (idata.indicator_data) { // Initialize each indicator with the data directly
this.init_indicators(idata.indicator_data); 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 { } else {
console.error('Indicator data is not available.'); console.log('No indicators defined for this user.');
} }
} }
init_indicators(data){ init_indicators(data){
// Loop through all the indicators. // Loop through all the indicators.
for (name in data){ for (name in data){
@ -583,6 +593,11 @@ class Indicators {
} }
this.i_objs[name].init(data[name]); 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 // This updates all the indicator data
@ -627,49 +642,72 @@ class Indicators {
} }
// This updates a specific indicator // This updates a specific indicator
updateIndicator(event) { updateIndicator(event) {
const row = event.target.closest('.indicator-row'); event.preventDefault(); // Prevent default form submission behavior
const inputs = row.querySelectorAll('input, select');
// Gather the indicator name from the row const row = event.target.closest('.indicator-row');
const nameDiv = row.querySelector('div:nth-child(2)'); // Second <div> contains the name const inputs = row.querySelectorAll('input, select');
const indicatorName = nameDiv.innerText.trim(); // Get the indicator name
// Initialize formObj with the name of the indicator // Gather the indicator name from the row
const formObj = { const nameDiv = row.querySelector('div:nth-child(2)'); // Second <div> contains the name
name: indicatorName, const indicatorName = nameDiv.innerText.trim(); // Get the indicator name
visible: false, // Default value for visible (will be updated based on the checkbox input)
source: {}, // Initialize the source object directly at the top level
properties: {} // Initialize the properties object
};
// Iterate over each input (text, checkbox, select) and add its name and value to formObj // Initialize formObj with the name of the indicator
inputs.forEach(input => { const formObj = {
if (input.name === 'visible') { name: indicatorName,
// Handle the visible checkbox separately visible: false, // Default value for visible (will be updated based on the checkbox input)
formObj.visible = input.checked; source: {}, // Initialize the source object directly at the top level
} else if (input.name === 'market' || input.name === 'timeframe' || input.name === 'exchange') { properties: {} // Initialize the properties object
// Directly map inputs to source object fields };
formObj.source[input.name] = input.value;
} else { // Define an exclusion list for properties that should not be parsed as numbers
// Add all other inputs (type, period, color) to the properties object const exclusionFields = ['custom_field_name']; // Add field names that should NOT be parsed as numbers
formObj.properties[input.name] = input.type === 'checkbox' ? input.checked : input.value;
} // 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(){ add_to_list(){
// Adds user input to a list and displays it in a HTML element. // 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 // Collect the property name and value input by the user
let n = document.getElementById("new_prop_name").value.trim(); let n = document.getElementById("new_prop_name").value.trim();
let v = document.getElementById("new_prop_value").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.');
});
}
} }

View File

@ -1,10 +1,11 @@
<div id='indicators' onmouseleave="document.getElementById('indicators').style.display = 'none' "> <div id='indicators' onmouseleave="document.getElementById('indicators').style.display = 'none' ">
<form action="/settings" method="post"> <form id="indicator-form" action="/settings" method="post">
<input type="hidden" name="setting" value="toggle_indicator"/> <input type="hidden" name="setting" value="toggle_indicator"/>
{% for indicator in indicator_list %} {% for indicator in indicator_list %}
<input type="checkbox" id="{{ indicator }}" name="indicator" value="{{ indicator }}"{% if indicator in checked %} checked{%endif%}> <input type="checkbox" id="{{ indicator }}" name="indicator" value="{{ indicator }}"{% if indicator in checked %} checked{%endif%}>
<label for = "{{ indicator }}">{{ indicator }}</label><br> <label for="{{ indicator }}">{{ indicator }}</label><br>
{% endfor %} {% endfor %}
<input type="submit" value="Submit Changes"> <input type="submit" value="Submit Changes">
</form> </form>
</div> </div>

View File

@ -21,7 +21,7 @@
<!-- Public checkbox --> <!-- Public checkbox -->
<div style="grid-column: 1;"> <div style="grid-column: 1;">
<label for="public_checkbox" style="display: inline-block; width: 20%;">Public:</label> <label for="public_checkbox" style="display: inline-block; width: 20%;">Public:</label>
<input type="checkbox" id="public_checkbox" name="public_checkbox" style="display: inline-block;" onclick="UI.strats.toggleFeeBox()"> <input type="checkbox" id="public_checkbox" name="public_checkbox" style="display: inline-block;" onclick="UI.strats.uiManager.toggleFeeInput()">
</div> </div>
<!-- Fee input field --> <!-- Fee input field -->
@ -32,7 +32,7 @@
<!-- Buttons --> <!-- Buttons -->
<div style="grid-column: 1; text-align: center;"> <div style="grid-column: 1; text-align: center;">
<button type="button" class="btn cancel" onclick="UI.strats.close_form()">Close</button> <button type="button" class="btn cancel" onclick="UI.strats.uiManager.hideForm()">Close</button>
<!-- Create Button --> <!-- Create Button -->
<button id="submit-create" type="button" class="btn next" onclick="UI.strats.submitStrategy('new')">Create Strategy</button> <button id="submit-create" type="button" class="btn next" onclick="UI.strats.submitStrategy('new')">Create Strategy</button>
<!-- Edit Button --> <!-- Edit Button -->

View File

@ -1,5 +1,5 @@
<div class="content" id="strats_content"> <div class="content" id="strats_content">
<button class="btn" id="new_strats_btn" onclick="UI.strats.openForm('new')">New Strategy</button> <button class="btn" id="new_strats_btn" onclick="UI.strats.uiManager.displayForm('new')">New Strategy</button>
<hr> <hr>
<h3>Strategies</h3> <h3>Strategies</h3>
<div class="strategies-container" id="strats_display"></div> <div class="strategies-container" id="strats_display"></div>