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)
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)

View File

@ -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.")

View File

@ -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.

View File

@ -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)

View File

@ -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 []

View File

@ -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 = '&#10008;';
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

View File

@ -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');

View File

@ -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;