diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 5153657..7e567ff 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -1,4 +1,5 @@ import json +import logging from typing import Any from Users import Users @@ -12,6 +13,8 @@ from indicators import Indicators from Signals import Signals from trade import Trades +# Configure logging +logger = logging.getLogger(__name__) class BrighterTrades: def __init__(self, socketio): @@ -340,32 +343,56 @@ class BrighterTrades: if not user_id: return {"success": False, "message": "User ID not found"} - # Validate data types + # Validate data types and contents if not isinstance(data['name'], str) or not data['name'].strip(): return {"success": False, "message": "Invalid or empty strategy name"} if not isinstance(data['workspace'], str) or not data['workspace'].strip(): return {"success": False, "message": "Invalid or empty workspace data"} - if not isinstance(data['code'], dict) or not data['code']: + if not isinstance(data['code'], (dict, str)) or not data['code']: return {"success": False, "message": "Invalid or empty strategy code"} - # Serialize code to JSON string for storage - code_json = json.dumps(data['code']) + try: + # Ensure 'code' is serialized as a JSON string + code_json = json.dumps(data['code']) if isinstance(data['code'], dict) else data['code'] + except (TypeError, ValueError) as e: + return {"success": False, "message": f"Invalid strategy code: {str(e)}"} # Prepare the strategy data for insertion - strategy_data = { - "creator": user_id, - "name": data['name'].strip(), - "workspace": data['workspace'].strip(), - "code": code_json, - "stats": data.get('stats', {}), - "public": int(data.get('public', 0)), - "fee": float(data.get('fee', 0.0)) - } + try: + strategy_data = { + "creator": user_id, + "name": data['name'].strip(), + "workspace": data['workspace'].strip(), + "code": code_json, + "stats": data.get('stats', {}), + "public": int(data.get('public', 0)), + "fee": float(data.get('fee', 0.0)) + } + except Exception as e: + return {"success": False, "message": f"Error preparing strategy data: {str(e)}"} - # The default source for undefined sources in the strategy. - default_source = self.users.get_chart_view(user_name=user_name) - # Save the new strategy (in both cache and database) and return the result. - return self.strategies.new_strategy(strategy_data, default_source) + # The default source for undefined sources in the strategy + try: + default_source = self.users.get_chart_view(user_name=user_name) + except Exception as e: + return {"success": False, "message": f"Error fetching chart view: {str(e)}"} + + # Save the new strategy (in both cache and database) and handle the result + try: + result = self.strategies.new_strategy(strategy_data, default_source) + if result.get("success"): + return { + "success": True, + "strategy": result.get("strategy"), # Strategy object without `strategy_components` + "updated_at": result.get("updated_at"), + "message": result.get("message", "Strategy created successfully") + } + else: + return {"success": False, "message": result.get("message", "Failed to create strategy")} + except Exception as e: + # Log unexpected exceptions for debugging + logger.error(f"Error creating new strategy: {e}", exc_info=True) + return {"success": False, "message": "An unexpected error occurred while creating the strategy"} def received_edit_strategy(self, data: dict) -> dict: """ @@ -377,6 +404,7 @@ class BrighterTrades: # 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: @@ -417,10 +445,28 @@ class BrighterTrades: "tbl_key": tbl_key # Include the tbl_key to identify the strategy } - # The default source for undefined sources in the strategy. - default_source = self.users.get_chart_view(user_name=user_name) + # Get the default source for undefined sources in the strategy + try: + default_source = self.users.get_chart_view(user_name=user_name) + except Exception as e: + return {"success": False, "message": f"Error fetching chart view: {str(e)}"} + # Call the edit_strategy method to update the strategy - return self.strategies.edit_strategy(strategy_data, default_source) + try: + result = self.strategies.edit_strategy(strategy_data, default_source) + if result.get("success"): + return { + "success": True, + "strategy": result.get("strategy"), # Strategy object without `strategy_components` + "updated_at": result.get("updated_at"), + "message": result.get("message", "Strategy updated successfully") + } + else: + return {"success": False, "message": result.get("message", "Failed to update strategy")} + except Exception as e: + # Log unexpected exceptions + logger.error(f"Error editing strategy: {e}", exc_info=True) + return {"success": False, "message": "An unexpected error occurred while editing the strategy"} def delete_strategy(self, data: dict) -> str | dict: """ @@ -593,10 +639,25 @@ class BrighterTrades: return self.trades.get_trades('dict') def delete_backtest(self, msg_data): - """ Delete an existing backtest. """ + """ Delete an existing backtest by interacting with the Backtester. """ backtest_name = msg_data.get('name') - if backtest_name in self.backtests: - del self.backtests[backtest_name] + user_name = msg_data.get('user_name') + + if not backtest_name or not user_name: + return {"success": False, "message": "Missing backtest name or user name."} + + # Construct the backtest_key based on Backtester’s naming convention + backtest_key = f"backtest:{user_name}:{backtest_name}" + + try: + # Delegate the deletion to the Backtester + self.backtester.remove_backtest(backtest_key) + return {"success": True, "message": f"Backtest '{backtest_name}' deleted successfully.", + "name": backtest_name} + except KeyError: + return {"success": False, "message": f"Backtest '{backtest_name}' not found."} + except Exception as e: + return {"success": False, "message": f"Error deleting backtest: {str(e)}"} def adjust_setting(self, user_name: str, setting: str, params: Any): """ @@ -729,12 +790,21 @@ class BrighterTrades: return standard_reply("signal_created", r_data) if msg_type == 'new_strategy': - if r_data := self.received_new_strategy(msg_data): - return standard_reply("strategy_created", r_data) + try: + if r_data := self.received_new_strategy(msg_data): + return standard_reply("strategy_created", r_data) + except Exception as e: + logger.error(f"Error processing new_strategy: {e}", exc_info=True) + return standard_reply("strategy_error", {"message": "Failed to create strategy."}) if msg_type == 'edit_strategy': - if r_data := self.received_edit_strategy(msg_data): - return standard_reply("strategy_created", r_data) + try: + if r_data := self.received_edit_strategy(msg_data): + return standard_reply("strategy_updated", r_data) + except Exception as e: + # Log the error for debugging + logger.error(f"Error processing edit_strategy: {e}", exc_info=True) + return standard_reply("strategy_error", {"message": "Failed to edit strategy."}) if msg_type == 'new_trade': if r_data := self.received_new_trade(msg_data): @@ -760,8 +830,8 @@ class BrighterTrades: return standard_reply("backtest_submitted", resp) if msg_type == 'delete_backtest': - self.delete_backtest(msg_data) - return standard_reply("backtest_deleted", {}) + response = self.delete_backtest(msg_data) + return standard_reply("backtest_deleted", response) if msg_type == 'reply': # If the message is a reply log the response to the terminal. diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 20be0b9..2b66eba 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -340,6 +340,39 @@ class TableBasedCache: except AttributeError as e: raise AttributeError(f"Error in metadata processing: {e}") + def query_with_operator(self, conditions: List[Tuple[str, str, Any]]) -> pd.DataFrame: + """ + Query rows based on conditions and return valid (non-expired) entries. + Todo: test and merge with query(). + :param conditions: List of tuples containing column name, operator, and value. + :return: Filtered DataFrame. + """ + self._purge_expired() # Remove expired rows before querying + + # Start with the entire cache + result = self.cache.copy() + + # Apply conditions using pandas filtering + if not result.empty and conditions: + for col, op, val in conditions: + if op.upper() == 'LIKE': + # Convert SQL LIKE pattern to regex + regex = '^' + val.replace('%', '.*') + '$' + result = result[result[col].astype(str).str.match(regex)] + elif op.upper() == 'IN' and isinstance(val, list): + result = result[result[col].isin(val)] + elif op == '=': + result = result[result[col] == val] + else: + # Add support for other operators as needed + raise ValueError(f"Unsupported operator '{op}' in filter conditions.") + + # Remove the metadata and tbl_key columns for the result + if 'metadata' in result.columns: + result = result.drop(columns=['metadata'], errors='ignore') + + return result + def query(self, conditions: List[Tuple[str, Any]]) -> pd.DataFrame: """Query rows based on conditions and return valid (non-expired) entries.""" self._purge_expired() # Remove expired rows before querying @@ -479,6 +512,37 @@ class CacheManager: # No result return an empty Dataframe return pd.DataFrame() + def get_rows_from_cache_with_operator( + self, + cache_name: str, + filter_vals: list[tuple[str, str, Any]] + ) -> pd.DataFrame | None: + """ + Retrieves rows from the cache if available. + Todo: merge this with the one above and test. + :param cache_name: The key used to identify the cache. + :param filter_vals: A list of tuples, each containing a column name, an operator, and the value to filter by. + :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. + """ + # Check if the cache exists + if cache_name in self.caches: + cache = self.get_cache(cache_name) + + # Ensure the cache contains DataFrames (required for querying) + if not isinstance(cache, (TableBasedCache, RowBasedCache)): + raise ValueError(f"Cache '{cache_name}' does not contain TableBasedCache or RowBasedCache.") + + # Perform the query on the cache using filter_vals + filtered_cache = cache.query_with_operator(filter_vals) # Pass the list of filters + + # If data is found in the cache, return it + if not filtered_cache.empty: + return filtered_cache + + # No result return an empty DataFrame + return pd.DataFrame() + def get_cache_item(self, item_name: str, cache_name: str, filter_vals: tuple[str, any]) -> any: """ Retrieves a specific item from the cache. @@ -755,6 +819,56 @@ class DatabaseInteractions(SnapshotDataCache): super().__init__() self.db = Database() + def get_rows_from_datacache_with_operator( + self, + cache_name: str, + filter_vals: list[tuple[str, str, Any]] = 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 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, an operator, and the value to filter by. + Example: [('strategy_instance_id', 'LIKE', 'test_%')] + :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. + """ + # Ensure at least one of filter_vals or key is provided + if not filter_vals and not key: + raise ValueError("At least one of 'filter_vals' or 'key' must be provided.") + + # Use an empty list if filter_vals is None + filter_vals = filter_vals or [] + + # Insert the key if provided + if key: + filter_vals.insert(0, ('tbl_key', '=', key)) + + # Perform the query on the cache using filter_vals + result = self.get_rows_from_cache_with_operator(cache_name, filter_vals) + + # Fallback to database only if all operators are '=' + if result.empty and all(op == '=' for _, op, _ in filter_vals): + # Extract (column, value) tuples for equality filters + equality_filters = [(col, val) for col, op, val in filter_vals if op == '='] + result = self._fetch_from_database(cache_name, equality_filters) + + # Only use _fetch_from_database_with_list_support if any filter values are lists and all operators are '=' + if result.empty and any(isinstance(val, list) for _, op, val in filter_vals) and all( + op == '=' for _, op, _ in filter_vals): + equality_filters = [(col, val) for col, op, val in filter_vals if op == '='] + result = self._fetch_from_database_with_list_support(cache_name, equality_filters) + + # Remove 'tbl_key' unless include_tbl_key is True + if not include_tbl_key and 'tbl_key' in result.columns: + result = result.drop(columns=['tbl_key'], errors='ignore') + + return result + def get_rows_from_datacache(self, cache_name: str, filter_vals: list[tuple[str, Any]] = None, key: str = None, include_tbl_key: bool = False) -> pd.DataFrame | None: """ diff --git a/src/Strategies.py b/src/Strategies.py index 9db05db..9ee7b9f 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -61,7 +61,6 @@ class Strategies: self.active_instances: dict[tuple[int, str], StrategyInstance] = {} # Key: (user_id, strategy_id) - def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict: """ Saves a strategy to the cache and database. Handles both creation and editing. @@ -72,9 +71,11 @@ class Strategies: """ is_edit = 'tbl_key' in strategy_data try: + # Determine if this is an edit or a new creation + tbl_key = strategy_data.get('tbl_key', str(uuid.uuid4())) + if is_edit: - # Editing an existing strategy - tbl_key = strategy_data['tbl_key'] + # Verify the existing strategy existing_strategy = self.data_cache.get_rows_from_datacache( cache_name='strategies', filter_vals=[('tbl_key', tbl_key)] @@ -82,11 +83,7 @@ class Strategies: if existing_strategy.empty: return {"success": False, "message": "Strategy not found."} else: - # Creating a new strategy - # Generate a unique identifier first - tbl_key = str(uuid.uuid4()) - - # Check if a strategy with the same name already exists for this user + # Check for duplicate strategy name filter_conditions = [ ('creator', strategy_data.get('creator')), ('name', strategy_data['name']) @@ -98,7 +95,7 @@ class Strategies: if not existing_strategy.empty: return {"success": False, "message": "A strategy with this name already exists"} - # Validate and serialize 'workspace' (XML string) + # Validate and serialize 'workspace' workspace_data = strategy_data.get('workspace') if not isinstance(workspace_data, str) or not workspace_data.strip(): return {"success": False, "message": "Invalid or empty workspace data"} @@ -110,10 +107,7 @@ class Strategies: except (TypeError, ValueError): return {"success": False, "message": "Invalid stats data format"} - default_source = default_source.copy() - strategy_id = tbl_key - - # Extract and validate 'code' as a dictionary + # Validate and parse 'code' code = strategy_data.get('code') if isinstance(code, str): try: @@ -124,99 +118,65 @@ class Strategies: return {"success": False, "message": "Invalid JSON format for 'code'."} elif isinstance(code, dict): strategy_json = code - # Serialize 'code' to JSON string - try: - serialized_code = json.dumps(code) - strategy_data['code'] = serialized_code - except (TypeError, ValueError): - return {"success": False, "message": "Unable to serialize 'code' field."} + strategy_data['code'] = json.dumps(strategy_json) # Serialize for storage else: return {"success": False, "message": "'code' must be a JSON string or dictionary."} - # Initialize PythonGenerator - python_generator = PythonGenerator(default_source, strategy_id) - - # Generate strategy components (code, indicators, data_sources, flags) + # Generate Python components using PythonGenerator + python_generator = PythonGenerator(default_source.copy(), tbl_key) strategy_components = python_generator.generate(strategy_json) - - # Add the combined strategy components to the data to be stored strategy_data['strategy_components'] = json.dumps(strategy_components) - if is_edit: - # Editing existing strategy - tbl_key = strategy_data['tbl_key'] - # Prepare the columns and values for the update - columns = ( - "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components" - ) - values = ( - strategy_data.get('creator'), - strategy_data['name'], - workspace_data, # Use the validated workspace data - strategy_data['code'], - stats_serialized, # Serialized stats - bool(strategy_data.get('public', 0)), - float(strategy_data.get('fee', 0.0)), - tbl_key, - strategy_data['strategy_components'] # Serialized strategy components - ) + # Prepare fields for database operations + columns = ( + "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components" + ) + values = ( + strategy_data.get('creator'), + strategy_data['name'], + workspace_data, + strategy_data['code'], + stats_serialized, + bool(strategy_data.get('public', 0)), + float(strategy_data.get('fee', 0.0)), + tbl_key, + strategy_data['strategy_components'] + ) - # Update the strategy in the database and cache + if is_edit: + # Update the existing strategy self.data_cache.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 + overwrite='tbl_key' ) - - # Return success message - return {"success": True, "message": "Strategy updated successfully"} - else: - # Creating new strategy - # Insert the strategy into the database and cache + # Insert a new strategy self.data_cache.insert_row_into_datacache( cache_name='strategies', - columns=( - "creator", "name", "workspace", "code", "stats", - "public", "fee", 'tbl_key', 'strategy_components' - ), - values=( - strategy_data.get('creator'), - strategy_data['name'], - strategy_data['workspace'], - strategy_data['code'], - stats_serialized, - bool(strategy_data.get('public', 0)), - float(strategy_data.get('fee', 0.0)), - tbl_key, - strategy_data['strategy_components'] - ) + columns=columns, + values=values ) - # Construct the saved strategy data to return - saved_strategy = { - "id": tbl_key, # Assuming tbl_key is used as a unique identifier - "creator": strategy_data.get('creator'), - "name": strategy_data['name'], - "workspace": workspace_data, # Original workspace data - "code": strategy_data['code'], - "stats": stats_data, - "public": bool(strategy_data.get('public', 0)), - "fee": float(strategy_data.get('fee', 0.0)) - } - # If everything is successful, return a success message along with the saved strategy data - return { - "success": True, - "message": "Strategy created and saved successfully", - "strategy": saved_strategy # Include the strategy data - } + # Prepare the response + response_strategy = strategy_data.copy() + response_strategy.pop("strategy_components", None) # Remove the sensitive field + + # Include `tbl_key` within the `strategy` object + response_strategy['tbl_key'] = tbl_key + + return { + "success": True, + "message": "Strategy saved successfully", + "strategy": response_strategy, + "updated_at": dt.datetime.now(dt.timezone.utc).isoformat() + } except Exception as e: - # Catch any exceptions and return a failure message - # Log the exception with traceback for debugging + # Handle exceptions and log errors logger.error(f"Failed to save strategy: {e}", exc_info=True) traceback.print_exc() operation = "update" if is_edit else "create" diff --git a/src/app.py b/src/app.py index d02d45c..7ca628d 100644 --- a/src/app.py +++ b/src/app.py @@ -263,7 +263,7 @@ def signout(): @app.route('/login', methods=['POST']) def login(): # Get the user_name and password from the form data - username = request.form.get('user_name') + username = request.form.get('username') password = request.form.get('password') # Validate the input @@ -295,21 +295,23 @@ def signup_submit(): validate_email(email) except EmailNotValidError as e: flash(message=f"Invalid email format: {e}") - return None + return redirect('/signup') # Redirect back to signup page # Validate user_name and password if not username or not password: flash(message="Missing user_name or password") - return None + return redirect('/signup') # Redirect back to signup page # Create a new user success = brighter_trades.create_new_user(email=email, username=username, password=password) if success: session['user'] = username - return redirect('/') + flash(message="Signup successful! You are now logged in.") + return redirect('/') # Redirect to the main page else: flash(message="An error has occurred during the signup process.") - return None + return redirect('/signup') # Redirect back to signup page + @app.route('/api/indicator_init', methods=['POST', 'GET']) diff --git a/src/backtesting.py b/src/backtesting.py index 54e43fc..e53a420 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -148,6 +148,25 @@ class Backtester: except Exception as e: logger.error(f"Error during cleanup of backtest '{backtest_key}': {e}", exc_info=True) + def remove_backtest(self, backtest_key: str): + """ + Remove a backtest from the 'tests' cache. + :param backtest_key: The unique key identifying the backtest. + """ + try: + self.data_cache.remove_row_from_datacache( + cache_name='tests', + filter_vals=[('tbl_key', backtest_key)], + remove_from_db = False # Assuming tests are transient and not stored in DB + ) + logger.info(f"Backtest '{backtest_key}' removed from 'tests' cache.") + except KeyError: + logger.error(f"Backtest '{backtest_key}' not found in 'tests' cache.") + raise KeyError(f"Backtest '{backtest_key}' not found.") + except Exception as e: + logger.error(f"Error removing backtest '{backtest_key}': {e}", exc_info=True) + raise + def validate_strategy_components(self, user_id: str, strategy_name: str, user_name: str) -> dict: """ Retrieves and validates the components of a user-defined strategy. @@ -360,7 +379,8 @@ class Backtester: precomputed_indicators=precomputed_indicators, # Pass precomputed indicators socketio=self.socketio, # Pass SocketIO instance socket_conn_id=socket_conn_id, # Pass SocketIO connection ID - data_length=len(data_feed) # Pass data length for progress updates + data_length=len(data_feed), # Pass data length for progress updates + backtest_name=backtest_name # Pass backtest_name for progress updates ) # Add data feed to Cerebro @@ -395,7 +415,10 @@ class Backtester: trades = strategy.trade_list # Send 100% completion - self.socketio.emit('message', {'reply': 'progress', 'data': {'progress': 100}}, room=socket_conn_id) + self.socketio.emit('message', {'reply': 'progress', + 'data': { 'test_id': backtest_name, + 'progress': 100}} + , room=socket_conn_id) # Prepare the results to pass into the callback backtest_results = { @@ -531,36 +554,25 @@ class Backtester: self.socketio.start_background_task(purge_task) - def cleanup_orphaned_backtest_contexts(self) -> None: """ Identifies and removes orphaned backtest contexts that do not have corresponding entries in 'tests' cache. """ try: # Fetch all strategy_instance_ids from 'strategy_contexts' that start with 'test_' - strategy_contexts_df = self.data_cache.get_rows_from_datacache( + strategy_contexts_df = self.data_cache.get_rows_from_datacache_with_operator( cache_name='strategy_contexts', - filter_vals=[] # Fetch all + filter_vals=[('strategy_instance_id', 'LIKE', 'test_%')] ) if strategy_contexts_df.empty: - logger.debug("No strategy contexts found for cleanup.") - return - - # Filter contexts that are backtests (strategy_instance_id starts with 'test_') - backtest_contexts = strategy_contexts_df[ - strategy_contexts_df['strategy_instance_id'].astype(str).str.startswith('test_') - ] - - if backtest_contexts.empty: logger.debug("No backtest contexts found for cleanup.") return - for _, row in backtest_contexts.iterrows(): + # Iterate through each backtest context + for _, row in strategy_contexts_df.iterrows(): strategy_instance_id = row['strategy_instance_id'] # Check if this backtest exists in 'tests' cache - # Since 'tests' cache uses 'backtest_key' as 'tbl_key', and it maps to 'strategy_instance_id' - # We'll need to search 'tests' cache for the corresponding 'strategy_instance_id' found = False tests_cache = self.data_cache.get_cache('tests') if isinstance(tests_cache, RowBasedCache): @@ -573,10 +585,11 @@ class Backtester: # Orphaned context found; proceed to remove it self.data_cache.remove_row_from_datacache( cache_name='strategy_contexts', - filter_vals=[('strategy_instance_id', strategy_instance_id)], + filter_vals=[('strategy_instance_id', strategy_instance_id)], # Correct format remove_from_db=True ) - logger.info(f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.") + logger.info( + f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.") except Exception as e: logger.error(f"Error during cleanup of orphaned backtest contexts: {e}", exc_info=True) diff --git a/src/mapped_strategy.py b/src/mapped_strategy.py index 928781f..1a97537 100644 --- a/src/mapped_strategy.py +++ b/src/mapped_strategy.py @@ -22,6 +22,7 @@ class MappedStrategy(bt.Strategy): ('socketio', None), # SocketIO instance for emitting progress ('socket_conn_id', None), # Socket connection ID for emitting progress ('data_length', None), # Total number of data points for progress calculation + ('backtest_name', None) # Name of the backtest_name ) def __init__(self): @@ -48,6 +49,7 @@ class MappedStrategy(bt.Strategy): # Initialize other needed variables self.starting_balance = self.broker.getvalue() self.last_progress = 0 # Initialize last_progress + self.backtest_name = self.p.backtest_name self.bar_executed = 0 # Initialize bar_executed def notify_order(self, order): @@ -140,7 +142,8 @@ class MappedStrategy(bt.Strategy): if self.p.socketio and self.p.socket_conn_id: self.p.socketio.emit( 'message', - {'reply': 'progress', 'data': {'progress': progress}}, + {'reply': 'progress', 'data': { 'test_id': self.backtest_name, + 'progress': progress}}, room=self.p.socket_conn_id ) logger.debug(f"Emitted progress: {progress}%") diff --git a/src/static/Strategies.js b/src/static/Strategies.js index 6544690..243ab7b 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -216,6 +216,16 @@ class StratDataManager { this.strategies.push(data); } + /** + * Retrieves a strategy by its tbl_key. + * @param {string} tbl_key - The tbl_key of the strategy to find. + * @returns {Object|null} - The strategy object or null if not found. + */ + getStrategyById(tbl_key) { + return this.strategies.find(strategy => strategy.tbl_key === tbl_key) || null; + } + + /** * Handles updates to the strategy itself (e.g., configuration changes). @@ -635,12 +645,18 @@ class Strategies { } /** - * Handles strategy-related error messages from the server. - * @param {Object} errorData - The data containing error details. + * Handles strategy-related errors sent from the server. + * @param {Object} errorData - The error message and additional details. */ handleStrategyError(errorData) { console.error("Strategy Error:", errorData.message); - alert(`Error: ${errorData.message}`); + + // Display a user-friendly error message + if (errorData.message) { + alert(`Error: ${errorData.message}`); + } else { + alert("An unknown error occurred while processing the strategy."); + } } /** @@ -665,6 +681,7 @@ class Strategies { } + /** * Handles the creation of a new strategy. * @param {Object} data - The data for the newly created strategy. @@ -681,16 +698,39 @@ class Strategies { } } - /** * Handles updates to the strategy itself (e.g., configuration changes). - * @param {Object} data - The updated strategy data. + * @param {Object} data - The server response containing strategy update metadata. */ handleStrategyUpdated(data) { - this.dataManager.updateStrategyData(data); - this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + if (data.success) { + console.log("Strategy updated successfully:", data); + + // Locate the strategy in the local state by its tbl_key + const updatedStrategyKey = data.strategy.tbl_key; + const updatedAt = data.updated_at; + + const strategy = this.dataManager.getStrategyById(updatedStrategyKey); + if (strategy) { + // Update the relevant strategy data + Object.assign(strategy, data.strategy); + + // Update the `updated_at` field + strategy.updated_at = updatedAt; + + // Refresh the UI to reflect the updated metadata + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + } else { + console.warn("Updated strategy not found in local records:", updatedStrategyKey); + } + } else { + console.error("Failed to update strategy:", data.message); + alert(`Strategy update failed: ${data.message}`); + } } + + /** * Handles the deletion of a strategy. * @param {Object} data - The data for the deleted strategy. diff --git a/src/static/backtesting.js b/src/static/backtesting.js index 44fcb22..c16c7f2 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -4,6 +4,7 @@ class Backtesting { this.comms = ui.data.comms; this.tests = []; // Stores the list of saved backtests this.target_id = 'backtest_display'; // The container to display backtests + this.currentTest = null; // Tracks the currently open test // Register handlers for backtesting messages this.comms.on('backtest_error', this.handleBacktestError.bind(this)); @@ -14,174 +15,272 @@ class Backtesting { this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this)); } + // Initialize method to cache DOM elements + initialize() { + this.cacheDOMElements(); + // Optionally, fetch saved tests or perform other initialization + } + + cacheDOMElements() { + this.progressContainer = document.getElementById('backtest-progress-container'); + this.progressBar = document.getElementById('progress_bar'); + this.formElement = document.getElementById('backtest_form'); + this.statusMessage = document.getElementById('backtest-status-message'); + this.resultsContainer = document.getElementById('backtest-results'); + this.resultsDisplay = document.getElementById('results_display'); + this.backtestDraggableHeader = document.getElementById('backtest-form-header'); // Updated to single h1 + this.backtestDisplay = document.getElementById(this.target_id); + this.strategyDropdown = document.getElementById('strategy_select'); + this.equityCurveChart = document.getElementById('equity_curve_chart'); + } + + // Utility Methods + showElement(element) { + if (element) element.classList.add('show'); + } + + hideElement(element) { + if (element) element.classList.remove('show'); + } + + setText(element, text) { + if (element) element.textContent = text; + } + + displayMessage(message, color = 'blue') { + if (this.statusMessage) { + this.showElement(this.statusMessage); + this.statusMessage.style.color = color; + this.setText(this.statusMessage, message); + } + } + + clearMessage() { + if (this.statusMessage) { + this.hideElement(this.statusMessage); + this.setText(this.statusMessage, ''); + } + } + + // Event Handlers handleBacktestSubmitted(data) { - console.log("Backtest response received:", data.status); if (data.status === "started") { - // Show the progress bar or any other UI updates - this.showRunningAnimation(); + const existingTest = this.tests.find(t => t.name === data.backtest_name); + const availableStrategies = this.getAvailableStrategies(); - // Add the new backtest to the tests array - const newTest = { - name: data.backtest_name, - strategy: data.strategy_name, - start_date: data.start_date, - // Include any other relevant data from the response if available - }; - this.tests.push(newTest); + if (existingTest) { + Object.assign(existingTest, { + status: 'running', + progress: 0, + start_date: data.start_date, + results: null, + strategy: availableStrategies.includes(data.strategy) + ? data.strategy + : availableStrategies[0] || 'default_strategy' + }); + } else { + const newTest = { + name: data.backtest_name, + strategy: availableStrategies.includes(data.strategy) + ? data.strategy + : availableStrategies[0] || 'default_strategy', + start_date: data.start_date, + status: 'running', + progress: 0, + results: null + }; + this.tests.push(newTest); + } - // Update the HTML to reflect the new backtest + // Set currentTest to the test name received from backend + this.currentTest = data.backtest_name; + console.log(`handleBacktestSubmitted: Backtest "${data.backtest_name}" started.`); + + this.showRunningAnimation(); // Display progress container this.updateHTML(); } } + + handleBacktestError(data) { console.error("Backtest error:", data.message); - // Display error message in the status message area - const statusMessage = document.getElementById('backtest-status-message'); - if (statusMessage) { - statusMessage.style.display = 'block'; - statusMessage.style.color = 'red'; // Set text color to red for errors - statusMessage.textContent = `Backtest error: ${data.message}`; - } else { - // Fallback to alert if the element is not found - alert(`Backtest error: ${data.message}`); + const test = this.tests.find(t => t.name === this.currentTest); + if (test) { + test.status = 'error'; // Update the test status + console.log(`Backtest "${test.name}" encountered an error.`); + this.updateHTML(); } - // Optionally hide progress bar and results - const progressContainer = document.getElementById('backtest-progress-container'); - if (progressContainer) { - progressContainer.classList.remove('show'); - } - const resultsContainer = document.getElementById('backtest-results'); - if (resultsContainer) { - resultsContainer.style.display = 'none'; - } + this.displayMessage(`Backtest error: ${data.message}`, 'red'); + + // Hide progress bar and results + this.hideElement(this.progressContainer); + this.hideElement(this.resultsContainer); } + handleBacktestResults(data) { - console.log("Backtest results received:", data.results); - // Logic to stop running animation and display results - this.stopRunningAnimation(data.results); + const test = this.tests.find(t => t.name === data.test_id); + if (test) { + Object.assign(test, { + status: 'complete', + progress: 100, + results: data.results + }); + + // Validate strategy + if (!test.strategy) { + console.warn(`Test "${test.name}" is missing a strategy. Setting a default.`); + test.strategy = 'default_strategy'; // Use a sensible default + } + + this.updateHTML(); + this.stopRunningAnimation(data.results); + } } handleProgress(data) { - console.log("Backtest progress:", data.progress); - // Logic to update progress bar - this.updateProgressBar(data.progress); + console.log("handleProgress: Backtest progress:", data.progress); + + if (!this.progressContainer) { + console.error('handleProgress: Progress container not found.'); + return; + } + + // Find the test that matches the progress update + const test = this.tests.find(t => t.name === data.test_id); + if (!test) { + console.warn(`handleProgress: Progress update received for unknown test: ${data.test_id}`); + return; + } + + // Update the progress for the correct test + test.progress = data.progress; + console.log(`handleProgress: Updated progress for "${test.name}" to ${data.progress}%.`); + + // If the currently open test matches, update the dialog's progress bar + if (this.currentTest === test.name && this.formElement.style.display === "grid") { + this.showElement(this.progressContainer); // Adds 'show' class + this.updateProgressBar(data.progress); + this.displayMessage('Backtest in progress...', 'blue'); + console.log(`handleProgress: Progress container updated for "${test.name}".`); + } } + handleBacktestsList(data) { console.log("Backtests list received:", data.tests); - // Logic to update backtesting UI - this.set_data(data.tests); + // Update the tests array + this.tests = data.tests; + this.updateHTML(); } handleBacktestDeleted(data) { console.log(`Backtest "${data.name}" was successfully deleted.`); - // Logic to refresh list of backtests - this.fetchSavedTests(); + // Remove the deleted test from the tests array + this.tests = this.tests.filter(t => t.name !== data.name); + this.updateHTML(); + } + + // Helper Methods + getAvailableStrategies() { + return this.ui.strats.dataManager.getAllStrategies().map(s => s.name); } updateProgressBar(progress) { - const progressBar = document.getElementById('progress_bar'); - if (progressBar) { + if (this.progressBar) { console.log(`Updating progress bar to ${progress}%`); - progressBar.style.width = `${progress}%`; - progressBar.textContent = `${progress}%`; + this.progressBar.style.width = `${progress}%`; + this.setText(this.progressBar, `${progress}%`); } else { console.log('Progress bar element not found'); } } - showRunningAnimation() { - const resultsContainer = document.getElementById('backtest-results'); - const resultsDisplay = document.getElementById('results_display'); - const progressContainer = document.getElementById('backtest-progress-container'); - const progressBar = document.getElementById('progress_bar'); - const statusMessage = document.getElementById('backtest-status-message'); - - resultsContainer.style.display = 'none'; - progressContainer.classList.add('show'); // Use class to control display - progressBar.style.width = '0%'; - progressBar.textContent = '0%'; - resultsDisplay.innerHTML = ''; - statusMessage.style.display = 'block'; - statusMessage.style.color = 'blue'; // Reset text color to blue - statusMessage.textContent = 'Backtest started...'; + this.hideElement(this.resultsContainer); + this.showElement(this.progressContainer); + this.updateProgressBar(0); + this.setText(this.progressBar, '0%'); + this.resultsDisplay.innerHTML = ''; // Clear previous results + this.displayMessage('Backtest started...', 'blue'); } - - displayTestResults(results) { - const resultsContainer = document.getElementById('backtest-results'); - const resultsDisplay = document.getElementById('results_display'); - - resultsContainer.style.display = 'block'; - - // Calculate total return - const totalReturn = (((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2); - - // Create HTML content + this.showElement(this.resultsContainer); let html = ` - Initial Capital: ${results.initial_capital} - Final Portfolio Value: ${results.final_portfolio_value} - Total Return: ${totalReturn}% - Run Duration: ${results.run_duration.toFixed(2)} seconds - `; + Initial Capital: ${results.initial_capital}
+ Final Portfolio Value: ${results.final_portfolio_value}
+ Total Return: ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%
+ Run Duration: ${results.run_duration.toFixed(2)} seconds + `; - // Add a container for the chart - html += `

