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.

This commit is contained in:
Rob 2024-11-20 00:18:01 -04:00
parent 2c644147a4
commit e1516a80cf
8 changed files with 585 additions and 268 deletions

View File

@ -468,30 +468,34 @@ class BrighterTrades:
logger.error(f"Error editing strategy: {e}", exc_info=True) logger.error(f"Error editing strategy: {e}", exc_info=True)
return {"success": False, "message": "An unexpected error occurred while editing the strategy"} 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 :param data: Dictionary containing 'tbl_key' and 'user_name'.
:raises ValueError: If the strategy does not exist or there are issues :return: A dictionary indicating success or failure with an appropriate message and the tbl_key.
with removing it from the configuration file.
""" """
# Extract user_name from the data and get user_id # Validate tbl_key
user_name = data.get('user_name') tbl_key = data.get('tbl_key')
if not user_name: if not tbl_key:
return {"success": False, "message": "User not specified"} return {"success": False, "message": "tbl_key not provided", "tbl_key": None}
# Fetch the user_id using the user_name # Call the delete_strategy method to remove the strategy
user_id = self.get_user_info(user_name=user_name, info='User_id') result = self.strategies.delete_strategy(tbl_key=tbl_key)
if not user_id:
return {"success": False, "message": "User ID not found"}
strategy_name = data.get('strategy_name') # Return the result with tbl_key included
if not strategy_name: if result.get('success'):
return {"success": False, "message": "strategy_name not found"} return {
"success": True,
self.strategies.delete_strategy(user_id=user_id, name=strategy_name) "message": result.get('message'),
return {"success": True, "message": "Strategy deleted", "strategy_name": strategy_name} "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: def delete_signal(self, signal_name: str) -> None:
""" """
@ -517,7 +521,7 @@ class BrighterTrades:
def get_strategies_json(self, user_id) -> list: 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. :return: list - A list of dictionaries, each representing a strategy.
""" """
@ -777,10 +781,15 @@ class BrighterTrades:
if msg_type == 'delete_strategy': if msg_type == 'delete_strategy':
result = self.delete_strategy(msg_data) result = self.delete_strategy(msg_data)
if result.get('success'): if result.get('success'):
return standard_reply("strategy_deleted", return standard_reply("strategy_deleted", {
{"message": result.get('message'), "strategy_name": result.get('strategy_name')}) "message": result.get('message'),
"tbl_key": result.get('tbl_key') # Include tbl_key in the response
})
else: 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': if msg_type == 'close_trade':
self.close_trade(msg_data) self.close_trade(msg_data)

View File

@ -53,8 +53,9 @@ def estimate_record_count(start_time, end_time, timeframe: str) -> int:
# Convert milliseconds to datetime if needed # Convert milliseconds to datetime if needed
if isinstance(start_time, (int, float, np.integer)) and isinstance(end_time, (int, float, np.integer)): 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) start_time = dt.datetime.fromtimestamp(start_time / 1000, tz=dt.timezone.utc)
end_time = dt.datetime.utcfromtimestamp(end_time / 1000).replace(tzinfo=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): elif isinstance(start_time, dt.datetime) and isinstance(end_time, dt.datetime):
if start_time.tzinfo is None or end_time.tzinfo is None: if start_time.tzinfo is None or end_time.tzinfo is None:
raise ValueError("start_time and end_time must be timezone-aware.") raise ValueError("start_time and end_time must be timezone-aware.")
@ -910,6 +911,25 @@ class DatabaseInteractions(SnapshotDataCache):
return result 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, def _fetch_from_database_with_list_support(self, cache_name: str,
filter_vals: List[tuple[str, Any]]) -> pd.DataFrame: filter_vals: List[tuple[str, Any]]) -> pd.DataFrame:
""" """
@ -1084,7 +1104,7 @@ class DatabaseInteractions(SnapshotDataCache):
filter_vals.insert(0, ('tbl_key', key)) filter_vals.insert(0, ('tbl_key', key))
# Retrieve the row from the cache or database # 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: if rows is None or rows.empty:
raise ValueError(f"Row not found in cache or database for {filter_vals}") 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: if start_datetime.tzinfo is None:
raise ValueError("start_datetime is timezone naive. Please provide a timezone-aware datetime.") 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: try:
args = { args = {
@ -1634,7 +1654,7 @@ class ServerInteractions(DatabaseInteractions):
start_datetime = dt.datetime(year=2017, month=1, day=1, tzinfo=dt.timezone.utc) start_datetime = dt.datetime(year=2017, month=1, day=1, tzinfo=dt.timezone.utc)
if end_datetime is None: 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: if start_datetime > end_datetime:
raise ValueError("Invalid start and end parameters: start_datetime must be before end_datetime.") raise ValueError("Invalid start and end parameters: start_datetime must be before end_datetime.")

View File

@ -100,6 +100,17 @@ class Database:
cur = con.cursor() cur = con.cursor()
cur.execute(sql, params) 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: 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. Returns an item from a table where the filter results should isolate a single row.

View File

@ -33,8 +33,9 @@ class Strategies:
size_limit=500, size_limit=500,
eviction_policy='deny', eviction_policy='deny',
default_expiration=dt.timedelta(hours=24), 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"]) "tbl_key", "strategy_components"])
# Create a cache for strategy contexts to store strategy states and settings # Create a cache for strategy contexts to store strategy states and settings
self.data_cache.create_cache( self.data_cache.create_cache(
name='strategy_contexts', name='strategy_contexts',
@ -78,7 +79,8 @@ class Strategies:
# Verify the existing strategy # Verify the existing strategy
existing_strategy = self.data_cache.get_rows_from_datacache( existing_strategy = self.data_cache.get_rows_from_datacache(
cache_name='strategies', cache_name='strategies',
filter_vals=[('tbl_key', tbl_key)] filter_vals=[('tbl_key', tbl_key)],
include_tbl_key=True
) )
if existing_strategy.empty: if existing_strategy.empty:
return {"success": False, "message": "Strategy not found."} return {"success": False, "message": "Strategy not found."}
@ -90,7 +92,8 @@ class Strategies:
] ]
existing_strategy = self.data_cache.get_rows_from_datacache( existing_strategy = self.data_cache.get_rows_from_datacache(
cache_name='strategies', cache_name='strategies',
filter_vals=filter_conditions filter_vals=filter_conditions,
include_tbl_key = True
) )
if not existing_strategy.empty: if not existing_strategy.empty:
return {"success": False, "message": "A strategy with this name already exists"} 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) 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: try:
self.data_cache.remove_row_from_datacache( self.data_cache.remove_row_from_datacache(
cache_name='strategies', cache_name='strategies',
filter_vals=[('creator', user_id), ('name', name)] filter_vals=[('tbl_key', tbl_key)]
) )
return {"success": True, "message": "Strategy deleted successfully."} return {"success": True, "message": "Strategy deleted successfully."}
except Exception as e: 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)}"} return {"success": False, "message": f"Failed to delete strategy: {str(e)}"}
def get_all_strategy_names(self, user_id: int) -> list | None: 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 # Fetch all strategy names from the cache or database
strategies_df = self.get_all_strategies(user_id, 'df') strategies_df = self.get_all_strategies(user_id, 'df')
@ -225,25 +234,43 @@ class Strategies:
return strategies_df['name'].tolist() return strategies_df['name'].tolist()
return None 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. 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 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. :return: A list of strategies in the requested format.
""" """
valid_forms = {'df', 'json', 'dict'} valid_forms = {'df', 'json', 'dict'}
if form not in valid_forms: if form not in valid_forms:
raise ValueError(f"Invalid form '{form}'. Expected one of {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 if include_all:
public_df = self.data_cache.get_rows_from_datacache(cache_name='strategies', filter_vals=[('public', 1)]) # Fetch all strategies regardless of user or public status
user_df = self.data_cache.get_rows_from_datacache(cache_name='strategies', filter_vals=[('creator', user_id), strategies_df = self.data_cache.get_all_rows_from_datacache(
('public', 0)]) 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) 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) strategies_df = pd.concat([public_df, user_df], ignore_index=True)
else:
strategies_df = public_df
# Return None if no strategies found # Return None if no strategies found
if strategies_df.empty: if strategies_df.empty:
@ -257,30 +284,23 @@ class Strategies:
elif form == 'dict': elif form == 'dict':
return strategies_df.to_dict('records') 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 tbl_key: The unique identifier of the strategy.
:param name: The name of the strategy to retrieve. :return: The strategy dictionary if found, else None.
:return: The strategy DataFrame row if found, otherwise None.
""" """
# Fetch all strategies (public and user-specific) as a DataFrame strategies_df = self.get_all_strategies(None, 'df', include_all=True)
strategies_df = self.get_all_strategies(user_id, 'df')
# Ensure that strategies_df is not None
if strategies_df is None: if strategies_df is None:
return None return None
# Filter the DataFrame to find the strategy by name (exact match) filtered_strategies = strategies_df.query('tbl_key == @tbl_key')
name = name
filtered_strategies = strategies_df.query('name == @name')
# Return None if no matching strategy is found
if filtered_strategies.empty: if filtered_strategies.empty:
return None return None
# Get the strategy row as a dictionary
strategy_row = filtered_strategies.iloc[0].to_dict() strategy_row = filtered_strategies.iloc[0].to_dict()
# Deserialize the 'strategy_components' field # Deserialize the 'strategy_components' field
@ -306,7 +326,8 @@ class Strategies:
try: try:
# Fetch the current stats # Fetch the current stats
strategy = self.data_cache.get_rows_from_datacache(cache_name='strategies', 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: if strategy.empty:
logger.warning(f"Strategy ID {strategy_id} not found for stats update.") logger.warning(f"Strategy ID {strategy_id} not found for stats update.")
@ -338,7 +359,7 @@ class Strategies:
""" """
try: try:
# Extract identifiers # 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') strategy_name = strategy_data.get('name')
user_id = strategy_data.get('creator') user_id = strategy_data.get('creator')
@ -420,7 +441,9 @@ class Strategies:
Loops through and executes all activated strategies. Loops through and executes all activated strategies.
""" """
try: 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: if active_strategies.empty:
logger.info("No active strategies to execute.") logger.info("No active strategies to execute.")
return # 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) logger.error(f"Error updating strategies: {e}", exc_info=True)
traceback.print_exc() 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. 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. :param stats: Dictionary containing statistics to update.
""" """
try: try:
# Fetch the current strategy data # Fetch the current strategy data
strategy = self.data_cache.get_rows_from_datacache( strategy = self.data_cache.get_rows_from_datacache(
cache_name='strategies', cache_name='strategies',
filter_vals=[('tbl_key', strategy_id)] filter_vals=[('tbl_key', tbl_key)],
) include_tbl_key=True)
if strategy.empty: 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 return
strategy_row = strategy.iloc[0].to_dict() strategy_row = strategy.iloc[0].to_dict()
@ -460,14 +483,14 @@ class Strategies:
# Update the stats in the data cache # Update the stats in the data cache
self.data_cache.modify_datacache_item( self.data_cache.modify_datacache_item(
cache_name='strategies', cache_name='strategies',
filter_vals=[('tbl_key', strategy_id)], filter_vals=[('tbl_key', tbl_key)],
field_names=('stats',), field_names=('stats',),
new_values=(updated_stats_serialized,), new_values=(updated_stats_serialized,),
key=strategy_id, key=tbl_key,
overwrite='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: except Exception as e:
logger.error(f"Error updating stats for strategy '{strategy_id}': {e}", exc_info=True) logger.error(f"Error updating stats for strategy '{tbl_key}': {e}", exc_info=True)

View File

@ -1,6 +1,7 @@
# backtesting.py # backtesting.py
import logging import logging
import math
import time import time
import uuid import uuid
import eventlet import eventlet
@ -92,19 +93,13 @@ class Backtester:
try: try:
# Check if the backtest already exists # Check if the backtest already exists
existing_backtest = self.data_cache.get_rows_from_cache( existing_backtest = self.data_cache.get_rows_from_cache(cache_name='tests',
cache_name='tests', filter_vals=[('tbl_key', backtest_key)])
filter_vals=[('tbl_key', backtest_key)]
)
if existing_backtest.empty: if existing_backtest.empty:
# Insert new backtest entry # Insert new backtest entry
self.data_cache.insert_row_into_cache( self.data_cache.insert_row_into_cache(
cache_name='tests', cache_name='tests', columns=columns, values=values, key=backtest_key)
columns=columns,
values=values,
key=backtest_key
)
logger.debug(f"Inserted new backtest entry '{backtest_key}'.") logger.debug(f"Inserted new backtest entry '{backtest_key}'.")
else: else:
# Update existing backtest entry (e.g., reset 'results' if needed) # 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) logger.error(f"Error removing backtest '{backtest_key}': {e}", exc_info=True)
raise 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. Retrieves and validates the components of a user-defined strategy.
Raises a ValueError if required components are missing or incorrectly formatted. 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: 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: except ValueError:
logger.error(f"Invalid user_id '{user_id}'. Must be an integer.") logger.error(f"Invalid tbl_key '{tbl_key}'.")
raise ValueError(f"Invalid user_id '{user_id}'. Must be an integer.") raise ValueError(f"Invalid tbl_key '{tbl_key}'.")
if not user_strategy: if not user_strategy:
logger.error(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 '{strategy_name}' 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', {}) strategy_components = user_strategy.get('strategy_components', {})
generated_code = strategy_components.get('generated_code') generated_code = strategy_components.get('generated_code')
@ -353,6 +352,8 @@ class Backtester:
Sends progress updates to the client via WebSocket. Sends progress updates to the client via WebSocket.
""" """
tbl_key = msg_data.get('strategy') # Expecting tbl_key instead of strategy_name
def execute_backtest(): def execute_backtest():
try: try:
# **Convert 'time' to 'datetime' if necessary** # **Convert 'time' to 'datetime' if necessary**
@ -433,7 +434,7 @@ class Backtester:
logger.info("Backtest executed successfully.") logger.info("Backtest executed successfully.")
# Invoke the callback with all necessary parameters # 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, strategy_instance.strategy_instance_id, socket_conn_id,
backtest_key, backtest_results) backtest_key, backtest_results)
@ -451,7 +452,7 @@ class Backtester:
} }
# Invoke callback with failure details to ensure cleanup # 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, strategy_instance.strategy_instance_id, socket_conn_id,
backtest_key, failure_results) backtest_key, failure_results)
@ -464,14 +465,23 @@ class Backtester:
""" """
# Extract and define backtest parameters # Extract and define backtest parameters
user_name = msg_data.get('user_name') user_name = msg_data.get('user_name')
strategy_name = msg_data.get('strategy') tbl_key = msg_data.get('strategy') # Expecting tbl_key instead of strategy_name
backtest_name = f"{strategy_name}_backtest" 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()}" strategy_instance_id = f"test_{uuid.uuid4()}"
backtest_key = f"backtest:{user_name}:{backtest_name}" backtest_key = f"backtest:{user_name}:{backtest_name}"
# Retrieve the user strategy and validate it. # Retrieve the user strategy and validate it.
try: 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: except ValueError as ve:
return {"error": str(ve)} return {"error": str(ve)}
@ -486,8 +496,8 @@ class Backtester:
# Instantiate BacktestStrategyInstance # Instantiate BacktestStrategyInstance
strategy_instance = BacktestStrategyInstance( strategy_instance = BacktestStrategyInstance(
strategy_instance_id=strategy_instance_id, strategy_instance_id=strategy_instance_id,
strategy_id=user_strategy.get("id"), strategy_id=tbl_key, # Use tbl_key as strategy_id
strategy_name=strategy_name, strategy_name=user_strategy.get("name"),
user_id=int(user_id), user_id=int(user_id),
generated_code=strategy_components.get("generated_code", ""), generated_code=strategy_components.get("generated_code", ""),
data_cache=self.data_cache, data_cache=self.data_cache,
@ -509,7 +519,7 @@ class Backtester:
backtest_name=backtest_name, backtest_name=backtest_name,
user_id=user_id, user_id=user_id,
backtest_key=backtest_key, backtest_key=backtest_key,
strategy_name=strategy_name, strategy_name=user_strategy.get("name"),
precomputed_indicators=precomputed_indicators precomputed_indicators=precomputed_indicators
) )
@ -517,22 +527,54 @@ class Backtester:
return {"status": "started", "backtest_name": backtest_name} return {"status": "started", "backtest_name": backtest_name}
# Define the backtest callback # Define the backtest callback
def backtest_callback(self, user_name, backtest_name, user_id, strategy_name, def backtest_callback(self, user_name, backtest_name, tbl_key,
strategy_instance_id, socket_conn_id, backtest_key, results): 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: try:
if results.get("success") is False: # If backtest failed
if not results.get("success", False):
self.store_backtest_results(backtest_key, results) self.store_backtest_results(backtest_key, results)
logger.error(f"Backtest '{backtest_name}' failed for user '{user_name}': {results.get('message')}") logger.error(f"Backtest '{backtest_name}' failed for user '{user_name}': {results.get('message')}")
# If backtest succeeded
else: 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.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( self.socketio.emit(
'message', '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, 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: finally:
# Ensure resources are cleaned up regardless of success or failure
self.cleanup_backtest(backtest_key, strategy_instance_id) 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): def start_periodic_purge(self, interval_seconds: int = 3600):
""" """
@ -619,62 +661,179 @@ class Backtester:
logger.info("Exiting.") logger.info("Exiting.")
exit(0) 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. Update the strategy stats with the backtest results.
:param user_id: ID of the user. :param tbl_key: The unique identifier of the strategy.
:param strategy_name: Name of the strategy.
:param results: Dictionary containing backtest results. :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: if strategy:
strategy_id = strategy.get('id') or strategy.get('tbl_key') # Calculate metrics
initial_capital = results.get('initial_capital') stats = self.calculate_stats(results)
final_value = results.get('final_portfolio_value')
equity_curve = results.get('equity_curve', [])
# Calculate returns based on the equity curve
returns = self.calculate_returns(equity_curve)
trades = results.get('trades', [])
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 # Update the strategy's stats using the Strategies class
self.strategies.update_stats(strategy_id, stats) self.strategies.update_stats(tbl_key, stats)
logger.info(f"Strategy '{strategy_name}' stats updated successfully.") logger.info(f"Strategy with tbl_key '{tbl_key}' stats updated successfully.")
else: else:
logger.warning("Missing 'returns' or 'trades' data for statistics calculation.") 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: else:
logger.error(f"Strategy '{strategy_name}' not found for user '{user_id}'.") 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): def store_backtest_results(self, backtest_key: str, results: dict):
""" Store the backtest results in the cache """ """ Store the backtest results in the cache """
@ -689,7 +848,8 @@ class Backtester:
except Exception as e: except Exception as e:
logger.error(f"Error storing backtest results for '{backtest_key}': {e}", exc_info=True) 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. Calculate returns based on the equity curve.
:param equity_curve: List of portfolio values over time. :param equity_curve: List of portfolio values over time.
@ -705,17 +865,3 @@ class Backtester:
returns.append(ret) returns.append(ret)
logger.debug(f"Calculated returns: {returns}") logger.debug(f"Calculated returns: {returns}")
return 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 []

View File

@ -103,11 +103,17 @@ class StratUIManager {
if (this.targetEl) { if (this.targetEl) {
// Clear existing content // Clear existing content
while (this.targetEl.firstChild) { while (this.targetEl.firstChild) {
// Log before removing the child
console.log('Removing child:', this.targetEl.firstChild);
this.targetEl.removeChild(this.targetEl.firstChild); this.targetEl.removeChild(this.targetEl.firstChild);
} }
// Create and append new elements for all strategies // Create and append new elements for all strategies
for (let strat of strategies) { for (let i = 0; i < strategies.length; i++) {
const strat = strategies[i];
console.log(`Processing strategy ${i + 1}/${strategies.length}:`, strat);
try {
const strategyItem = document.createElement('div'); const strategyItem = document.createElement('div');
strategyItem.className = 'strategy-item'; strategyItem.className = 'strategy-item';
@ -116,19 +122,22 @@ class StratUIManager {
deleteButton.className = 'delete-button'; deleteButton.className = 'delete-button';
deleteButton.innerHTML = '&#10008;'; deleteButton.innerHTML = '&#10008;';
deleteButton.addEventListener('click', () => { deleteButton.addEventListener('click', () => {
console.log(`Delete button clicked for strategy: ${strat.name}`);
if (this.onDeleteStrategy) { if (this.onDeleteStrategy) {
this.onDeleteStrategy(strat.name); // Call the callback set by Strategies this.onDeleteStrategy(strat.tbl_key); // Call the callback set by Strategies
} else { } else {
console.error("Delete strategy callback is not set."); console.error("Delete strategy callback is not set.");
} }
}); });
strategyItem.appendChild(deleteButton); strategyItem.appendChild(deleteButton);
console.log('Delete button appended:', deleteButton);
// Strategy icon // Strategy icon
const strategyIcon = document.createElement('div'); const strategyIcon = document.createElement('div');
strategyIcon.className = 'strategy-icon'; strategyIcon.className = 'strategy-icon';
// Open the form with strategy data when clicked // Open the form with strategy data when clicked
strategyIcon.addEventListener('click', () => { strategyIcon.addEventListener('click', () => {
console.log(`Strategy icon clicked for strategy: ${strat.name}`);
this.displayForm('edit', strat).catch(error => { this.displayForm('edit', strat).catch(error => {
console.error('Error displaying form:', error); console.error('Error displaying form:', error);
}); });
@ -140,16 +149,24 @@ class StratUIManager {
strategyName.textContent = strat.name || 'Unnamed Strategy'; // Fallback for undefined strategyName.textContent = strat.name || 'Unnamed Strategy'; // Fallback for undefined
strategyIcon.appendChild(strategyName); strategyIcon.appendChild(strategyName);
strategyItem.appendChild(strategyIcon); strategyItem.appendChild(strategyIcon);
console.log('Strategy icon and name appended:', strategyIcon);
// Strategy hover details // Strategy hover details
const strategyHover = document.createElement('div'); const strategyHover = document.createElement('div');
strategyHover.className = 'strategy-hover'; strategyHover.className = 'strategy-hover';
strategyHover.innerHTML = `<strong>${strat.name || 'Unnamed Strategy'}</strong><br>Stats: ${JSON.stringify(strat.stats, null, 2)}`; strategyHover.innerHTML = `<strong>${strat.name || 'Unnamed Strategy'}</strong><br>Stats: ${JSON.stringify(strat.stats, null, 2)}`;
strategyItem.appendChild(strategyHover); strategyItem.appendChild(strategyHover);
console.log('Strategy hover details appended:', strategyHover);
// Append to target element // Append to target element
this.targetEl.appendChild(strategyItem); 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 { } else {
console.error("Target element for updating strategies is not set."); console.error("Target element for updating strategies is not set.");
} }
@ -243,17 +260,20 @@ class StratDataManager {
/** /**
* Handles the deletion of a strategy. * 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 { try {
console.log("Strategy deleted:", name); console.log(`Removing strategy with tbl_key: ${tbl_key}`);
this.strategies = this.strategies.filter(strat => strat.name !== name); // 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) { } catch (error) {
console.error("Error handling strategy deletion:", error.message); console.error("Error handling strategy deletion:", error.message);
} }
} }
/** /**
* Handles batch updates for strategies, such as multiple configuration or performance updates. * Handles batch updates for strategies, such as multiple configuration or performance updates.
* @param {Object} data - The data containing batch updates for strategies. * @param {Object} data - The data containing batch updates for strategies.
@ -733,17 +753,32 @@ class Strategies {
/** /**
* Handles the deletion of a strategy. * 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) { handleStrategyDeleted(response) {
const strategyName = data.strategy_name; // Extract the strategy name // 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 if (success) {
this.dataManager.removeStrategy(strategyName); 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 // Update the UI to reflect the deletion
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies()); 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. * Handles batch updates for strategies, such as multiple configuration or performance updates.
@ -848,15 +883,14 @@ class Strategies {
/** /**
* Deletes a strategy by its name. * Deletes a strategy by its name.
* @param {string} name - The name of the strategy to be deleted. * @param {string} tbl_key - The name of the strategy to be deleted.
*/ */
deleteStrategy(name) { deleteStrategy(tbl_key) {
console.log(`Deleting strategy: ${name}`); console.log(`Deleting strategy: ${tbl_key}`);
// Prepare data for server request // Prepare data for server request
const deleteData = { const deleteData = {
user_name: this.data.user_name, tbl_key: tbl_key
strategy_name: name
}; };
// Send delete request to the server // Send delete request to the server

View File

@ -68,22 +68,21 @@ class Backtesting {
const existingTest = this.tests.find(t => t.name === data.backtest_name); const existingTest = this.tests.find(t => t.name === data.backtest_name);
const availableStrategies = this.getAvailableStrategies(); 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) { if (existingTest) {
Object.assign(existingTest, { Object.assign(existingTest, {
status: 'running', status: 'running',
progress: 0, progress: 0,
start_date: data.start_date, start_date: data.start_date,
results: null, results: null,
strategy: availableStrategies.includes(data.strategy) strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy'
? data.strategy
: availableStrategies[0] || 'default_strategy'
}); });
} else { } else {
const newTest = { const newTest = {
name: data.backtest_name, name: data.backtest_name,
strategy: availableStrategies.includes(data.strategy) strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy',
? data.strategy
: availableStrategies[0] || 'default_strategy',
start_date: data.start_date, start_date: data.start_date,
status: 'running', status: 'running',
progress: 0, progress: 0,
@ -103,6 +102,7 @@ class Backtesting {
handleBacktestError(data) { handleBacktestError(data) {
console.error("Backtest error:", data.message); console.error("Backtest error:", data.message);
@ -187,9 +187,13 @@ class Backtesting {
// Helper Methods // Helper Methods
getAvailableStrategies() { 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) { updateProgressBar(progress) {
if (this.progressBar) { if (this.progressBar) {
console.log(`Updating progress bar to ${progress}%`); console.log(`Updating progress bar to ${progress}%`);
@ -211,11 +215,12 @@ class Backtesting {
displayTestResults(results) { displayTestResults(results) {
this.showElement(this.resultsContainer); this.showElement(this.resultsContainer);
let html = ` let html = `
<span><strong>Initial Capital:</strong> ${results.initial_capital}</span><br> <span><strong>Initial Capital:</strong> ${results.initial_capital}</span><br>
<span><strong>Final Portfolio Value:</strong> ${results.final_portfolio_value}</span><br> <span><strong>Final Portfolio Value:</strong> ${results.final_portfolio_value}</span><br>
<span><strong>Total Return:</strong> ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%</span><br> <span><strong>Total Return:</strong> ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%</span><br>
<span><strong>Run Duration:</strong> ${results.run_duration.toFixed(2)} seconds</span> <span><strong>Run Duration:</strong> ${results.run_duration ? results.run_duration.toFixed(2) : 'N/A'} seconds</span>
`; `;
// Equity Curve // Equity Curve
@ -224,6 +229,25 @@ class Backtesting {
<div id="equity_curve_chart" style="width: 100%; height: 300px;"></div> <div id="equity_curve_chart" style="width: 100%; height: 300px;"></div>
`; `;
// Stats Section
if (results.stats) {
html += `
<h4>Statistics</h4>
<div class="stats-container" style="display: flex; flex-wrap: wrap; gap: 10px;">
`;
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 += `
<div class="stat-item" title="${description}" style="flex: 1 1 30%; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<strong>${this.formatStatKey(key)}:</strong>
<span>${formattedValue}</span>
</div>
`;
}
html += `</div>`;
}
// Trades Table // Trades Table
if (results.trades && results.trades.length > 0) { if (results.trades && results.trades.length > 0) {
html += ` html += `
@ -263,6 +287,33 @@ class Backtesting {
this.drawEquityCurveChart(results.equity_curve); 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) { drawEquityCurveChart(equityCurve) {
const equityCurveChart = document.getElementById('equity_curve_chart'); const equityCurveChart = document.getElementById('equity_curve_chart');
if (!equityCurveChart) { if (!equityCurveChart) {
@ -273,24 +324,20 @@ class Backtesting {
// Clear previous chart // Clear previous chart
equityCurveChart.innerHTML = ''; equityCurveChart.innerHTML = '';
// Get container dimensions
const width = equityCurveChart.clientWidth || 600; const width = equityCurveChart.clientWidth || 600;
const height = equityCurveChart.clientHeight || 300; const height = equityCurveChart.clientHeight || 300;
const padding = 40; const padding = 40;
// Calculate min and max values
const minValue = Math.min(...equityCurve); const minValue = Math.min(...equityCurve);
const maxValue = Math.max(...equityCurve); const maxValue = Math.max(...equityCurve);
const valueRange = maxValue - minValue || 1; const valueRange = maxValue - minValue || 1;
// Normalize data points
const normalizedData = equityCurve.map((value, index) => { const normalizedData = equityCurve.map((value, index) => {
const x = padding + (index / (equityCurve.length - 1)) * (width - 2 * padding); const x = padding + (index / (equityCurve.length - 1)) * (width - 2 * padding);
const y = height - padding - ((value - minValue) / valueRange) * (height - 2 * padding); const y = height - padding - ((value - minValue) / valueRange) * (height - 2 * padding);
return { x, y }; return { x, y };
}); });
// Create SVG element
const svgNS = "http://www.w3.org/2000/svg"; const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg"); const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", width); svg.setAttribute("width", width);
@ -313,6 +360,22 @@ class Backtesting {
yAxis.setAttribute("stroke", "black"); yAxis.setAttribute("stroke", "black");
svg.appendChild(yAxis); 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 // Draw equity curve
const polyline = document.createElementNS(svgNS, "polyline"); const polyline = document.createElementNS(svgNS, "polyline");
const points = normalizedData.map(point => `${point.x},${point.y}`).join(' '); const points = normalizedData.map(point => `${point.x},${point.y}`).join(' ');
@ -362,11 +425,13 @@ class Backtesting {
// Validate and set strategy // Validate and set strategy
const availableStrategies = this.getAvailableStrategies(); const availableStrategies = this.getAvailableStrategies();
if (test.strategy && availableStrategies.includes(test.strategy)) { const matchedStrategy = availableStrategies.find(s => s.name === test.strategy || s.tbl_key === test.strategy);
this.strategyDropdown.value = test.strategy;
if (matchedStrategy) {
this.strategyDropdown.value = matchedStrategy.tbl_key; // Set dropdown to tbl_key
} else { } else {
console.warn(`openTestDialog: Strategy "${test.strategy}" not found in dropdown. Defaulting to first available.`); 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 // Populate other form fields
@ -399,7 +464,6 @@ class Backtesting {
console.log(`openTestDialog: Opened dialog for backtest "${test.name}".`); console.log(`openTestDialog: Opened dialog for backtest "${test.name}".`);
} }
runTest(testName) { runTest(testName) {
const testData = { name: testName, user_name: this.ui.data.user_name }; const testData = { name: testName, user_name: this.ui.data.user_name };
this.comms.sendToApp('run_backtest', testData); this.comms.sendToApp('run_backtest', testData);
@ -426,12 +490,13 @@ class Backtesting {
strategies.forEach(strategy => { strategies.forEach(strategy => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = strategy; option.value = strategy.tbl_key; // Use tbl_key as the value
option.text = strategy; option.text = strategy.name; // Use strategy name as the display text
this.strategyDropdown.appendChild(option); this.strategyDropdown.appendChild(option);
}); });
} }
openForm(testName = null) { openForm(testName = null) {
if (testName) { if (testName) {
this.openTestDialog(testName); this.openTestDialog(testName);
@ -479,12 +544,17 @@ class Backtesting {
} }
submitTest() { 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 start_date = document.getElementById('start_date').value;
const capital = parseFloat(document.getElementById('initial_capital').value) || 10000; const capital = parseFloat(document.getElementById('initial_capital').value) || 10000;
const commission = parseFloat(document.getElementById('commission').value) || 0.001; const commission = parseFloat(document.getElementById('commission').value) || 0.001;
if (!strategy) { // Validate strategy selection
if (!strategyTblKey || !strategyName) {
alert("Please select a strategy."); alert("Please select a strategy.");
console.log('submitTest: Submission failed - No strategy selected.'); console.log('submitTest: Submission failed - No strategy selected.');
return; return;
@ -493,6 +563,7 @@ class Backtesting {
const now = new Date(); const now = new Date();
const startDate = new Date(start_date); const startDate = new Date(start_date);
// Validate start date
if (startDate > now) { if (startDate > now) {
alert("Start date cannot be in the future."); alert("Start date cannot be in the future.");
console.log('submitTest: Submission failed - Start date is in the future.'); console.log('submitTest: Submission failed - Start date is in the future.');
@ -505,20 +576,21 @@ class Backtesting {
testName = this.currentTest; testName = this.currentTest;
console.log(`submitTest: Editing existing backtest "${testName}".`); console.log(`submitTest: Editing existing backtest "${testName}".`);
} else { } else {
// Creating a new test without timestamp // Creating a new test using the strategy's name for readability
testName = `${strategy}_backtest`; testName = `${strategyName}_backtest`;
console.log(`submitTest: Creating new backtest "${testName}".`); 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')) { if (this.tests.find(t => t.name === testName && t.status === 'running')) {
alert(`A test named "${testName}" is already running.`); alert(`A test named "${testName}" is already running.`);
console.log(`submitTest: Submission blocked - "${testName}" is already running.`); console.log(`submitTest: Submission blocked - "${testName}" is already running.`);
return; return;
} }
// Prepare test data payload
const testData = { const testData = {
strategy, strategy: strategyTblKey, // Use tbl_key as identifier
start_date, start_date,
capital, capital,
commission, commission,
@ -532,13 +604,13 @@ class Backtesting {
submitButton.disabled = true; submitButton.disabled = true;
} }
// Submit the test // Submit the test data to the backend
this.comms.sendToApp('submit_backtest', testData); this.comms.sendToApp('submit_backtest', testData);
// Log the submission and keep the form open for progress monitoring // Log the submission and keep the form open for progress monitoring
console.log('submitTest: Backtest data submitted and form remains 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(() => { setTimeout(() => {
if (submitButton) { if (submitButton) {
submitButton.disabled = false; submitButton.disabled = false;
@ -547,6 +619,7 @@ class Backtesting {
} }
// Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format // Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format
formatDateToLocalInput(date) { formatDateToLocalInput(date) {
const pad = (num) => num.toString().padStart(2, '0'); const pad = (num) => num.toString().padStart(2, '0');

View File

@ -46,6 +46,7 @@ hr{
.form-popup #SigName_div, label, h1, select, #Signal_type, span{ .form-popup #SigName_div, label, h1, select, #Signal_type, span{
text-align:center; text-align:center;
height:20px; height:20px;
display: inline-table;
} }
.form-popup input[type="radio"], .form-popup input[type="checkbox"] { .form-popup input[type="radio"], .form-popup input[type="checkbox"] {
width: 15px; width: 15px;