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:
parent
2c644147a4
commit
e1516a80cf
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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)
|
||||
logger.error(f"Error updating stats for strategy '{tbl_key}': {e}", exc_info=True)
|
||||
|
|
@ -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 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,
|
||||
}
|
||||
# Calculate metrics
|
||||
stats = self.calculate_stats(results)
|
||||
|
||||
# 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:
|
||||
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:
|
||||
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):
|
||||
""" 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.
|
||||
|
|
@ -705,17 +865,3 @@ class Backtester:
|
|||
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 []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,11 +103,17 @@ 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) {
|
||||
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');
|
||||
strategyItem.className = 'strategy-item';
|
||||
|
||||
|
|
@ -116,19 +122,22 @@ class StratUIManager {
|
|||
deleteButton.className = 'delete-button';
|
||||
deleteButton.innerHTML = '✘';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
console.log(`Delete button clicked for strategy: ${strat.name}`);
|
||||
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 {
|
||||
console.error("Delete strategy callback is not set.");
|
||||
}
|
||||
});
|
||||
strategyItem.appendChild(deleteButton);
|
||||
console.log('Delete button appended:', deleteButton);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
|
@ -140,16 +149,24 @@ class StratUIManager {
|
|||
strategyName.textContent = strat.name || 'Unnamed Strategy'; // Fallback for undefined
|
||||
strategyIcon.appendChild(strategyName);
|
||||
strategyItem.appendChild(strategyIcon);
|
||||
console.log('Strategy icon and name appended:', strategyIcon);
|
||||
|
||||
// Strategy hover details
|
||||
const strategyHover = document.createElement('div');
|
||||
strategyHover.className = 'strategy-hover';
|
||||
strategyHover.innerHTML = `<strong>${strat.name || 'Unnamed Strategy'}</strong><br>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,17 +753,32 @@ 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());
|
||||
} 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,11 +215,12 @@ class Backtesting {
|
|||
|
||||
displayTestResults(results) {
|
||||
this.showElement(this.resultsContainer);
|
||||
|
||||
let html = `
|
||||
<span><strong>Initial Capital:</strong> ${results.initial_capital}</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>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
|
||||
|
|
@ -224,6 +229,25 @@ class Backtesting {
|
|||
<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
|
||||
if (results.trades && results.trades.length > 0) {
|
||||
html += `
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ hr{
|
|||
.form-popup #SigName_div, label, h1, select, #Signal_type, span{
|
||||
text-align:center;
|
||||
height:20px;
|
||||
display: inline-table;
|
||||
}
|
||||
.form-popup input[type="radio"], .form-popup input[type="checkbox"] {
|
||||
width: 15px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue