From 86843e8cb4c2a16f8c7fcb99c1adccd723b6932e Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 25 Sep 2024 14:27:56 -0300 Subject: [PATCH] Strategies is now fixed and I am ready to implement backtesting. --- src/BrighterTrades.py | 57 +++++++++++++++++++++++++++++++ src/DataCache_v3.py | 73 +++++++++++++++++++--------------------- src/Strategies.py | 62 +++++++++++++++++++++++++++++++--- src/Users.py | 7 ++-- src/indicators.py | 30 +++++++++++------ src/static/Strategies.js | 8 ++++- 6 files changed, 180 insertions(+), 57 deletions(-) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index ee39ed7..5091329 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -346,6 +346,59 @@ class BrighterTrades: # Save the new strategy (in both cache and database) and return the result. return self.strategies.new_strategy(strategy_data) + def received_edit_strategy(self, data: dict) -> dict: + """ + Handles editing an existing strategy based on the provided data. + + :param data: A dictionary containing the attributes of the strategy to edit. + :return: A dictionary containing success or failure information. + """ + # Extract user_name and strategy name from the data + user_name = data.get('user_name') + strategy_name = data.get('name') + if not user_name: + return {"success": False, "message": "User not specified"} + if not strategy_name: + return {"success": False, "message": "Strategy name not specified"} + + # Fetch the user_id using the user_name + user_id = self.get_user_info(user_name=user_name, info='User_id') + if not user_id: + return {"success": False, "message": "User ID not found"} + + # Retrieve the tbl_key using user_id and strategy name + filter_conditions = [('creator', user_id), ('name', strategy_name)] + strategy_row = self.data.get_rows_from_datacache( + cache_name='strategies', + filter_vals=filter_conditions, + include_tbl_key=True # Include tbl_key in the result + ) + + if strategy_row.empty: + return {"success": False, "message": "Strategy not found"} + + # Ensure only one strategy is found + if len(strategy_row) > 1: + return {"success": False, "message": "Multiple strategies found. Please provide more specific information."} + + # Extract the tbl_key + tbl_key = strategy_row.iloc[0]['tbl_key'] + + # Prepare the updated strategy data + strategy_data = { + "creator": user_id, + "name": strategy_name, + "workspace": data.get('workspace'), + "code": data.get('code'), + "stats": data.get('stats', {}), + "public": data.get('public', 0), + "fee": data.get('fee', None), + "tbl_key": tbl_key # Include the tbl_key to identify the strategy + } + + # Call the edit_strategy method to update the strategy + return self.strategies.edit_strategy(strategy_data) + def delete_strategy(self, data: dict) -> str | dict: """ Deletes the specified strategy from the strategies instance and the configuration file. @@ -640,6 +693,10 @@ class BrighterTrades: if r_data := self.received_new_strategy(msg_data): return standard_reply("strategy_created", r_data) + if msg_type == 'edit_strategy': + if r_data := self.received_edit_strategy(msg_data): + return standard_reply("strategy_created", r_data) + if msg_type == 'new_trade': if r_data := self.received_new_trade(msg_data): return standard_reply("trade_created", r_data) diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 6de2e63..0379d0e 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -727,11 +727,12 @@ class DatabaseInteractions(SnapshotDataCache): self.db = Database() def get_rows_from_datacache(self, cache_name: str, filter_vals: list[tuple[str, Any]] = None, - key: str = None) -> pd.DataFrame | None: + key: str = None, include_tbl_key: bool = False) -> pd.DataFrame | None: """ Retrieves rows from the cache if available; otherwise, queries the database and caches the result. - :param key: Optional + :param include_tbl_key: If True, includes 'tbl_key' in the returned DataFrame. + :param key: Optional key to filter by 'tbl_key'. :param cache_name: The key used to identify the cache (also the name of the database table). :param filter_vals: A list of tuples, each containing a column name and the value(s) to filter by. :return: A DataFrame containing the requested rows, or None if no matching rows are found. @@ -753,8 +754,11 @@ class DatabaseInteractions(SnapshotDataCache): # Fallback: Fetch from the database and cache the result if necessary result = self._fetch_from_database(cache_name, filter_vals) - # Take the key out on return. - return result.drop(columns=['tbl_key'], errors='ignore') + # Remove 'tbl_key' unless include_tbl_key is True + if not include_tbl_key: + result = result.drop(columns=['tbl_key'], errors='ignore') + + return result def _fetch_from_database(self, cache_name: str, filter_vals: List[tuple[str, Any]]) -> pd.DataFrame: """ @@ -822,7 +826,12 @@ class DatabaseInteractions(SnapshotDataCache): columns, values = columns + ('tbl_key',), values + (key,) # Insert the row into the database - self.db.insert_row(table=cache_name, columns=columns, values=values) + last_inserted_id = self.db.insert_row(table=cache_name, columns=columns, values=values) + + # Now include the 'id' in the columns and values when inserting into the cache + columns = ('id',) + columns + values = (last_inserted_id,) + values + # Insert the row into the cache if skip_cache: return @@ -869,18 +878,18 @@ class DatabaseInteractions(SnapshotDataCache): # Execute the SQL query to remove the row from the database self.db.execute_sql(sql, params) - def modify_datacache_item(self, cache_name: str, filter_vals: List[Tuple[str, any]], field_name: str, - new_data: any, key: str = None, overwrite: str = None) -> None: + def modify_datacache_item(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 a specific field in a row within the cache and updates the database accordingly. + Modifies specific fields in a row within the cache and updates the database accordingly. - :param overwrite: - :param key: optional key - :param cache_name: The name used to identify the cache (also the name of the database table). + :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. - :param field_name: The field to be updated. - :param new_data: The new data to be set. - :raises ValueError: If the row is not found in the cache or the database, or if multiple rows are returned. + :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 the row is not found or multiple rows are returned. """ if key: filter_vals.insert(0, ('tbl_key', key)) @@ -893,44 +902,30 @@ class DatabaseInteractions(SnapshotDataCache): # Check if multiple rows are returned if len(rows) > 1: - raise ValueError(f"Multiple rows found for {filter_vals}. Please provide a more specific filter.") + raise ValueError(f"Multiple rows found for {filter_vals}. Please provide more specific filter.") - # Ensure consistency when storing boolean data like 'visible' - if isinstance(new_data, bool): - # If storing in a system that supports booleans, you can store directly as boolean - updated_value = new_data # For cache systems that support booleans - - # If your cache or database only supports strings, convert boolean to string - # updated_value = 'true' if new_data else 'false' - elif isinstance(new_data, str): - updated_value = new_data - else: - updated_value = json.dumps(new_data) # Convert non-string data to JSON string if necessary - - # Update the DataFrame with the new value - rows[field_name] = updated_value + # Update the DataFrame 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) # Set the updated row in the cache if isinstance(cache, RowBasedCache): - # For row-based cache, the 'tbl_key' must be in filter_vals - key_value = next((val for key, val in filter_vals if key == 'tbl_key'), None) - if key_value is None: - raise ValueError("'tbl_key' must be present in filter_vals for row-based caches.") # Update the cache entry with the modified row - cache.add_entry(key=key_value, data=rows) + cache.add_entry(key=key, data=rows) elif isinstance(cache, TableBasedCache): - # For table-based cache, use the existing query method to update the correct rows + # Use 'overwrite' to ensure correct row is updated cache.add_table(rows, overwrite=overwrite) else: raise ValueError(f"Unsupported cache type for {cache_name}") - # Update the value in the database as well - sql_update = f"UPDATE {cache_name} SET {field_name} = ? " \ - f"WHERE {' AND '.join([f'{col} = ?' for col, _ in filter_vals])}" - params = [updated_value] + [val for _, val in filter_vals] + # Update the values in the database + set_clause = ", ".join([f"{field} = ?" for field in field_names]) + where_clause = " AND ".join([f"{col} = ?" for col, _ in filter_vals]) + sql_update = f"UPDATE {cache_name} SET {set_clause} WHERE {where_clause}" + params = list(new_values) + [val for _, val in filter_vals] # Execute the SQL update to modify the database self.db.execute_sql(sql_update, params) diff --git a/src/Strategies.py b/src/Strategies.py index 2b72af3..41d8a7f 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -1,4 +1,5 @@ import json +import uuid import pandas as pd @@ -199,18 +200,25 @@ class Strategies: :return: A dictionary containing success or failure information. """ try: - # # Create a new Strategy object and add it to the list - # self.strat_list.append(Strategy(**data)) + # Check if a strategy with the same name already exists for this user + filter_conditions = [('creator', data.get('creator')), ('name', data['name'])] + existing_strategy = self.data.get_rows_from_datacache(cache_name='strategies', + filter_vals=filter_conditions) + if not existing_strategy.empty: + return {"success": False, "message": "A strategy with this name already exists"} # Serialize complex data fields like workspace and stats workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ 'workspace'] stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string + # generate a unique identifier + tbl_key = str(uuid.uuid4()) + # Insert the strategy into the database and cache self.data.insert_row_into_datacache( cache_name='strategies', - columns=("creator", "name", "workspace", "code", "stats", "public", "fee"), + columns=("creator", "name", "workspace", "code", "stats", "public", "fee", 'tbl_key'), values=( data.get('creator'), data['name'], @@ -218,7 +226,8 @@ class Strategies: data['code'], stats_serialized, # Serialized stats data.get('public', False), - data.get('fee', 0) + data.get('fee', 0), + tbl_key ) ) @@ -243,6 +252,51 @@ class Strategies: filter_vals=[('creator', user_id), ('name', name)] ) + def edit_strategy(self, data: dict) -> dict: + """ + Updates an existing strategy in the cache and database. + + :param data: A dictionary containing the updated strategy data. + :return: A dictionary containing success or failure information. + """ + try: + tbl_key = data['tbl_key'] # The unique identifier for the strategy + + # Serialize complex data fields like workspace and stats + workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ + 'workspace'] + stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string + + # Prepare the columns and values for the update + columns = ("creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key") + values = ( + data.get('creator'), + data['name'], + workspace_serialized, # Serialized workspace + data['code'], + stats_serialized, # Serialized stats + data.get('public', False), + data.get('fee', 0), + tbl_key + ) + + # Update the strategy in the database and cache + self.data.modify_datacache_item( + cache_name='strategies', + filter_vals=[('tbl_key', tbl_key)], + field_names=columns, + new_values=values, + key=tbl_key, + overwrite='tbl_key' # Use 'tbl_key' to identify the entry to overwrite + ) + + # Return success message + return {"success": True, "message": "Strategy updated successfully"} + + except Exception as e: + # Handle exceptions and return failure message + return {"success": False, "message": f"Failed to update strategy: {str(e)}"} + def get_all_strategy_names(self, user_id) -> list | None: """ Return a list of all strategy names stored in the cache or database. diff --git a/src/Users.py b/src/Users.py index 990291f..7415ace 100644 --- a/src/Users.py +++ b/src/Users.py @@ -108,9 +108,10 @@ class BaseUser: self.data.modify_datacache_item( cache_name='users', filter_vals=[('user_name', username)], - field_name=field_name, - new_data=new_data, - overwrite='user_name') + field_names=(field_name,), + new_values=(new_data,), + overwrite='user_name' + ) class UserAccountManagement(BaseUser): diff --git a/src/indicators.py b/src/indicators.py index 72e169a..adc2d6b 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -361,12 +361,22 @@ class Indicators: return # Set visibility for all indicators off - self.cache_manager.modify_datacache_item('indicators', [('creator', str(user_id))], - field_name='visible', new_data=False, overwrite='name') + self.cache_manager.modify_datacache_item( + cache_name='indicators', + filter_vals=[('creator', str(user_id))], + field_names=('visible',), + new_values=(False,), + overwrite='name' + ) # Set visibility for the specified indicators on - self.cache_manager.modify_datacache_item('indicators', [('creator', str(user_id)), ('name', indicator_names)], - field_name='visible', new_data=True, overwrite='name') + self.cache_manager.modify_datacache_item( + cache_name='indicators', + filter_vals=[('creator', str(user_id)), ('name', indicator_names)], + field_names=('visible',), + new_values=(True,), + overwrite='name' + ) def edit_indicator(self, user_name: str, params: dict): """ @@ -397,8 +407,8 @@ class Indicators: self.cache_manager.modify_datacache_item( 'indicators', [('creator', str(user_id)), ('name', indicator_name)], - field_name='properties', - new_data=new_properties, + field_name=('properties',), + new_data=(new_properties,), overwrite='name' ) @@ -414,8 +424,8 @@ class Indicators: self.cache_manager.modify_datacache_item( 'indicators', [('creator', str(user_id)), ('name', indicator_name)], - field_name='source', - new_data=new_source, + field_name=('source',), + new_data=(new_source,), overwrite='name' ) @@ -427,8 +437,8 @@ class Indicators: self.cache_manager.modify_datacache_item( 'indicators', [('creator', str(user_id)), ('name', indicator_name)], - field_name='visible', - new_data=new_visible, + field_name=('visible',), + new_data=(new_visible,), overwrite='name' ) diff --git a/src/static/Strategies.js b/src/static/Strategies.js index c7caab7..baee611 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -228,10 +228,16 @@ class Strategies { strategy_name: name // Strategy name to be deleted }; - // Corrected call to send message type and data separately window.UI.data.comms.sendToApp('delete_strategy', deleteData); + + // Remove the strategy from the local array + this.strategies = this.strategies.filter(strat => strat.name !== name); + + // Update the UI + this.update_html(); } + // Initialize strategies initialize() { this.target = document.getElementById(this.target_id);