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 += `
-
| Trade ID | -Size | -Price | -P&L | -
|---|
| Trade ID | +Size | +Price | +P&L | +
|---|---|---|---|
| ${trade.ref} | -${trade.size} | -${trade.price} | -${trade.pnl} | -
| ${trade.ref} | +${trade.size} | +${trade.price} | +${trade.pnl} | +
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;