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:
parent
89e0f8b849
commit
4eda0b6f81
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,7 +371,8 @@ 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:
|
||||||
|
self.cache_manager.modify_multiple_datacache_items(
|
||||||
cache_name='indicators',
|
cache_name='indicators',
|
||||||
filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
|
filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
|
||||||
field_names=('visible',),
|
field_names=('visible',),
|
||||||
|
|
@ -385,52 +386,60 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
indicator_name = params.get('name')
|
indicator_name = params.get('name')
|
||||||
if not indicator_name:
|
if not indicator_name:
|
||||||
raise ValueError("Indicator name is required for editing.")
|
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))])
|
'indicators',
|
||||||
|
[('name', indicator_name), ('creator', str(user_id))]
|
||||||
|
)
|
||||||
|
|
||||||
if indicator.empty:
|
if indicator.empty:
|
||||||
raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
|
raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
|
||||||
|
|
||||||
# Compare existing_properties and new_properties as strings
|
# Validate and update 'properties'
|
||||||
new_properties = params.get('properties')
|
new_properties = params.get('properties')
|
||||||
if new_properties is not None:
|
if new_properties is not None:
|
||||||
new_properties_str = json.dumps(new_properties, sort_keys=True) # Convert new properties to a JSON string
|
if not isinstance(new_properties, dict):
|
||||||
existing_properties_str = indicator['properties'].iloc[0] # Existing properties are already a JSON string
|
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]
|
||||||
|
|
||||||
# Compare the strings directly
|
if existing_properties_str != new_properties_json:
|
||||||
if existing_properties_str != new_properties_str:
|
|
||||||
self.cache_manager.modify_datacache_item(
|
self.cache_manager.modify_datacache_item(
|
||||||
'indicators',
|
'indicators',
|
||||||
[('creator', str(user_id)), ('name', indicator_name)],
|
[('creator', str(user_id)), ('name', indicator_name)],
|
||||||
field_name=('properties',),
|
('properties',), # field_names as a single-element tuple
|
||||||
new_data=(new_properties,),
|
(new_properties_json,), # new_values as a single-element tuple
|
||||||
overwrite='name'
|
overwrite='name' # overwrite remains as a keyword argument
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compare existing_source and new_source as strings
|
# Validate and update 'source'
|
||||||
new_source = params.get('source')
|
new_source = params.get('source')
|
||||||
if new_source is not None:
|
if new_source is not None:
|
||||||
new_source_str = json.dumps(new_source, sort_keys=True) # Convert new source to a JSON string
|
if not isinstance(new_source, dict):
|
||||||
existing_source_str = indicator['source'].iloc[
|
raise ValueError("'source' must be a dictionary.")
|
||||||
0] if 'source' in indicator else None # Existing source as JSON string
|
# 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
|
||||||
|
|
||||||
# Compare the strings directly
|
if existing_source_str != new_source_json and new_source_json is not None:
|
||||||
if existing_source_str != new_source_str and new_source_str is not None:
|
|
||||||
self.cache_manager.modify_datacache_item(
|
self.cache_manager.modify_datacache_item(
|
||||||
'indicators',
|
'indicators',
|
||||||
[('creator', str(user_id)), ('name', indicator_name)],
|
[('creator', str(user_id)), ('name', indicator_name)],
|
||||||
field_name=('source',),
|
('source',), # field_names as a single-element tuple
|
||||||
new_data=(new_source,),
|
(new_source_json,), # new_values as a single-element tuple
|
||||||
overwrite='name'
|
overwrite='name' # overwrite remains as a keyword argument
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert current_visible to boolean and check if it has changed
|
# Validate and update 'visible'
|
||||||
current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes']
|
current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes']
|
||||||
new_visible = bool(params.get('visible'))
|
new_visible = bool(params.get('visible'))
|
||||||
|
|
||||||
|
|
@ -438,10 +447,12 @@ class Indicators:
|
||||||
self.cache_manager.modify_datacache_item(
|
self.cache_manager.modify_datacache_item(
|
||||||
'indicators',
|
'indicators',
|
||||||
[('creator', str(user_id)), ('name', indicator_name)],
|
[('creator', str(user_id)), ('name', indicator_name)],
|
||||||
field_name=('visible',),
|
('visible',), # field_names as a single-element tuple
|
||||||
new_data=(new_visible,),
|
(new_visible,), # new_values as a single-element tuple
|
||||||
overwrite='name'
|
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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
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.target_el = document.getElementById(target_id);
|
this.targetEl = document.getElementById(targetId);
|
||||||
if (!this.target_el) {
|
if (!this.targetEl) {
|
||||||
throw new Error(`Element for displaying strategies "${target_id}" not found.`);
|
throw new Error(`Element for displaying strategies "${targetId}" not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the form element for strategy creation
|
// Get the form element for strategy creation
|
||||||
|
|
@ -35,51 +21,195 @@ class Strategies {
|
||||||
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) {
|
* Displays the form for creating or editing a strategy.
|
||||||
throw new Error('Communications instance not provided in data.');
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register handlers with Comms for specific message types
|
// Update form based on action
|
||||||
this.comms.on('strategy_created', this.handleStrategyCreated.bind(this));
|
if (action === 'new') {
|
||||||
this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this));
|
headerTitle.textContent = "Create New Strategy";
|
||||||
this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this));
|
submitCreateBtn.style.display = "inline-block";
|
||||||
this.comms.on('updates', this.handleUpdates.bind(this));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch saved strategies from the server
|
// Display the form
|
||||||
this.fetchSavedStrategies();
|
this.formElement.style.display = "grid";
|
||||||
this._initialized = true;
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error("Error initializing Strategies instance:", error.message);
|
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) {
|
||||||
this.strategies.push(data);
|
console.error("Strategy data missing 'name' field:", data);
|
||||||
// Update the UI
|
|
||||||
this.updateHtml();
|
|
||||||
}
|
}
|
||||||
|
this.strategies.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,28 +247,39 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize custom blocks and Blockly workspace
|
||||||
|
this._loadModulesAndInitWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadModulesAndInitWorkspace() {
|
||||||
|
if (!this.blocksDefined) {
|
||||||
try {
|
try {
|
||||||
// Load all modules concurrently to reduce loading time
|
// Load all modules concurrently to reduce loading time
|
||||||
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
|
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
|
||||||
|
|
@ -163,15 +298,15 @@ class Strategies {
|
||||||
console.error("Error loading Blockly modules: ", error);
|
console.error("Error loading Blockly modules: ", error);
|
||||||
return;
|
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 {
|
||||||
// Prepare request data, including user name if available
|
// Initialize UI Manager
|
||||||
const requestData = {
|
this.uiManager.initUI(targetId, formElId);
|
||||||
request: 'strategies',
|
|
||||||
user_name: this.data?.user_name
|
if (!data || typeof data !== 'object') {
|
||||||
};
|
throw new Error("Invalid data object provided for initialization.");
|
||||||
// Send request to application server
|
}
|
||||||
this.comms.sendToApp('request', requestData);
|
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching saved strategies:", error.message);
|
console.error("Error initializing Strategies instance:", error.message);
|
||||||
alert('Unable to connect to the server. Please check your connection or try reinitializing the application.');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = '✘';
|
|
||||||
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
|
||||||
|
if (this.comms) {
|
||||||
this.comms.sendToApp('delete_strategy', deleteData);
|
this.comms.sendToApp('delete_strategy', deleteData);
|
||||||
|
} else {
|
||||||
|
console.error("Comms instance not available.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
if (idata.indicators && Object.keys(idata.indicators).length > 0) {
|
||||||
this.create_indicators(idata.indicators, charts);
|
this.create_indicators(idata.indicators, charts);
|
||||||
// Initialize each indicator with the data directly
|
// Initialize each indicator with the data directly
|
||||||
if (idata.indicator_data) {
|
if (idata.indicator_data && Object.keys(idata.indicator_data).length > 0) {
|
||||||
this.init_indicators(idata.indicator_data);
|
this.init_indicators(idata.indicator_data);
|
||||||
} else {
|
} else {
|
||||||
console.error('Indicator data is not available.');
|
console.warn('Indicator data is empty. No indicators to initialize.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
|
@ -628,6 +643,8 @@ class Indicators {
|
||||||
|
|
||||||
// This updates a specific indicator
|
// This updates a specific indicator
|
||||||
updateIndicator(event) {
|
updateIndicator(event) {
|
||||||
|
event.preventDefault(); // Prevent default form submission behavior
|
||||||
|
|
||||||
const row = event.target.closest('.indicator-row');
|
const row = event.target.closest('.indicator-row');
|
||||||
const inputs = row.querySelectorAll('input, select');
|
const inputs = row.querySelectorAll('input, select');
|
||||||
|
|
||||||
|
|
@ -643,17 +660,33 @@ updateIndicator(event) {
|
||||||
properties: {} // Initialize the properties object
|
properties: {} // Initialize the properties object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define an exclusion list for properties that should not be parsed as numbers
|
||||||
|
const exclusionFields = ['custom_field_name']; // Add field names that should NOT be parsed as numbers
|
||||||
|
|
||||||
|
// Function to check if a value contains mixed data (e.g., 'abc123')
|
||||||
|
const isMixedData = (value) => /\D/.test(value) && /\d/.test(value);
|
||||||
|
|
||||||
// Iterate over each input (text, checkbox, select) and add its name and value to formObj
|
// Iterate over each input (text, checkbox, select) and add its name and value to formObj
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
|
let value = input.value;
|
||||||
|
|
||||||
|
// Handle the 'visible' checkbox separately
|
||||||
if (input.name === 'visible') {
|
if (input.name === 'visible') {
|
||||||
// Handle the visible checkbox separately
|
|
||||||
formObj.visible = input.checked;
|
formObj.visible = input.checked;
|
||||||
} else if (input.name === 'market' || input.name === 'timeframe' || input.name === 'exchange') {
|
} else if (['market', 'timeframe', 'exchange'].includes(input.name)) {
|
||||||
// Directly map inputs to source object fields
|
// Directly map inputs to source object fields
|
||||||
formObj.source[input.name] = input.value;
|
formObj.source[input.name] = input.value;
|
||||||
} else {
|
} else {
|
||||||
// Add all other inputs (type, period, color) to the properties object
|
// Check if the value should be parsed as a number
|
||||||
formObj.properties[input.name] = input.type === 'checkbox' ? input.checked : input.value;
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -664,12 +697,17 @@ updateIndicator(event) {
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to update the indicator.');
|
alert('Failed to update the indicator.');
|
||||||
}
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error updating indicator:', error);
|
||||||
|
alert('An unexpected error occurred while updating the indicator.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
add_to_list(){
|
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.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<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%}>
|
||||||
|
|
@ -7,4 +7,5 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<input type="submit" value="Submit Changes">
|
<input type="submit" value="Submit Changes">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue