From e1516a80cf79db25ffa44cc92226d99bfbf69429 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 20 Nov 2024 00:18:01 -0400 Subject: [PATCH] A lot of stuff is fixed. only issues at the moment are that errors during testing are not displayed in the ui and the strategies need to reflect the stats. --- src/BrighterTrades.py | 55 +++--- src/DataCache_v3.py | 30 +++- src/Database.py | 11 ++ src/Strategies.py | 103 ++++++----- src/backtesting.py | 326 ++++++++++++++++++++++++---------- src/static/Strategies.js | 138 ++++++++------ src/static/backtesting.js | 185 +++++++++++++------ src/static/brighterStyles.css | 5 +- 8 files changed, 585 insertions(+), 268 deletions(-) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 7e567ff..8abc513 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -468,30 +468,34 @@ class BrighterTrades: 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: + def delete_strategy(self, data: dict) -> dict: """ - Deletes the specified strategy from the strategies instance and the configuration file. + Deletes the specified strategy identified by tbl_key from the strategies instance. - :return: None - :raises ValueError: If the strategy does not exist or there are issues - with removing it from the configuration file. + :param data: Dictionary containing 'tbl_key' and 'user_name'. + :return: A dictionary indicating success or failure with an appropriate message and the tbl_key. """ - # Extract user_name from the data and get user_id - user_name = data.get('user_name') - if not user_name: - return {"success": False, "message": "User not specified"} + # Validate tbl_key + tbl_key = data.get('tbl_key') + if not tbl_key: + return {"success": False, "message": "tbl_key not provided", "tbl_key": None} - # 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"} + # Call the delete_strategy method to remove the strategy + result = self.strategies.delete_strategy(tbl_key=tbl_key) - strategy_name = data.get('strategy_name') - if not strategy_name: - return {"success": False, "message": "strategy_name not found"} - - self.strategies.delete_strategy(user_id=user_id, name=strategy_name) - return {"success": True, "message": "Strategy deleted", "strategy_name": strategy_name} + # Return the result with tbl_key included + if result.get('success'): + return { + "success": True, + "message": result.get('message'), + "tbl_key": tbl_key # Include tbl_key in the response + } + else: + return { + "success": False, + "message": result.get('message'), + "tbl_key": tbl_key # Include tbl_key even on failure for debugging + } def delete_signal(self, signal_name: str) -> None: """ @@ -517,7 +521,7 @@ class BrighterTrades: def get_strategies_json(self, user_id) -> list: """ - Retrieve all the strategies from the strategies instance and return them as a list of dictionaries. + Retrieve all public and user strategies from the strategies instance and return them as a list of dictionaries. :return: list - A list of dictionaries, each representing a strategy. """ @@ -777,10 +781,15 @@ class BrighterTrades: if msg_type == 'delete_strategy': 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')}) + return standard_reply("strategy_deleted", { + "message": result.get('message'), + "tbl_key": result.get('tbl_key') # Include tbl_key in the response + }) else: - return standard_reply("strategy_error", {"message": result.get('message')}) + return standard_reply("strategy_error", { + "message": result.get('message'), + "tbl_key": result.get('tbl_key') # Include tbl_key for debugging purposes + }) if msg_type == 'close_trade': self.close_trade(msg_data) diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 2b66eba..32041d8 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -53,8 +53,9 @@ def estimate_record_count(start_time, end_time, timeframe: str) -> int: # Convert milliseconds to datetime if needed if isinstance(start_time, (int, float, np.integer)) and isinstance(end_time, (int, float, np.integer)): - start_time = dt.datetime.utcfromtimestamp(start_time / 1000).replace(tzinfo=dt.timezone.utc) - end_time = dt.datetime.utcfromtimestamp(end_time / 1000).replace(tzinfo=dt.timezone.utc) + start_time = dt.datetime.fromtimestamp(start_time / 1000, tz=dt.timezone.utc) + end_time = dt.datetime.fromtimestamp(end_time / 1000, tz=dt.timezone.utc) + elif isinstance(start_time, dt.datetime) and isinstance(end_time, dt.datetime): if start_time.tzinfo is None or end_time.tzinfo is None: raise ValueError("start_time and end_time must be timezone-aware.") @@ -910,6 +911,25 @@ class DatabaseInteractions(SnapshotDataCache): return result + def get_all_rows_from_datacache(self, cache_name: str) -> pd.DataFrame: + """ + Retrieves all rows from the cache or database. + + :param cache_name: The cache or database table name. + :return: DataFrame containing all rows. + """ + # Retrieve cache + cache = self.get_cache(cache_name) + + # Case 1: Retrieve all rows from the cache + if isinstance(cache, RowBasedCache): + return pd.DataFrame.from_dict(cache.get_all_items(), orient='index') + elif isinstance(cache, TableBasedCache): + return cache.get_all_items() + + # Case 2: Fallback to retrieve all rows from the database using Database class + return self.db.get_all_rows(cache_name) + def _fetch_from_database_with_list_support(self, cache_name: str, filter_vals: List[tuple[str, Any]]) -> pd.DataFrame: """ @@ -1084,7 +1104,7 @@ class DatabaseInteractions(SnapshotDataCache): filter_vals.insert(0, ('tbl_key', key)) # Retrieve the row from the cache or database - rows = self.get_rows_from_datacache(cache_name=cache_name, filter_vals=filter_vals) + rows = self.get_rows_from_datacache(cache_name=cache_name, filter_vals=filter_vals, include_tbl_key=True) if rows is None or rows.empty: raise ValueError(f"Row not found in cache or database for {filter_vals}") @@ -1387,7 +1407,7 @@ class ServerInteractions(DatabaseInteractions): if start_datetime.tzinfo is None: raise ValueError("start_datetime is timezone naive. Please provide a timezone-aware datetime.") - end_datetime = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc) + end_datetime = dt.datetime.now(dt.timezone.utc) try: args = { @@ -1634,7 +1654,7 @@ class ServerInteractions(DatabaseInteractions): start_datetime = dt.datetime(year=2017, month=1, day=1, tzinfo=dt.timezone.utc) if end_datetime is None: - end_datetime = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc) + end_datetime = dt.datetime.now(dt.timezone.utc) if start_datetime > end_datetime: raise ValueError("Invalid start and end parameters: start_datetime must be before end_datetime.") diff --git a/src/Database.py b/src/Database.py index a4112c1..e1198b3 100644 --- a/src/Database.py +++ b/src/Database.py @@ -100,6 +100,17 @@ class Database: cur = con.cursor() cur.execute(sql, params) + def get_all_rows(self, table_name: str) -> pd.DataFrame: + """ + Retrieves all rows from a table. + + :param table_name: The name of the table. + :return: DataFrame containing all rows. + """ + sql_query = f"SELECT * FROM {table_name}" + with SQLite(self.db_file) as conn: + return pd.read_sql(sql_query, conn) + def get_item_where(self, item_name: str, table_name: str, filter_vals: Tuple[str, Any]) -> Any: """ Returns an item from a table where the filter results should isolate a single row. diff --git a/src/Strategies.py b/src/Strategies.py index 9ee7b9f..e38948a 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -33,8 +33,9 @@ class Strategies: size_limit=500, eviction_policy='deny', default_expiration=dt.timedelta(hours=24), - columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee", + columns=["creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components"]) + # Create a cache for strategy contexts to store strategy states and settings self.data_cache.create_cache( name='strategy_contexts', @@ -78,7 +79,8 @@ class Strategies: # Verify the existing strategy existing_strategy = self.data_cache.get_rows_from_datacache( cache_name='strategies', - filter_vals=[('tbl_key', tbl_key)] + filter_vals=[('tbl_key', tbl_key)], + include_tbl_key=True ) if existing_strategy.empty: return {"success": False, "message": "Strategy not found."} @@ -90,7 +92,8 @@ class Strategies: ] existing_strategy = self.data_cache.get_rows_from_datacache( cache_name='strategies', - filter_vals=filter_conditions + filter_vals=filter_conditions, + include_tbl_key = True ) if not existing_strategy.empty: return {"success": False, "message": "A strategy with this name already exists"} @@ -203,20 +206,26 @@ class Strategies: """ return self._save_strategy(strategy_data, default_source) - def delete_strategy(self, user_id: int, name: str) -> dict: + def delete_strategy(self, tbl_key: str) -> dict: + """ + Deletes a strategy identified by its tbl_key. + + :param tbl_key: The unique identifier of the strategy to delete. + :return: A dictionary indicating success or failure with an appropriate message. + """ try: self.data_cache.remove_row_from_datacache( cache_name='strategies', - filter_vals=[('creator', user_id), ('name', name)] + filter_vals=[('tbl_key', tbl_key)] ) return {"success": True, "message": "Strategy deleted successfully."} except Exception as e: - logger.error(f"Failed to delete strategy '{name}' for user '{user_id}': {e}", exc_info=True) + logger.error(f"Failed to delete strategy '{tbl_key}': {e}", exc_info=True) return {"success": False, "message": f"Failed to delete strategy: {str(e)}"} def get_all_strategy_names(self, user_id: int) -> list | None: """ - Return a list of all strategy names stored in the cache or database. + Return a list of all public and user strategy names stored in the cache or database. """ # Fetch all strategy names from the cache or database strategies_df = self.get_all_strategies(user_id, 'df') @@ -225,25 +234,43 @@ class Strategies: return strategies_df['name'].tolist() return None - def get_all_strategies(self, user_id: int, form: str): + def get_all_strategies(self, user_id: int | None, form: str, include_all: bool = False): """ Return stored strategies in various formats. - :param user_id: the id of the user making the request. + :param user_id: The ID of the user making the request. If None, fetch all public strategies. :param form: The desired format ('obj', 'json', or 'dict'). + :param include_all: If True, fetch all strategies (public and private) for all users. :return: A list of strategies in the requested format. """ valid_forms = {'df', 'json', 'dict'} if form not in valid_forms: raise ValueError(f"Invalid form '{form}'. Expected one of {valid_forms}.") - # Fetch all public strategies and user's strategies from the cache or database - public_df = self.data_cache.get_rows_from_datacache(cache_name='strategies', filter_vals=[('public', 1)]) - user_df = self.data_cache.get_rows_from_datacache(cache_name='strategies', filter_vals=[('creator', user_id), - ('public', 0)]) + if include_all: + # Fetch all strategies regardless of user or public status + strategies_df = self.data_cache.get_all_rows_from_datacache( + cache_name='strategies', + ) + else: + # Fetch public strategies + public_df = self.data_cache.get_rows_from_datacache( + cache_name='strategies', + filter_vals=[('public', 1)], + include_tbl_key=True + ) - # Concatenate the two DataFrames (rows from public and user-created strategies) - strategies_df = pd.concat([public_df, user_df], ignore_index=True) + if user_id is not None: + # Fetch user-specific private strategies + user_df = self.data_cache.get_rows_from_datacache( + cache_name='strategies', + filter_vals=[('creator', user_id), ('public', 0)], + include_tbl_key=True + ) + # Concatenate the two DataFrames + strategies_df = pd.concat([public_df, user_df], ignore_index=True) + else: + strategies_df = public_df # Return None if no strategies found if strategies_df.empty: @@ -257,30 +284,23 @@ class Strategies: elif form == 'dict': return strategies_df.to_dict('records') - def get_strategy_by_name(self, user_id: int, name: str) -> dict[str, Any] | None: + def get_strategy_by_tbl_key(self, tbl_key: str) -> dict[str, Any] | None: """ - Retrieve a strategy object by name. + Retrieve a strategy object by tbl_key. - :param user_id: The ID of the user making the request. - :param name: The name of the strategy to retrieve. - :return: The strategy DataFrame row if found, otherwise None. + :param tbl_key: The unique identifier of the strategy. + :return: The strategy dictionary if found, else None. """ - # Fetch all strategies (public and user-specific) as a DataFrame - strategies_df = self.get_all_strategies(user_id, 'df') + strategies_df = self.get_all_strategies(None, 'df', include_all=True) - # Ensure that strategies_df is not None if strategies_df is None: return None - # Filter the DataFrame to find the strategy by name (exact match) - name = name - filtered_strategies = strategies_df.query('name == @name') + filtered_strategies = strategies_df.query('tbl_key == @tbl_key') - # Return None if no matching strategy is found if filtered_strategies.empty: return None - # Get the strategy row as a dictionary strategy_row = filtered_strategies.iloc[0].to_dict() # Deserialize the 'strategy_components' field @@ -306,7 +326,8 @@ class Strategies: try: # Fetch the current stats strategy = self.data_cache.get_rows_from_datacache(cache_name='strategies', - filter_vals=[('tbl_key', strategy_id)]) + filter_vals=[('tbl_key', strategy_id)], + include_tbl_key=True) if strategy.empty: logger.warning(f"Strategy ID {strategy_id} not found for stats update.") @@ -338,7 +359,7 @@ class Strategies: """ try: # Extract identifiers - strategy_id = strategy_data.get('id') or strategy_data.get('tbl_key') + strategy_id = strategy_data.get('tbl_key') strategy_name = strategy_data.get('name') user_id = strategy_data.get('creator') @@ -420,7 +441,9 @@ class Strategies: Loops through and executes all activated strategies. """ try: - active_strategies = self.data_cache.get_rows_from_datacache('strategies', [('active', True)]) + active_strategies = self.data_cache.get_rows_from_datacache('strategies', + [('active', True)], + include_tbl_key=True) if active_strategies.empty: logger.info("No active strategies to execute.") return # No active strategies to execute @@ -430,22 +453,22 @@ class Strategies: logger.error(f"Error updating strategies: {e}", exc_info=True) traceback.print_exc() - def update_stats(self, strategy_id: str, stats: dict) -> None: + def update_stats(self, tbl_key: str, stats: dict) -> None: """ Updates the strategy's statistics with the provided stats. - :param strategy_id: Identifier of the strategy (tbl_key). + :param tbl_key: Identifier of the strategy (tbl_key). :param stats: Dictionary containing statistics to update. """ try: # Fetch the current strategy data strategy = self.data_cache.get_rows_from_datacache( cache_name='strategies', - filter_vals=[('tbl_key', strategy_id)] - ) + filter_vals=[('tbl_key', tbl_key)], + include_tbl_key=True) if strategy.empty: - logger.warning(f"Strategy ID {strategy_id} not found for stats update.") + logger.warning(f"Strategy ID {tbl_key} not found for stats update.") return strategy_row = strategy.iloc[0].to_dict() @@ -460,14 +483,14 @@ class Strategies: # Update the stats in the data cache self.data_cache.modify_datacache_item( cache_name='strategies', - filter_vals=[('tbl_key', strategy_id)], + filter_vals=[('tbl_key', tbl_key)], field_names=('stats',), new_values=(updated_stats_serialized,), - key=strategy_id, + key=tbl_key, overwrite='tbl_key' ) - logger.info(f"Updated stats for strategy '{strategy_id}': {current_stats}") + logger.info(f"Updated stats for strategy '{tbl_key}': {current_stats}") except Exception as e: - logger.error(f"Error updating stats for strategy '{strategy_id}': {e}", exc_info=True) \ No newline at end of file + logger.error(f"Error updating stats for strategy '{tbl_key}': {e}", exc_info=True) \ No newline at end of file diff --git a/src/backtesting.py b/src/backtesting.py index e53a420..c5462b4 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -1,6 +1,7 @@ # backtesting.py import logging +import math import time import uuid import eventlet @@ -92,19 +93,13 @@ class Backtester: try: # Check if the backtest already exists - existing_backtest = self.data_cache.get_rows_from_cache( - cache_name='tests', - filter_vals=[('tbl_key', backtest_key)] - ) + existing_backtest = self.data_cache.get_rows_from_cache(cache_name='tests', + filter_vals=[('tbl_key', backtest_key)]) if existing_backtest.empty: # Insert new backtest entry self.data_cache.insert_row_into_cache( - cache_name='tests', - columns=columns, - values=values, - key=backtest_key - ) + cache_name='tests', columns=columns, values=values, key=backtest_key) logger.debug(f"Inserted new backtest entry '{backtest_key}'.") else: # Update existing backtest entry (e.g., reset 'results' if needed) @@ -167,20 +162,24 @@ class Backtester: 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: + def validate_strategy_components(self, tbl_key: str, user_name: str) -> dict: """ Retrieves and validates the components of a user-defined strategy. Raises a ValueError if required components are missing or incorrectly formatted. + + :param tbl_key: The unique identifier of the strategy. + :param user_name: The username associated with the strategy. + :return: The validated strategy data. """ try: - user_strategy = self.strategies.get_strategy_by_name(user_id=int(user_id), name=strategy_name) + user_strategy = self.strategies.get_strategy_by_tbl_key(tbl_key) except ValueError: - logger.error(f"Invalid user_id '{user_id}'. Must be an integer.") - raise ValueError(f"Invalid user_id '{user_id}'. Must be an integer.") + logger.error(f"Invalid tbl_key '{tbl_key}'.") + raise ValueError(f"Invalid tbl_key '{tbl_key}'.") if not user_strategy: - logger.error(f"Strategy '{strategy_name}' not found for user '{user_name}'.") - raise ValueError(f"Strategy '{strategy_name}' not found for user '{user_name}'.") + logger.error(f"Strategy with tbl_key '{tbl_key}' not found for user '{user_name}'.") + raise ValueError(f"Strategy with tbl_key '{tbl_key}' not found for user '{user_name}'.") strategy_components = user_strategy.get('strategy_components', {}) generated_code = strategy_components.get('generated_code') @@ -353,6 +352,8 @@ class Backtester: Sends progress updates to the client via WebSocket. """ + tbl_key = msg_data.get('strategy') # Expecting tbl_key instead of strategy_name + def execute_backtest(): try: # **Convert 'time' to 'datetime' if necessary** @@ -433,7 +434,7 @@ class Backtester: logger.info("Backtest executed successfully.") # Invoke the callback with all necessary parameters - self.backtest_callback(user_name, backtest_name, user_id, strategy_name, + self.backtest_callback(user_name, backtest_name, tbl_key, strategy_instance.strategy_instance_id, socket_conn_id, backtest_key, backtest_results) @@ -451,7 +452,7 @@ class Backtester: } # Invoke callback with failure details to ensure cleanup - self.backtest_callback(user_name, backtest_name, user_id, strategy_name, + self.backtest_callback(user_name, backtest_name, tbl_key, strategy_instance.strategy_instance_id, socket_conn_id, backtest_key, failure_results) @@ -464,14 +465,23 @@ class Backtester: """ # Extract and define backtest parameters user_name = msg_data.get('user_name') - strategy_name = msg_data.get('strategy') - backtest_name = f"{strategy_name}_backtest" + tbl_key = msg_data.get('strategy') # Expecting tbl_key instead of strategy_name + backtest_name = msg_data.get('backtest_name') # Use the client-provided backtest_name + + if not backtest_name: + # If backtest_name is not provided, generate a unique name + backtest_name = f"{tbl_key}_backtest" + + # To ensure uniqueness, append a UUID if necessary + # Alternatively, rely on the client to provide a unique name + # backtest_name = f"{backtest_name}_{uuid.uuid4()}" + strategy_instance_id = f"test_{uuid.uuid4()}" backtest_key = f"backtest:{user_name}:{backtest_name}" # Retrieve the user strategy and validate it. try: - user_strategy = self.validate_strategy_components(user_id, strategy_name, user_name) + user_strategy = self.validate_strategy_components(tbl_key, user_name) except ValueError as ve: return {"error": str(ve)} @@ -486,8 +496,8 @@ class Backtester: # Instantiate BacktestStrategyInstance strategy_instance = BacktestStrategyInstance( strategy_instance_id=strategy_instance_id, - strategy_id=user_strategy.get("id"), - strategy_name=strategy_name, + strategy_id=tbl_key, # Use tbl_key as strategy_id + strategy_name=user_strategy.get("name"), user_id=int(user_id), generated_code=strategy_components.get("generated_code", ""), data_cache=self.data_cache, @@ -509,7 +519,7 @@ class Backtester: backtest_name=backtest_name, user_id=user_id, backtest_key=backtest_key, - strategy_name=strategy_name, + strategy_name=user_strategy.get("name"), precomputed_indicators=precomputed_indicators ) @@ -517,22 +527,54 @@ class Backtester: return {"status": "started", "backtest_name": backtest_name} # Define the backtest callback - def backtest_callback(self, user_name, backtest_name, user_id, strategy_name, - strategy_instance_id, socket_conn_id,backtest_key, results): + def backtest_callback(self, user_name, backtest_name, tbl_key, + strategy_instance_id, socket_conn_id, backtest_key, results): + """ + Callback to handle the completion of a backtest. + - Updates strategy stats. + - Emits results to the client. + - Ensures cleanup of backtest resources. + """ try: - if results.get("success") is False: + # If backtest failed + if not results.get("success", False): self.store_backtest_results(backtest_key, results) logger.error(f"Backtest '{backtest_name}' failed for user '{user_name}': {results.get('message')}") + + # If backtest succeeded else: + # Calculate additional stats + stats = self.calculate_stats(results) + results["stats"] = stats # Include stats in results + + # Store results and update strategy stats self.store_backtest_results(backtest_key, results) - self.update_strategy_stats(int(user_id), strategy_name, results) + self.update_strategy_stats(tbl_key, results) + + # Emit results to the client + logger.debug(f"Emitting backtest results: {results}") + sanitized_results = self.sanitize_results(results) self.socketio.emit( 'message', - {"reply": 'backtest_results', "data": {'test_id': backtest_name, "results": results}}, + { + "reply": 'backtest_results', + "data": { + 'test_id': backtest_name, + 'results': sanitized_results + } + }, room=socket_conn_id, ) + + logger.info(f"Backtest '{backtest_name}' completed successfully for user '{user_name}'.") + + except Exception as e: + logger.error(f"Error in backtest callback for '{backtest_name}': {str(e)}", exc_info=True) + finally: + # Ensure resources are cleaned up regardless of success or failure self.cleanup_backtest(backtest_key, strategy_instance_id) + logger.info(f"Cleanup completed for backtest '{backtest_name}' (key: {backtest_key}).") def start_periodic_purge(self, interval_seconds: int = 3600): """ @@ -619,62 +661,179 @@ class Backtester: logger.info("Exiting.") exit(0) - def update_strategy_stats(self, user_id: int, strategy_name: str, results: dict): + def calculate_stats(self, results: dict) -> dict: + """ + Calculate performance metrics from backtest results. + + :param results: Dictionary containing backtest results. + :return: Dictionary of calculated performance metrics. + """ + # Extract required data + initial_capital = results.get('initial_capital', 0) + final_value = results.get('final_portfolio_value', 0) + equity_curve = results.get('equity_curve', []) + trades = results.get('trades', []) + + # Ensure equity curve and trades are valid + if not equity_curve or not trades: + return {key: 0 for key in [ + 'total_return', 'sharpe_ratio', 'sortino_ratio', 'calmar_ratio', + 'volatility', 'max_drawdown', 'profit_factor', 'average_pnl', + 'number_of_trades', 'win_loss_ratio', 'max_consecutive_wins', + 'max_consecutive_losses', 'win_rate', 'loss_rate']} + + # Convert equity_curve to numpy array + equity_curve = np.array(equity_curve, dtype=float) + + # Calculate Returns + returns = np.array( + [(equity_curve[i] - equity_curve[i - 1]) / equity_curve[i - 1] + for i in range(1, len(equity_curve))], + dtype=float + ) + + # Performance Metrics + try: + total_return = (final_value - initial_capital) / initial_capital * 100 + except ZeroDivisionError: + total_return = 0 + + mean_return = np.mean(returns) + risk_free_rate = 0.0 # Adjust if needed + + # Filter downside returns and calculate downside standard deviation + downside_returns = returns[returns < 0] + downside_std = np.std(downside_returns) if len(downside_returns) > 0 else 1 + + # Sortino and Calmar Ratios + sortino_ratio = (mean_return - risk_free_rate) / downside_std if downside_std != 0 else 0 + max_drawdown = self.calculate_max_drawdown(equity_curve) + calmar_ratio = total_return / abs(max_drawdown) if max_drawdown != 0 else 0 + + # Volatility + volatility = np.std(returns) * np.sqrt(252) # Annualized + + # Trade Metrics + total_profit = sum(trade.get('pnl', 0) for trade in trades if trade.get('pnl', 0) > 0) + total_loss = abs(sum(trade.get('pnl', 0) for trade in trades if trade.get('pnl', 0) < 0)) + profit_factor = total_profit / total_loss if total_loss != 0 else float('inf') + average_pnl = sum(trade.get('pnl', 0) for trade in trades) / len(trades) if trades else 0 + + # Consecutive Wins and Losses + max_consec_wins, max_consec_losses = self.calculate_max_consecutive_trades(trades) + + # Win/Loss Rates + num_wins = sum(1 for trade in trades if trade.get('pnl', 0) > 0) + num_losses = sum(1 for trade in trades if trade.get('pnl', 0) < 0) + win_rate = (num_wins / len(trades)) * 100 if trades else 0 + loss_rate = (num_losses / len(trades)) * 100 if trades else 0 + + # Metrics Dictionary + return { + 'total_return': total_return, + 'sharpe_ratio': results.get('sharpe_ratio', 0), + 'sortino_ratio': sortino_ratio, + 'calmar_ratio': calmar_ratio, + 'volatility': volatility, + 'max_drawdown': max_drawdown, + 'profit_factor': profit_factor, + 'average_pnl': average_pnl, + 'number_of_trades': len(trades), + 'win_loss_ratio': num_wins / num_losses if num_losses > 0 else num_wins, + 'max_consecutive_wins': max_consec_wins, + 'max_consecutive_losses': max_consec_losses, + 'win_rate': win_rate, + 'loss_rate': loss_rate, + } + + def sanitize_results(self, results): + import math + for key, value in results.items(): + if isinstance(value, float) and (math.isinf(value) or math.isnan(value)): + results[key] = None # Replace `inf` or `nan` with `None` + elif isinstance(value, list): + results[key] = self.sanitize_results_in_list(value) + elif isinstance(value, dict): + results[key] = self.sanitize_results(value) + return results + + def sanitize_results_in_list(self, results_list): + sanitized_list = [] + for item in results_list: + if isinstance(item, dict): + sanitized_list.append(self.sanitize_results(item)) + elif isinstance(item, float) and (math.isinf(item) or math.isnan(item)): + sanitized_list.append(None) + else: + sanitized_list.append(item) + return sanitized_list + + def update_strategy_stats(self, tbl_key: str, results: dict): """ Update the strategy stats with the backtest results. - :param user_id: ID of the user. - :param strategy_name: Name of the strategy. + :param tbl_key: The unique identifier of the strategy. :param results: Dictionary containing backtest results. """ - strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name) + strategy = self.strategies.get_strategy_by_tbl_key(tbl_key) if strategy: - strategy_id = strategy.get('id') or strategy.get('tbl_key') - initial_capital = results.get('initial_capital') - final_value = results.get('final_portfolio_value') - equity_curve = results.get('equity_curve', []) + # Calculate metrics + stats = self.calculate_stats(results) - # Calculate returns based on the equity curve - returns = self.calculate_returns(equity_curve) - trades = results.get('trades', []) + # Update the strategy's stats using the Strategies class + self.strategies.update_stats(tbl_key, stats) - if returns and trades: - returns = np.array(returns) - equity_curve = np.array(equity_curve) - - total_return = (final_value - initial_capital) / initial_capital * 100 - - risk_free_rate = 0.0 # Modify as needed - mean_return = np.mean(returns) - std_return = np.std(returns) - sharpe_ratio = (mean_return - risk_free_rate) / std_return if std_return != 0 else 0 - - running_max = np.maximum.accumulate(equity_curve) - drawdowns = (equity_curve - running_max) / running_max - max_drawdown = np.min(drawdowns) * 100 - - num_trades = len(trades) - wins = sum(1 for trade in trades if trade.get('pnl', 0) > 0) - losses = num_trades - wins - win_loss_ratio = wins / losses if losses != 0 else wins - - stats = { - 'total_return': total_return, - 'sharpe_ratio': sharpe_ratio, - 'max_drawdown': max_drawdown, - 'number_of_trades': num_trades, - 'win_loss_ratio': win_loss_ratio, - } - - # Update the strategy's stats using the Strategies class - self.strategies.update_stats(strategy_id, stats) - - logger.info(f"Strategy '{strategy_name}' stats updated successfully.") - else: - logger.warning("Missing 'returns' or 'trades' data for statistics calculation.") + logger.info(f"Strategy with tbl_key '{tbl_key}' stats updated successfully.") else: - logger.error(f"Strategy '{strategy_name}' not found for user '{user_id}'.") + logger.error(f"Strategy with tbl_key '{tbl_key}' not found.") + + @staticmethod + def calculate_max_drawdown(equity_curve) -> float: + """ + Calculate the maximum drawdown from the equity curve. + + :param equity_curve: List or numpy array of portfolio values. + :return: Maximum drawdown as a percentage. + """ + # Explicitly convert to numpy array if it's not already + equity_curve = np.array(equity_curve, dtype=float) + + # Calculate peaks and drawdowns + peak = np.maximum.accumulate(equity_curve) + drawdowns = (equity_curve - peak) / peak + max_drawdown = np.min(drawdowns) * 100 # Percentage + return max_drawdown + + @staticmethod + def calculate_max_consecutive_trades(trades: list) -> tuple: + """ + Calculate the maximum number of consecutive wins and losses. + + :param trades: List of trade dictionaries. + :return: Tuple of (max_consecutive_wins, max_consecutive_losses) + """ + max_consec_wins = 0 + max_consec_losses = 0 + current_wins = 0 + current_losses = 0 + + for trade in trades: + pnl = trade.get('pnl', 0) + if pnl > 0: + current_wins += 1 + current_losses = 0 + elif pnl < 0: + current_losses += 1 + current_wins = 0 + else: + current_wins = 0 + current_losses = 0 + + max_consec_wins = max(max_consec_wins, current_wins) + max_consec_losses = max(max_consec_losses, current_losses) + + return max_consec_wins, max_consec_losses def store_backtest_results(self, backtest_key: str, results: dict): """ Store the backtest results in the cache """ @@ -689,7 +848,8 @@ class Backtester: except Exception as e: logger.error(f"Error storing backtest results for '{backtest_key}': {e}", exc_info=True) - def calculate_returns(self, equity_curve: list) -> list: + @staticmethod + def calculate_returns(equity_curve: list) -> list: """ Calculate returns based on the equity curve. :param equity_curve: List of portfolio values over time. @@ -704,18 +864,4 @@ class Backtester: ret = (equity_curve[i] - equity_curve[i - 1]) / equity_curve[i - 1] returns.append(ret) logger.debug(f"Calculated returns: {returns}") - return returns - - def extract_trades(self, strategy_instance: StrategyInstance) -> list: - """ - Extract trades from the strategy instance. - :param strategy_instance: The strategy instance. - :return: List of trades with profit information. - """ - # Since Trades class is not used, extract trades from TradeAnalyzer - # This method is now obsolete due to integration with TradeAnalyzer - # Instead, trades are extracted directly from 'results' in run_backtest - # Kept here for backward compatibility or future use - return [] - - + return returns \ No newline at end of file diff --git a/src/static/Strategies.js b/src/static/Strategies.js index 243ab7b..a018c65 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -103,53 +103,70 @@ class StratUIManager { if (this.targetEl) { // Clear existing content while (this.targetEl.firstChild) { + // Log before removing the child + console.log('Removing child:', 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'; + for (let i = 0; i < strategies.length; i++) { + const strat = strategies[i]; + console.log(`Processing strategy ${i + 1}/${strategies.length}:`, strat); - // 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); + try { + const strategyItem = document.createElement('div'); + strategyItem.className = 'strategy-item'; - // Strategy icon - const strategyIcon = document.createElement('div'); - strategyIcon.className = 'strategy-icon'; - // Open the form with strategy data when clicked - strategyIcon.addEventListener('click', () => { - this.displayForm('edit', strat).catch(error => { - console.error('Error displaying form:', error); + // Delete button + const deleteButton = document.createElement('button'); + deleteButton.className = 'delete-button'; + deleteButton.innerHTML = '✘'; + deleteButton.addEventListener('click', () => { + console.log(`Delete button clicked for strategy: ${strat.name}`); + if (this.onDeleteStrategy) { + this.onDeleteStrategy(strat.tbl_key); // Call the callback set by Strategies + } else { + console.error("Delete strategy callback is not set."); + } }); - }); + strategyItem.appendChild(deleteButton); + console.log('Delete button appended:', deleteButton); - // 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 icon + const strategyIcon = document.createElement('div'); + strategyIcon.className = 'strategy-icon'; + // Open the form with strategy data when clicked + strategyIcon.addEventListener('click', () => { + console.log(`Strategy icon clicked for strategy: ${strat.name}`); + this.displayForm('edit', strat).catch(error => { + console.error('Error displaying form:', error); + }); + }); - // Strategy hover details - const strategyHover = document.createElement('div'); - strategyHover.className = 'strategy-hover'; - strategyHover.innerHTML = `${strat.name || 'Unnamed Strategy'}
Stats: ${JSON.stringify(strat.stats, null, 2)}`; - strategyItem.appendChild(strategyHover); + // 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); + console.log('Strategy icon and name appended:', strategyIcon); - // Append to target element - this.targetEl.appendChild(strategyItem); + // Strategy hover details + const strategyHover = document.createElement('div'); + strategyHover.className = 'strategy-hover'; + strategyHover.innerHTML = `${strat.name || 'Unnamed Strategy'}
Stats: ${JSON.stringify(strat.stats, null, 2)}`; + strategyItem.appendChild(strategyHover); + console.log('Strategy hover details appended:', strategyHover); + + // Append to target element + this.targetEl.appendChild(strategyItem); + console.log('Strategy item appended to target element:', strategyItem); + } catch (error) { + console.error(`Error processing strategy ${i + 1}:`, error); + } } + + console.log('All strategies have been processed and appended.'); } else { console.error("Target element for updating strategies is not set."); } @@ -243,17 +260,20 @@ class StratDataManager { /** * Handles the deletion of a strategy. - * @param {Object} name - The name for the deleted strategy. + * @param {string} tbl_key - The tbl_key for the deleted strategy. */ - removeStrategy(name) { + removeStrategy(tbl_key) { try { - console.log("Strategy deleted:", name); - this.strategies = this.strategies.filter(strat => strat.name !== name); + console.log(`Removing strategy with tbl_key: ${tbl_key}`); + // Filter out the strategy with the matching tbl_key + this.strategies = this.strategies.filter(strat => strat.tbl_key !== tbl_key); + console.log("Remaining strategies:", this.strategies); } catch (error) { console.error("Error handling strategy deletion:", error.message); } } + /** * Handles batch updates for strategies, such as multiple configuration or performance updates. * @param {Object} data - The data containing batch updates for strategies. @@ -733,18 +753,33 @@ class Strategies { /** * Handles the deletion of a strategy. - * @param {Object} data - The data for the deleted strategy. + * @param {Object} response - The full response object from the server. */ - handleStrategyDeleted(data) { - const strategyName = data.strategy_name; // Extract the strategy name + handleStrategyDeleted(response) { + // Extract the message and tbl_key from the response + const success = response.message === "Strategy deleted successfully."; // Use the message to confirm success + const tbl_key = response.tbl_key; // Extract tbl_key directly - // Remove the strategy using the provided strategy_name - this.dataManager.removeStrategy(strategyName); + if (success) { + if (tbl_key) { + console.log(`Successfully deleted strategy with tbl_key: ${tbl_key}`); + // Remove the strategy using tbl_key + this.dataManager.removeStrategy(tbl_key); - // Update the UI to reflect the deletion - this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + // Update the UI to reflect the deletion + this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); + } else { + console.warn("tbl_key is missing in the server response, unable to remove strategy from UI."); + } + } else { + // Handle failure + console.error("Failed to delete strategy:", response.message || "Unknown error"); + alert(`Failed to delete strategy: ${response.message || "Unknown error"}`); + } } + + /** * Handles batch updates for strategies, such as multiple configuration or performance updates. * @param {Object} data - The data containing batch updates for strategies. @@ -848,15 +883,14 @@ class Strategies { /** * Deletes a strategy by its name. - * @param {string} name - The name of the strategy to be deleted. + * @param {string} tbl_key - The name of the strategy to be deleted. */ - deleteStrategy(name) { - console.log(`Deleting strategy: ${name}`); + deleteStrategy(tbl_key) { + console.log(`Deleting strategy: ${tbl_key}`); // Prepare data for server request const deleteData = { - user_name: this.data.user_name, - strategy_name: name + tbl_key: tbl_key }; // Send delete request to the server diff --git a/src/static/backtesting.js b/src/static/backtesting.js index c16c7f2..9d600b2 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -68,22 +68,21 @@ class Backtesting { const existingTest = this.tests.find(t => t.name === data.backtest_name); const availableStrategies = this.getAvailableStrategies(); + // Find the strategy that matches the tbl_key from the data + const matchedStrategy = availableStrategies.find(s => s.tbl_key === data.strategy); + 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' + strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy' }); } else { const newTest = { name: data.backtest_name, - strategy: availableStrategies.includes(data.strategy) - ? data.strategy - : availableStrategies[0] || 'default_strategy', + strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy', start_date: data.start_date, status: 'running', progress: 0, @@ -103,6 +102,7 @@ class Backtesting { + handleBacktestError(data) { console.error("Backtest error:", data.message); @@ -187,9 +187,13 @@ class Backtesting { // Helper Methods getAvailableStrategies() { - return this.ui.strats.dataManager.getAllStrategies().map(s => s.name); + return this.ui.strats.dataManager.getAllStrategies().map(s => ({ + name: s.name, // Strategy name + tbl_key: s.tbl_key // Unique identifier + })); } + updateProgressBar(progress) { if (this.progressBar) { console.log(`Updating progress bar to ${progress}%`); @@ -211,50 +215,70 @@ class Backtesting { displayTestResults(results) { this.showElement(this.resultsContainer); + let html = ` - 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 - `; + 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 ? results.run_duration.toFixed(2) : 'N/A'} seconds + `; // Equity Curve html += ` -