Equity Curve

-
`; + // Equity Curve + html += ` +

Equity Curve

+
+ `; - // If there are trades, display them + // Trades Table if (results.trades && results.trades.length > 0) { - html += `

Trades Executed

-
- - - - - - - `; + html += ` +

Trades Executed

+
+
Trade IDSizePriceP&L
+ + + + + + + + + + `; results.trades.forEach(trade => { - html += ` + html += ` + - `; + + `; }); - html += `
Trade IDSizePriceP&L
${trade.ref} ${trade.size} ${trade.price} ${trade.pnl}
`; + html += ` + + + + `; } else { html += `

No trades were executed.

`; } - resultsDisplay.innerHTML = html; - - // Generate the equity curve chart + this.resultsDisplay.innerHTML = html; this.drawEquityCurveChart(results.equity_curve); } + drawEquityCurveChart(equityCurve) { - const chartContainer = document.getElementById('equity_curve_chart'); - if (!chartContainer) { + const equityCurveChart = document.getElementById('equity_curve_chart'); + if (!equityCurveChart) { console.error('Chart container not found'); return; } - // Get the dimensions of the container - const width = chartContainer.clientWidth || 600; - const height = chartContainer.clientHeight || 300; + // Clear previous chart + equityCurveChart.innerHTML = ''; + + // Get container dimensions + const width = equityCurveChart.clientWidth || 600; + const height = equityCurveChart.clientHeight || 300; const padding = 40; - // Find min and max values + // Calculate min and max values const minValue = Math.min(...equityCurve); const maxValue = Math.max(...equityCurve); - - // Avoid division by zero if all values are the same const valueRange = maxValue - minValue || 1; // Normalize data points @@ -191,42 +290,47 @@ class Backtesting { return { x, y }; }); - // Create SVG content - let svgContent = ``; + // Create SVG element + const svgNS = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(svgNS, "svg"); + svg.setAttribute("width", width); + svg.setAttribute("height", height); // Draw axes - svgContent += ``; // X-axis - svgContent += ``; // Y-axis + const xAxis = document.createElementNS(svgNS, "line"); + xAxis.setAttribute("x1", padding); + xAxis.setAttribute("y1", height - padding); + xAxis.setAttribute("x2", width - padding); + xAxis.setAttribute("y2", height - padding); + xAxis.setAttribute("stroke", "black"); + svg.appendChild(xAxis); + + const yAxis = document.createElementNS(svgNS, "line"); + yAxis.setAttribute("x1", padding); + yAxis.setAttribute("y1", padding); + yAxis.setAttribute("x2", padding); + yAxis.setAttribute("y2", height - padding); + yAxis.setAttribute("stroke", "black"); + svg.appendChild(yAxis); // Draw equity curve - svgContent += ``; + const polyline = document.createElementNS(svgNS, "polyline"); + const points = normalizedData.map(point => `${point.x},${point.y}`).join(' '); + polyline.setAttribute("points", points); + polyline.setAttribute("fill", "none"); + polyline.setAttribute("stroke", "blue"); + polyline.setAttribute("stroke-width", "2"); + svg.appendChild(polyline); - // Close SVG - svgContent += ``; - - // Set SVG content - chartContainer.innerHTML = svgContent; + equityCurveChart.appendChild(svg); } - stopRunningAnimation(results) { - const progressContainer = document.getElementById('backtest-progress-container'); - progressContainer.classList.remove('show'); - - // Hide the status message - const statusMessage = document.getElementById('backtest-status-message'); - statusMessage.style.display = 'none'; - statusMessage.textContent = ''; - + this.hideElement(this.progressContainer); + this.clearMessage(); this.displayTestResults(results); } - - fetchSavedTests() { this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); } @@ -234,15 +338,68 @@ class Backtesting { updateHTML() { let html = ''; for (const test of this.tests) { + const statusClass = test.status || 'default'; // Use the status or fallback to 'default' html += ` -
- -
${test.name}
+
+ +
${test.name}
`; } - document.getElementById(this.target_id).innerHTML = html; + this.backtestDisplay.innerHTML = html; } + openTestDialog(testName) { + const test = this.tests.find(t => t.name === testName); + if (!test) { + alert('Test not found.'); + return; + } + + this.currentTest = testName; // Set the currently open test + + // Populate the strategy dropdown + this.populateStrategyDropdown(); + + // Validate and set strategy + const availableStrategies = this.getAvailableStrategies(); + if (test.strategy && availableStrategies.includes(test.strategy)) { + this.strategyDropdown.value = test.strategy; + } else { + console.warn(`openTestDialog: Strategy "${test.strategy}" not found in dropdown. Defaulting to first available.`); + this.strategyDropdown.value = availableStrategies[0] || ''; + } + + // Populate other form fields + document.getElementById('start_date').value = test.start_date + ? this.formatDateToLocalInput(new Date(test.start_date)) + : this.formatDateToLocalInput(new Date(Date.now() - 60 * 60 * 1000)); // 1 hour ago + document.getElementById('initial_capital').value = test.results?.initial_capital || 10000; + document.getElementById('commission').value = test.results?.commission || 0.001; + console.log(`openTestDialog: Set start_date to ${document.getElementById('start_date').value}`); + + // Display results or show progress + if (test.status === 'complete') { + this.displayTestResults(test.results); + this.hideElement(this.progressContainer); + } else { + this.hideElement(this.resultsContainer); + this.showElement(this.progressContainer); + this.updateProgressBar(test.progress); + this.displayMessage('Backtest in progress...', 'blue'); + } + + // Update header and show form + this.setText(this.backtestDraggableHeader, `Edit Backtest - ${test.name}`); + + // Manage button visibility + this.showElement(document.getElementById('backtest-submit-edit')); + this.hideElement(document.getElementById('backtest-submit-create')); + + this.formElement.style.display = "grid"; + console.log(`openTestDialog: Opened dialog for backtest "${test.name}".`); + } + + runTest(testName) { const testData = { name: testName, user_name: this.ui.data.user_name }; this.comms.sendToApp('run_backtest', testData); @@ -254,70 +411,82 @@ class Backtesting { } populateStrategyDropdown() { - const strategyDropdown = document.getElementById('strategy_select'); - strategyDropdown.innerHTML = ''; - const strategies = this.ui.strats.dataManager.getAllStrategies(); - console.log("Available strategies:", strategies); - - strategies.forEach(strategy => { - const option = document.createElement('option'); - option.value = strategy.name; - option.text = strategy.name; - strategyDropdown.appendChild(option); - }); - - if (strategies.length > 0) { - const firstStrategyName = strategies[0].name; - console.log("Setting default strategy to:", firstStrategyName); - strategyDropdown.value = firstStrategyName; - } - } - - openForm(testName = null) { - const formElement = document.getElementById("backtest_form"); - if (!formElement) { - console.error('Form element not found'); + if (!this.strategyDropdown) { + console.error('Strategy dropdown element not found.'); return; } - this.populateStrategyDropdown(); + this.strategyDropdown.innerHTML = ''; // Clear existing options - if (testName) { - const testData = this.tests.find(test => test.name === testName); - if (testData) { - document.querySelector("#backtest_draggable_header h1").textContent = "Edit Backtest"; - document.getElementById('strategy_select').value = testData.strategy; - document.getElementById('start_date').value = testData.start_date; - } - } else { - document.querySelector("#backtest_draggable_header h1").textContent = "Create New Backtest"; - this.clearForm(); + const strategies = this.getAvailableStrategies(); + if (!strategies || strategies.length === 0) { + console.warn('No strategies available to populate dropdown.'); + return; } - formElement.style.display = "grid"; + strategies.forEach(strategy => { + const option = document.createElement('option'); + option.value = strategy; + option.text = strategy; + this.strategyDropdown.appendChild(option); + }); } + openForm(testName = null) { + if (testName) { + this.openTestDialog(testName); + } else { + this.currentTest = null; // Reset the currently open test + + // Populate the strategy dropdown + this.populateStrategyDropdown(); + + // Update header and show form + this.setText(this.backtestDraggableHeader, "Create New Backtest"); + this.clearForm(); + + // Set default start_date to 1 hour ago + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); // Current time minus 1 hour + const formattedDate = this.formatDateToLocalInput(oneHourAgo); + document.getElementById('start_date').value = formattedDate; + console.log(`openForm: Set default start_date to ${formattedDate}`); + + // Manage button visibility + this.showElement(document.getElementById('backtest-submit-create')); + this.hideElement(document.getElementById('backtest-submit-edit')); + + this.formElement.style.display = "grid"; + console.log('openForm: Opened form for creating a new backtest.'); + } + } + + closeForm() { - document.getElementById("backtest_form").style.display = "none"; - // Hide and clear the status message - const statusMessage = document.getElementById('backtest-status-message'); - statusMessage.style.display = 'none'; - statusMessage.textContent = ''; + this.formElement.style.display = "none"; + this.currentTest = null; // Reset the currently open test + + // Optionally hide progress/results to avoid stale UI + this.hideElement(this.resultsContainer); + this.hideElement(this.progressContainer); + this.clearMessage(); } clearForm() { - document.getElementById('strategy_select').value = ''; + if (this.strategyDropdown) this.strategyDropdown.value = ''; document.getElementById('start_date').value = ''; + document.getElementById('initial_capital').value = 10000; + document.getElementById('commission').value = 0.001; } submitTest() { - const strategy = document.getElementById('strategy_select').value; + const strategy = this.strategyDropdown ? this.strategyDropdown.value : null; const start_date = document.getElementById('start_date').value; const capital = parseFloat(document.getElementById('initial_capital').value) || 10000; const commission = parseFloat(document.getElementById('commission').value) || 0.001; if (!strategy) { alert("Please select a strategy."); + console.log('submitTest: Submission failed - No strategy selected.'); return; } @@ -326,6 +495,25 @@ class Backtesting { if (startDate > now) { alert("Start date cannot be in the future."); + console.log('submitTest: Submission failed - Start date is in the future.'); + return; + } + + let testName; + if (this.currentTest && this.tests.find(t => t.name === this.currentTest)) { + // Editing an existing test + testName = this.currentTest; + console.log(`submitTest: Editing existing backtest "${testName}".`); + } else { + // Creating a new test without timestamp + testName = `${strategy}_backtest`; + console.log(`submitTest: Creating new backtest "${testName}".`); + } + + // Check if the test is already running + if (this.tests.find(t => t.name === testName && t.status === 'running')) { + alert(`A test named "${testName}" is already running.`); + console.log(`submitTest: Submission blocked - "${testName}" is already running.`); return; } @@ -334,9 +522,41 @@ class Backtesting { start_date, capital, commission, - user_name: this.ui.data.user_name + user_name: this.ui.data.user_name, + backtest_name: testName, }; + // Disable the submit button to prevent duplicate submissions + const submitButton = document.getElementById('backtest-submit-create'); + if (submitButton) { + submitButton.disabled = true; + } + + // Submit the test this.comms.sendToApp('submit_backtest', testData); + + // Log the submission and keep the form open for progress monitoring + console.log('submitTest: Backtest data submitted and form remains open for progress monitoring.'); + + // Re-enable the button after submission (adjust timing as necessary) + setTimeout(() => { + if (submitButton) { + submitButton.disabled = false; + } + }, 2000); // Example: Re-enable after 2 seconds or on callback } -} \ No newline at end of file + + + // Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format + formatDateToLocalInput(date) { + const pad = (num) => num.toString().padStart(2, '0'); + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); // Months are zero-based + const day = pad(date.getDate()); + const hours = pad(date.getHours()); + const minutes = pad(date.getMinutes()); + return `${year}-${month}-${day}T${hours}:${minutes}`; + } + + +} diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index 314e00f..71314d4 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -210,12 +210,12 @@ height: 500px; border: none; margin-bottom: 15px; color: #bfc0c0; - background: #262626; + background: #246486; padding: 20px; font-size: 16px; border-radius: 10px; box-shadow: inset 5px 5px 5px #232323, - inset -5px -5px 5px #292929; + inset -5px -5px 5px #292929; outline: none; } .second-button { @@ -246,20 +246,20 @@ height: 500px; cursor: pointer; } .form-popup .close-btn { - color: white; - font-size: 30px; - border-radius: 50%; - background: #292929; - position: absolute; - right: 20px; - top: 20px; - width: 30px; - padding: 2px 5px 7px 5px; - height: 30px; - box-shadow: 5px 5px 15px #1e1e1e, - -5px -5px 15px #1e1e1e; - text-align: center; - color: white; + border-color: black; + border-style: solid; + border-radius: 50%; + background: #9f180f; + position: absolute; + right: 20px; + top: 20px; + width: 30px; + font-size: larger; + height: 30px; + box-shadow: 5px 5px 15px #1e1e1e, + -5px -5px 15px #1e1e1e; + text-align: center; + color: white; } .form-popup .close-btn:hover { box-shadow: 5px 5px 15px #9f180f, diff --git a/src/static/general.js b/src/static/general.js index 53f6e13..755d5ed 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -28,6 +28,9 @@ class User_Interface { // Initialize other UI components here this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy"); this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest"); + + // Initialize Backtesting's DOM elements + this.backtesting.initialize(); } catch (error) { console.error('Initialization failed:', error); } diff --git a/src/templates/backtest_popup.html b/src/templates/backtest_popup.html index 6b079c4..0ea4122 100644 --- a/src/templates/backtest_popup.html +++ b/src/templates/backtest_popup.html @@ -3,8 +3,7 @@
-

Create New Backtest

-

Edit Backtest

+

Create New Backtest

@@ -36,8 +35,8 @@
- - + +
@@ -51,7 +50,7 @@
-