Equity Curve

-
+

Equity Curve

+
+ `; + + // Stats Section + if (results.stats) { + html += ` +

Statistics

+
`; + for (const [key, value] of Object.entries(results.stats)) { + const description = this.getStatDescription(key); + const formattedValue = value != null ? value.toFixed(2) : 'N/A'; // Safeguard against null or undefined + html += ` +
+ ${this.formatStatKey(key)}: + ${formattedValue} +
+ `; + } + html += `
`; + } // Trades Table if (results.trades && results.trades.length > 0) { html += ` -

Trades Executed

-
- - - - - - - - - - - `; +

Trades Executed

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

No trades were executed.

`; } @@ -263,6 +287,33 @@ class Backtesting { this.drawEquityCurveChart(results.equity_curve); } +// Helper to format stat keys + formatStatKey(key) { + return key.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); + } + +// Helper to get stat descriptions + getStatDescription(key) { + const descriptions = { + total_return: "The percentage change in portfolio value over the test period.", + sharpe_ratio: "A measure of risk-adjusted return; higher values indicate better risk-adjusted performance.", + sortino_ratio: "Similar to Sharpe Ratio but penalizes only downside volatility.", + calmar_ratio: "A measure of return relative to maximum drawdown.", + volatility: "The annualized standard deviation of portfolio returns, representing risk.", + max_drawdown: "The largest percentage loss from a peak to a trough in the equity curve.", + profit_factor: "The ratio of gross profits to gross losses; values above 1 indicate profitability.", + average_pnl: "The average profit or loss per trade.", + number_of_trades: "The total number of trades executed.", + win_loss_ratio: "The ratio of winning trades to losing trades.", + max_consecutive_wins: "The highest number of consecutive profitable trades.", + max_consecutive_losses: "The highest number of consecutive losing trades.", + win_rate: "The percentage of trades that were profitable.", + loss_rate: "The percentage of trades that resulted in a loss." + }; + return descriptions[key] || "No description available."; + } + + drawEquityCurveChart(equityCurve) { const equityCurveChart = document.getElementById('equity_curve_chart'); if (!equityCurveChart) { @@ -273,24 +324,20 @@ class Backtesting { // Clear previous chart equityCurveChart.innerHTML = ''; - // Get container dimensions const width = equityCurveChart.clientWidth || 600; const height = equityCurveChart.clientHeight || 300; const padding = 40; - // Calculate min and max values const minValue = Math.min(...equityCurve); const maxValue = Math.max(...equityCurve); const valueRange = maxValue - minValue || 1; - // Normalize data points const normalizedData = equityCurve.map((value, index) => { const x = padding + (index / (equityCurve.length - 1)) * (width - 2 * padding); const y = height - padding - ((value - minValue) / valueRange) * (height - 2 * padding); return { x, y }; }); - // Create SVG element const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("width", width); @@ -313,6 +360,22 @@ class Backtesting { yAxis.setAttribute("stroke", "black"); svg.appendChild(yAxis); + // Add labels + const xLabel = document.createElementNS(svgNS, "text"); + xLabel.textContent = "Time (Steps)"; + xLabel.setAttribute("x", width / 2); + xLabel.setAttribute("y", height - 5); + xLabel.setAttribute("text-anchor", "middle"); + svg.appendChild(xLabel); + + const yLabel = document.createElementNS(svgNS, "text"); + yLabel.textContent = "Equity Value"; + yLabel.setAttribute("x", -height / 2); + yLabel.setAttribute("y", 15); + yLabel.setAttribute("transform", "rotate(-90)"); + yLabel.setAttribute("text-anchor", "middle"); + svg.appendChild(yLabel); + // Draw equity curve const polyline = document.createElementNS(svgNS, "polyline"); const points = normalizedData.map(point => `${point.x},${point.y}`).join(' '); @@ -362,11 +425,13 @@ class Backtesting { // Validate and set strategy const availableStrategies = this.getAvailableStrategies(); - if (test.strategy && availableStrategies.includes(test.strategy)) { - this.strategyDropdown.value = test.strategy; + const matchedStrategy = availableStrategies.find(s => s.name === test.strategy || s.tbl_key === test.strategy); + + if (matchedStrategy) { + this.strategyDropdown.value = matchedStrategy.tbl_key; // Set dropdown to tbl_key } else { console.warn(`openTestDialog: Strategy "${test.strategy}" not found in dropdown. Defaulting to first available.`); - this.strategyDropdown.value = availableStrategies[0] || ''; + this.strategyDropdown.value = availableStrategies[0]?.tbl_key || ''; } // Populate other form fields @@ -399,7 +464,6 @@ class Backtesting { 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); @@ -426,12 +490,13 @@ class Backtesting { strategies.forEach(strategy => { const option = document.createElement('option'); - option.value = strategy; - option.text = strategy; + option.value = strategy.tbl_key; // Use tbl_key as the value + option.text = strategy.name; // Use strategy name as the display text this.strategyDropdown.appendChild(option); }); } + openForm(testName = null) { if (testName) { this.openTestDialog(testName); @@ -479,12 +544,17 @@ class Backtesting { } submitTest() { - const strategy = this.strategyDropdown ? this.strategyDropdown.value : null; + // Retrieve selected strategy + const strategyTblKey = this.strategyDropdown ? this.strategyDropdown.value : null; + const selectedStrategy = this.getAvailableStrategies().find(s => s.tbl_key === strategyTblKey); + const strategyName = selectedStrategy ? selectedStrategy.name : 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) { + // Validate strategy selection + if (!strategyTblKey || !strategyName) { alert("Please select a strategy."); console.log('submitTest: Submission failed - No strategy selected.'); return; @@ -493,6 +563,7 @@ class Backtesting { const now = new Date(); const startDate = new Date(start_date); + // Validate start date if (startDate > now) { alert("Start date cannot be in the future."); console.log('submitTest: Submission failed - Start date is in the future.'); @@ -505,20 +576,21 @@ class Backtesting { testName = this.currentTest; console.log(`submitTest: Editing existing backtest "${testName}".`); } else { - // Creating a new test without timestamp - testName = `${strategy}_backtest`; + // Creating a new test using the strategy's name for readability + testName = `${strategyName}_backtest`; console.log(`submitTest: Creating new backtest "${testName}".`); } - // Check if the test is already running + // Check if a test with the same name 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; } + // Prepare test data payload const testData = { - strategy, + strategy: strategyTblKey, // Use tbl_key as identifier start_date, capital, commission, @@ -532,13 +604,13 @@ class Backtesting { submitButton.disabled = true; } - // Submit the test + // Submit the test data to the backend 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) + // Re-enable the submit button after a delay setTimeout(() => { if (submitButton) { submitButton.disabled = false; @@ -547,6 +619,7 @@ class Backtesting { } + // Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format formatDateToLocalInput(date) { const pad = (num) => num.toString().padStart(2, '0'); diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index 71314d4..175a9e1 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -44,8 +44,9 @@ hr{ /* Styles for elements inside the popup.*/ .form-popup #SigName_div, label, h1, select, #Signal_type, span{ - text-align:center; - height:20px; + text-align:center; + height:20px; + display: inline-table; } .form-popup input[type="radio"], .form-popup input[type="checkbox"] { width: 15px;