diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py
index 5153657..7e567ff 100644
--- a/src/BrighterTrades.py
+++ b/src/BrighterTrades.py
@@ -1,4 +1,5 @@
import json
+import logging
from typing import Any
from Users import Users
@@ -12,6 +13,8 @@ from indicators import Indicators
from Signals import Signals
from trade import Trades
+# Configure logging
+logger = logging.getLogger(__name__)
class BrighterTrades:
def __init__(self, socketio):
@@ -340,32 +343,56 @@ class BrighterTrades:
if not user_id:
return {"success": False, "message": "User ID not found"}
- # Validate data types
+ # Validate data types and contents
if not isinstance(data['name'], str) or not data['name'].strip():
return {"success": False, "message": "Invalid or empty strategy name"}
if not isinstance(data['workspace'], str) or not data['workspace'].strip():
return {"success": False, "message": "Invalid or empty workspace data"}
- if not isinstance(data['code'], dict) or not data['code']:
+ if not isinstance(data['code'], (dict, str)) or not data['code']:
return {"success": False, "message": "Invalid or empty strategy code"}
- # Serialize code to JSON string for storage
- code_json = json.dumps(data['code'])
+ try:
+ # Ensure 'code' is serialized as a JSON string
+ code_json = json.dumps(data['code']) if isinstance(data['code'], dict) else data['code']
+ except (TypeError, ValueError) as e:
+ return {"success": False, "message": f"Invalid strategy code: {str(e)}"}
# Prepare the strategy data for insertion
- strategy_data = {
- "creator": user_id,
- "name": data['name'].strip(),
- "workspace": data['workspace'].strip(),
- "code": code_json,
- "stats": data.get('stats', {}),
- "public": int(data.get('public', 0)),
- "fee": float(data.get('fee', 0.0))
- }
+ try:
+ strategy_data = {
+ "creator": user_id,
+ "name": data['name'].strip(),
+ "workspace": data['workspace'].strip(),
+ "code": code_json,
+ "stats": data.get('stats', {}),
+ "public": int(data.get('public', 0)),
+ "fee": float(data.get('fee', 0.0))
+ }
+ except Exception as e:
+ return {"success": False, "message": f"Error preparing strategy data: {str(e)}"}
- # The default source for undefined sources in the strategy.
- default_source = self.users.get_chart_view(user_name=user_name)
- # Save the new strategy (in both cache and database) and return the result.
- return self.strategies.new_strategy(strategy_data, default_source)
+ # The default source for undefined sources in the strategy
+ try:
+ default_source = self.users.get_chart_view(user_name=user_name)
+ except Exception as e:
+ return {"success": False, "message": f"Error fetching chart view: {str(e)}"}
+
+ # Save the new strategy (in both cache and database) and handle the result
+ try:
+ result = self.strategies.new_strategy(strategy_data, default_source)
+ if result.get("success"):
+ return {
+ "success": True,
+ "strategy": result.get("strategy"), # Strategy object without `strategy_components`
+ "updated_at": result.get("updated_at"),
+ "message": result.get("message", "Strategy created successfully")
+ }
+ else:
+ return {"success": False, "message": result.get("message", "Failed to create strategy")}
+ except Exception as e:
+ # Log unexpected exceptions for debugging
+ logger.error(f"Error creating new strategy: {e}", exc_info=True)
+ return {"success": False, "message": "An unexpected error occurred while creating the strategy"}
def received_edit_strategy(self, data: dict) -> dict:
"""
@@ -377,6 +404,7 @@ class BrighterTrades:
# Extract user_name and strategy name from the data
user_name = data.get('user_name')
strategy_name = data.get('name')
+
if not user_name:
return {"success": False, "message": "User not specified"}
if not strategy_name:
@@ -417,10 +445,28 @@ class BrighterTrades:
"tbl_key": tbl_key # Include the tbl_key to identify the strategy
}
- # The default source for undefined sources in the strategy.
- default_source = self.users.get_chart_view(user_name=user_name)
+ # Get the default source for undefined sources in the strategy
+ try:
+ default_source = self.users.get_chart_view(user_name=user_name)
+ except Exception as e:
+ return {"success": False, "message": f"Error fetching chart view: {str(e)}"}
+
# Call the edit_strategy method to update the strategy
- return self.strategies.edit_strategy(strategy_data, default_source)
+ try:
+ result = self.strategies.edit_strategy(strategy_data, default_source)
+ if result.get("success"):
+ return {
+ "success": True,
+ "strategy": result.get("strategy"), # Strategy object without `strategy_components`
+ "updated_at": result.get("updated_at"),
+ "message": result.get("message", "Strategy updated successfully")
+ }
+ else:
+ return {"success": False, "message": result.get("message", "Failed to update strategy")}
+ except Exception as e:
+ # Log unexpected exceptions
+ 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:
"""
@@ -593,10 +639,25 @@ class BrighterTrades:
return self.trades.get_trades('dict')
def delete_backtest(self, msg_data):
- """ Delete an existing backtest. """
+ """ Delete an existing backtest by interacting with the Backtester. """
backtest_name = msg_data.get('name')
- if backtest_name in self.backtests:
- del self.backtests[backtest_name]
+ user_name = msg_data.get('user_name')
+
+ if not backtest_name or not user_name:
+ return {"success": False, "message": "Missing backtest name or user name."}
+
+ # Construct the backtest_key based on Backtester’s naming convention
+ backtest_key = f"backtest:{user_name}:{backtest_name}"
+
+ try:
+ # Delegate the deletion to the Backtester
+ self.backtester.remove_backtest(backtest_key)
+ return {"success": True, "message": f"Backtest '{backtest_name}' deleted successfully.",
+ "name": backtest_name}
+ except KeyError:
+ return {"success": False, "message": f"Backtest '{backtest_name}' not found."}
+ except Exception as e:
+ return {"success": False, "message": f"Error deleting backtest: {str(e)}"}
def adjust_setting(self, user_name: str, setting: str, params: Any):
"""
@@ -729,12 +790,21 @@ class BrighterTrades:
return standard_reply("signal_created", r_data)
if msg_type == 'new_strategy':
- if r_data := self.received_new_strategy(msg_data):
- return standard_reply("strategy_created", r_data)
+ try:
+ if r_data := self.received_new_strategy(msg_data):
+ return standard_reply("strategy_created", r_data)
+ except Exception as e:
+ logger.error(f"Error processing new_strategy: {e}", exc_info=True)
+ return standard_reply("strategy_error", {"message": "Failed to create strategy."})
if msg_type == 'edit_strategy':
- if r_data := self.received_edit_strategy(msg_data):
- return standard_reply("strategy_created", r_data)
+ try:
+ if r_data := self.received_edit_strategy(msg_data):
+ return standard_reply("strategy_updated", r_data)
+ except Exception as e:
+ # Log the error for debugging
+ logger.error(f"Error processing edit_strategy: {e}", exc_info=True)
+ return standard_reply("strategy_error", {"message": "Failed to edit strategy."})
if msg_type == 'new_trade':
if r_data := self.received_new_trade(msg_data):
@@ -760,8 +830,8 @@ class BrighterTrades:
return standard_reply("backtest_submitted", resp)
if msg_type == 'delete_backtest':
- self.delete_backtest(msg_data)
- return standard_reply("backtest_deleted", {})
+ response = self.delete_backtest(msg_data)
+ return standard_reply("backtest_deleted", response)
if msg_type == 'reply':
# If the message is a reply log the response to the terminal.
diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py
index 20be0b9..2b66eba 100644
--- a/src/DataCache_v3.py
+++ b/src/DataCache_v3.py
@@ -340,6 +340,39 @@ class TableBasedCache:
except AttributeError as e:
raise AttributeError(f"Error in metadata processing: {e}")
+ def query_with_operator(self, conditions: List[Tuple[str, str, Any]]) -> pd.DataFrame:
+ """
+ Query rows based on conditions and return valid (non-expired) entries.
+ Todo: test and merge with query().
+ :param conditions: List of tuples containing column name, operator, and value.
+ :return: Filtered DataFrame.
+ """
+ self._purge_expired() # Remove expired rows before querying
+
+ # Start with the entire cache
+ result = self.cache.copy()
+
+ # Apply conditions using pandas filtering
+ if not result.empty and conditions:
+ for col, op, val in conditions:
+ if op.upper() == 'LIKE':
+ # Convert SQL LIKE pattern to regex
+ regex = '^' + val.replace('%', '.*') + '$'
+ result = result[result[col].astype(str).str.match(regex)]
+ elif op.upper() == 'IN' and isinstance(val, list):
+ result = result[result[col].isin(val)]
+ elif op == '=':
+ result = result[result[col] == val]
+ else:
+ # Add support for other operators as needed
+ raise ValueError(f"Unsupported operator '{op}' in filter conditions.")
+
+ # Remove the metadata and tbl_key columns for the result
+ if 'metadata' in result.columns:
+ result = result.drop(columns=['metadata'], errors='ignore')
+
+ return result
+
def query(self, conditions: List[Tuple[str, Any]]) -> pd.DataFrame:
"""Query rows based on conditions and return valid (non-expired) entries."""
self._purge_expired() # Remove expired rows before querying
@@ -479,6 +512,37 @@ class CacheManager:
# No result return an empty Dataframe
return pd.DataFrame()
+ def get_rows_from_cache_with_operator(
+ self,
+ cache_name: str,
+ filter_vals: list[tuple[str, str, Any]]
+ ) -> pd.DataFrame | None:
+ """
+ Retrieves rows from the cache if available.
+ Todo: merge this with the one above and test.
+ :param cache_name: The key used to identify the cache.
+ :param filter_vals: A list of tuples, each containing a column name, an operator, and the value to filter by.
+ :return: A DataFrame containing the requested rows, or None if no matching rows are found.
+ :raises ValueError: If the cache is not a DataFrame or does not contain DataFrames in the 'data' column.
+ """
+ # Check if the cache exists
+ if cache_name in self.caches:
+ cache = self.get_cache(cache_name)
+
+ # Ensure the cache contains DataFrames (required for querying)
+ if not isinstance(cache, (TableBasedCache, RowBasedCache)):
+ raise ValueError(f"Cache '{cache_name}' does not contain TableBasedCache or RowBasedCache.")
+
+ # Perform the query on the cache using filter_vals
+ filtered_cache = cache.query_with_operator(filter_vals) # Pass the list of filters
+
+ # If data is found in the cache, return it
+ if not filtered_cache.empty:
+ return filtered_cache
+
+ # No result return an empty DataFrame
+ return pd.DataFrame()
+
def get_cache_item(self, item_name: str, cache_name: str, filter_vals: tuple[str, any]) -> any:
"""
Retrieves a specific item from the cache.
@@ -755,6 +819,56 @@ class DatabaseInteractions(SnapshotDataCache):
super().__init__()
self.db = Database()
+ def get_rows_from_datacache_with_operator(
+ self,
+ cache_name: str,
+ filter_vals: list[tuple[str, str, Any]] = None,
+ key: str = None,
+ include_tbl_key: bool = False
+ ) -> pd.DataFrame | None:
+ """
+ Retrieves rows from the cache if available; otherwise, queries the database and caches the result.
+
+ :param include_tbl_key: If True, includes 'tbl_key' in the returned DataFrame.
+ :param key: Optional key to filter by 'tbl_key'.
+ :param cache_name: The key used to identify the cache (also the name of the database table).
+ :param filter_vals: A list of tuples, each containing a column name, an operator, and the value to filter by.
+ Example: [('strategy_instance_id', 'LIKE', 'test_%')]
+ :return: A DataFrame containing the requested rows, or None if no matching rows are found.
+ :raises ValueError: If the cache is not a DataFrame or does not contain DataFrames in the 'data' column.
+ """
+ # Ensure at least one of filter_vals or key is provided
+ if not filter_vals and not key:
+ raise ValueError("At least one of 'filter_vals' or 'key' must be provided.")
+
+ # Use an empty list if filter_vals is None
+ filter_vals = filter_vals or []
+
+ # Insert the key if provided
+ if key:
+ filter_vals.insert(0, ('tbl_key', '=', key))
+
+ # Perform the query on the cache using filter_vals
+ result = self.get_rows_from_cache_with_operator(cache_name, filter_vals)
+
+ # Fallback to database only if all operators are '='
+ if result.empty and all(op == '=' for _, op, _ in filter_vals):
+ # Extract (column, value) tuples for equality filters
+ equality_filters = [(col, val) for col, op, val in filter_vals if op == '=']
+ result = self._fetch_from_database(cache_name, equality_filters)
+
+ # Only use _fetch_from_database_with_list_support if any filter values are lists and all operators are '='
+ if result.empty and any(isinstance(val, list) for _, op, val in filter_vals) and all(
+ op == '=' for _, op, _ in filter_vals):
+ equality_filters = [(col, val) for col, op, val in filter_vals if op == '=']
+ result = self._fetch_from_database_with_list_support(cache_name, equality_filters)
+
+ # Remove 'tbl_key' unless include_tbl_key is True
+ if not include_tbl_key and 'tbl_key' in result.columns:
+ result = result.drop(columns=['tbl_key'], errors='ignore')
+
+ return result
+
def get_rows_from_datacache(self, cache_name: str, filter_vals: list[tuple[str, Any]] = None,
key: str = None, include_tbl_key: bool = False) -> pd.DataFrame | None:
"""
diff --git a/src/Strategies.py b/src/Strategies.py
index 9db05db..9ee7b9f 100644
--- a/src/Strategies.py
+++ b/src/Strategies.py
@@ -61,7 +61,6 @@ class Strategies:
self.active_instances: dict[tuple[int, str], StrategyInstance] = {} # Key: (user_id, strategy_id)
-
def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict:
"""
Saves a strategy to the cache and database. Handles both creation and editing.
@@ -72,9 +71,11 @@ class Strategies:
"""
is_edit = 'tbl_key' in strategy_data
try:
+ # Determine if this is an edit or a new creation
+ tbl_key = strategy_data.get('tbl_key', str(uuid.uuid4()))
+
if is_edit:
- # Editing an existing strategy
- tbl_key = strategy_data['tbl_key']
+ # Verify the existing strategy
existing_strategy = self.data_cache.get_rows_from_datacache(
cache_name='strategies',
filter_vals=[('tbl_key', tbl_key)]
@@ -82,11 +83,7 @@ class Strategies:
if existing_strategy.empty:
return {"success": False, "message": "Strategy not found."}
else:
- # Creating a new strategy
- # Generate a unique identifier first
- tbl_key = str(uuid.uuid4())
-
- # Check if a strategy with the same name already exists for this user
+ # Check for duplicate strategy name
filter_conditions = [
('creator', strategy_data.get('creator')),
('name', strategy_data['name'])
@@ -98,7 +95,7 @@ class Strategies:
if not existing_strategy.empty:
return {"success": False, "message": "A strategy with this name already exists"}
- # Validate and serialize 'workspace' (XML string)
+ # Validate and serialize 'workspace'
workspace_data = strategy_data.get('workspace')
if not isinstance(workspace_data, str) or not workspace_data.strip():
return {"success": False, "message": "Invalid or empty workspace data"}
@@ -110,10 +107,7 @@ class Strategies:
except (TypeError, ValueError):
return {"success": False, "message": "Invalid stats data format"}
- default_source = default_source.copy()
- strategy_id = tbl_key
-
- # Extract and validate 'code' as a dictionary
+ # Validate and parse 'code'
code = strategy_data.get('code')
if isinstance(code, str):
try:
@@ -124,99 +118,65 @@ class Strategies:
return {"success": False, "message": "Invalid JSON format for 'code'."}
elif isinstance(code, dict):
strategy_json = code
- # Serialize 'code' to JSON string
- try:
- serialized_code = json.dumps(code)
- strategy_data['code'] = serialized_code
- except (TypeError, ValueError):
- return {"success": False, "message": "Unable to serialize 'code' field."}
+ strategy_data['code'] = json.dumps(strategy_json) # Serialize for storage
else:
return {"success": False, "message": "'code' must be a JSON string or dictionary."}
- # Initialize PythonGenerator
- python_generator = PythonGenerator(default_source, strategy_id)
-
- # Generate strategy components (code, indicators, data_sources, flags)
+ # Generate Python components using PythonGenerator
+ python_generator = PythonGenerator(default_source.copy(), tbl_key)
strategy_components = python_generator.generate(strategy_json)
-
- # Add the combined strategy components to the data to be stored
strategy_data['strategy_components'] = json.dumps(strategy_components)
- if is_edit:
- # Editing existing strategy
- tbl_key = strategy_data['tbl_key']
- # Prepare the columns and values for the update
- columns = (
- "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components"
- )
- values = (
- strategy_data.get('creator'),
- strategy_data['name'],
- workspace_data, # Use the validated workspace data
- strategy_data['code'],
- stats_serialized, # Serialized stats
- bool(strategy_data.get('public', 0)),
- float(strategy_data.get('fee', 0.0)),
- tbl_key,
- strategy_data['strategy_components'] # Serialized strategy components
- )
+ # Prepare fields for database operations
+ columns = (
+ "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components"
+ )
+ values = (
+ strategy_data.get('creator'),
+ strategy_data['name'],
+ workspace_data,
+ strategy_data['code'],
+ stats_serialized,
+ bool(strategy_data.get('public', 0)),
+ float(strategy_data.get('fee', 0.0)),
+ tbl_key,
+ strategy_data['strategy_components']
+ )
- # Update the strategy in the database and cache
+ if is_edit:
+ # Update the existing strategy
self.data_cache.modify_datacache_item(
cache_name='strategies',
filter_vals=[('tbl_key', tbl_key)],
field_names=columns,
new_values=values,
key=tbl_key,
- overwrite='tbl_key' # Use 'tbl_key' to identify the entry to overwrite
+ overwrite='tbl_key'
)
-
- # Return success message
- return {"success": True, "message": "Strategy updated successfully"}
-
else:
- # Creating new strategy
- # Insert the strategy into the database and cache
+ # Insert a new strategy
self.data_cache.insert_row_into_datacache(
cache_name='strategies',
- columns=(
- "creator", "name", "workspace", "code", "stats",
- "public", "fee", 'tbl_key', 'strategy_components'
- ),
- values=(
- strategy_data.get('creator'),
- strategy_data['name'],
- strategy_data['workspace'],
- strategy_data['code'],
- stats_serialized,
- bool(strategy_data.get('public', 0)),
- float(strategy_data.get('fee', 0.0)),
- tbl_key,
- strategy_data['strategy_components']
- )
+ columns=columns,
+ values=values
)
- # Construct the saved strategy data to return
- saved_strategy = {
- "id": tbl_key, # Assuming tbl_key is used as a unique identifier
- "creator": strategy_data.get('creator'),
- "name": strategy_data['name'],
- "workspace": workspace_data, # Original workspace data
- "code": strategy_data['code'],
- "stats": stats_data,
- "public": bool(strategy_data.get('public', 0)),
- "fee": float(strategy_data.get('fee', 0.0))
- }
- # If everything is successful, return a success message along with the saved strategy data
- return {
- "success": True,
- "message": "Strategy created and saved successfully",
- "strategy": saved_strategy # Include the strategy data
- }
+ # Prepare the response
+ response_strategy = strategy_data.copy()
+ response_strategy.pop("strategy_components", None) # Remove the sensitive field
+
+ # Include `tbl_key` within the `strategy` object
+ response_strategy['tbl_key'] = tbl_key
+
+ return {
+ "success": True,
+ "message": "Strategy saved successfully",
+ "strategy": response_strategy,
+ "updated_at": dt.datetime.now(dt.timezone.utc).isoformat()
+ }
except Exception as e:
- # Catch any exceptions and return a failure message
- # Log the exception with traceback for debugging
+ # Handle exceptions and log errors
logger.error(f"Failed to save strategy: {e}", exc_info=True)
traceback.print_exc()
operation = "update" if is_edit else "create"
diff --git a/src/app.py b/src/app.py
index d02d45c..7ca628d 100644
--- a/src/app.py
+++ b/src/app.py
@@ -263,7 +263,7 @@ def signout():
@app.route('/login', methods=['POST'])
def login():
# Get the user_name and password from the form data
- username = request.form.get('user_name')
+ username = request.form.get('username')
password = request.form.get('password')
# Validate the input
@@ -295,21 +295,23 @@ def signup_submit():
validate_email(email)
except EmailNotValidError as e:
flash(message=f"Invalid email format: {e}")
- return None
+ return redirect('/signup') # Redirect back to signup page
# Validate user_name and password
if not username or not password:
flash(message="Missing user_name or password")
- return None
+ return redirect('/signup') # Redirect back to signup page
# Create a new user
success = brighter_trades.create_new_user(email=email, username=username, password=password)
if success:
session['user'] = username
- return redirect('/')
+ flash(message="Signup successful! You are now logged in.")
+ return redirect('/') # Redirect to the main page
else:
flash(message="An error has occurred during the signup process.")
- return None
+ return redirect('/signup') # Redirect back to signup page
+
@app.route('/api/indicator_init', methods=['POST', 'GET'])
diff --git a/src/backtesting.py b/src/backtesting.py
index 54e43fc..e53a420 100644
--- a/src/backtesting.py
+++ b/src/backtesting.py
@@ -148,6 +148,25 @@ class Backtester:
except Exception as e:
logger.error(f"Error during cleanup of backtest '{backtest_key}': {e}", exc_info=True)
+ def remove_backtest(self, backtest_key: str):
+ """
+ Remove a backtest from the 'tests' cache.
+ :param backtest_key: The unique key identifying the backtest.
+ """
+ try:
+ self.data_cache.remove_row_from_datacache(
+ cache_name='tests',
+ filter_vals=[('tbl_key', backtest_key)],
+ remove_from_db = False # Assuming tests are transient and not stored in DB
+ )
+ logger.info(f"Backtest '{backtest_key}' removed from 'tests' cache.")
+ except KeyError:
+ logger.error(f"Backtest '{backtest_key}' not found in 'tests' cache.")
+ raise KeyError(f"Backtest '{backtest_key}' not found.")
+ except Exception as e:
+ 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:
"""
Retrieves and validates the components of a user-defined strategy.
@@ -360,7 +379,8 @@ class Backtester:
precomputed_indicators=precomputed_indicators, # Pass precomputed indicators
socketio=self.socketio, # Pass SocketIO instance
socket_conn_id=socket_conn_id, # Pass SocketIO connection ID
- data_length=len(data_feed) # Pass data length for progress updates
+ data_length=len(data_feed), # Pass data length for progress updates
+ backtest_name=backtest_name # Pass backtest_name for progress updates
)
# Add data feed to Cerebro
@@ -395,7 +415,10 @@ class Backtester:
trades = strategy.trade_list
# Send 100% completion
- self.socketio.emit('message', {'reply': 'progress', 'data': {'progress': 100}}, room=socket_conn_id)
+ self.socketio.emit('message', {'reply': 'progress',
+ 'data': { 'test_id': backtest_name,
+ 'progress': 100}}
+ , room=socket_conn_id)
# Prepare the results to pass into the callback
backtest_results = {
@@ -531,36 +554,25 @@ class Backtester:
self.socketio.start_background_task(purge_task)
-
def cleanup_orphaned_backtest_contexts(self) -> None:
"""
Identifies and removes orphaned backtest contexts that do not have corresponding entries in 'tests' cache.
"""
try:
# Fetch all strategy_instance_ids from 'strategy_contexts' that start with 'test_'
- strategy_contexts_df = self.data_cache.get_rows_from_datacache(
+ strategy_contexts_df = self.data_cache.get_rows_from_datacache_with_operator(
cache_name='strategy_contexts',
- filter_vals=[] # Fetch all
+ filter_vals=[('strategy_instance_id', 'LIKE', 'test_%')]
)
if strategy_contexts_df.empty:
- logger.debug("No strategy contexts found for cleanup.")
- return
-
- # Filter contexts that are backtests (strategy_instance_id starts with 'test_')
- backtest_contexts = strategy_contexts_df[
- strategy_contexts_df['strategy_instance_id'].astype(str).str.startswith('test_')
- ]
-
- if backtest_contexts.empty:
logger.debug("No backtest contexts found for cleanup.")
return
- for _, row in backtest_contexts.iterrows():
+ # Iterate through each backtest context
+ for _, row in strategy_contexts_df.iterrows():
strategy_instance_id = row['strategy_instance_id']
# Check if this backtest exists in 'tests' cache
- # Since 'tests' cache uses 'backtest_key' as 'tbl_key', and it maps to 'strategy_instance_id'
- # We'll need to search 'tests' cache for the corresponding 'strategy_instance_id'
found = False
tests_cache = self.data_cache.get_cache('tests')
if isinstance(tests_cache, RowBasedCache):
@@ -573,10 +585,11 @@ class Backtester:
# Orphaned context found; proceed to remove it
self.data_cache.remove_row_from_datacache(
cache_name='strategy_contexts',
- filter_vals=[('strategy_instance_id', strategy_instance_id)],
+ filter_vals=[('strategy_instance_id', strategy_instance_id)], # Correct format
remove_from_db=True
)
- logger.info(f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.")
+ logger.info(
+ f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.")
except Exception as e:
logger.error(f"Error during cleanup of orphaned backtest contexts: {e}", exc_info=True)
diff --git a/src/mapped_strategy.py b/src/mapped_strategy.py
index 928781f..1a97537 100644
--- a/src/mapped_strategy.py
+++ b/src/mapped_strategy.py
@@ -22,6 +22,7 @@ class MappedStrategy(bt.Strategy):
('socketio', None), # SocketIO instance for emitting progress
('socket_conn_id', None), # Socket connection ID for emitting progress
('data_length', None), # Total number of data points for progress calculation
+ ('backtest_name', None) # Name of the backtest_name
)
def __init__(self):
@@ -48,6 +49,7 @@ class MappedStrategy(bt.Strategy):
# Initialize other needed variables
self.starting_balance = self.broker.getvalue()
self.last_progress = 0 # Initialize last_progress
+ self.backtest_name = self.p.backtest_name
self.bar_executed = 0 # Initialize bar_executed
def notify_order(self, order):
@@ -140,7 +142,8 @@ class MappedStrategy(bt.Strategy):
if self.p.socketio and self.p.socket_conn_id:
self.p.socketio.emit(
'message',
- {'reply': 'progress', 'data': {'progress': progress}},
+ {'reply': 'progress', 'data': { 'test_id': self.backtest_name,
+ 'progress': progress}},
room=self.p.socket_conn_id
)
logger.debug(f"Emitted progress: {progress}%")
diff --git a/src/static/Strategies.js b/src/static/Strategies.js
index 6544690..243ab7b 100644
--- a/src/static/Strategies.js
+++ b/src/static/Strategies.js
@@ -216,6 +216,16 @@ class StratDataManager {
this.strategies.push(data);
}
+ /**
+ * Retrieves a strategy by its tbl_key.
+ * @param {string} tbl_key - The tbl_key of the strategy to find.
+ * @returns {Object|null} - The strategy object or null if not found.
+ */
+ getStrategyById(tbl_key) {
+ return this.strategies.find(strategy => strategy.tbl_key === tbl_key) || null;
+ }
+
+
/**
* Handles updates to the strategy itself (e.g., configuration changes).
@@ -635,12 +645,18 @@ class Strategies {
}
/**
- * Handles strategy-related error messages from the server.
- * @param {Object} errorData - The data containing error details.
+ * Handles strategy-related errors sent from the server.
+ * @param {Object} errorData - The error message and additional details.
*/
handleStrategyError(errorData) {
console.error("Strategy Error:", errorData.message);
- alert(`Error: ${errorData.message}`);
+
+ // Display a user-friendly error message
+ if (errorData.message) {
+ alert(`Error: ${errorData.message}`);
+ } else {
+ alert("An unknown error occurred while processing the strategy.");
+ }
}
/**
@@ -665,6 +681,7 @@ class Strategies {
}
+
/**
* Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy.
@@ -681,16 +698,39 @@ class Strategies {
}
}
-
/**
* Handles updates to the strategy itself (e.g., configuration changes).
- * @param {Object} data - The updated strategy data.
+ * @param {Object} data - The server response containing strategy update metadata.
*/
handleStrategyUpdated(data) {
- this.dataManager.updateStrategyData(data);
- this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
+ if (data.success) {
+ console.log("Strategy updated successfully:", data);
+
+ // Locate the strategy in the local state by its tbl_key
+ const updatedStrategyKey = data.strategy.tbl_key;
+ const updatedAt = data.updated_at;
+
+ const strategy = this.dataManager.getStrategyById(updatedStrategyKey);
+ if (strategy) {
+ // Update the relevant strategy data
+ Object.assign(strategy, data.strategy);
+
+ // Update the `updated_at` field
+ strategy.updated_at = updatedAt;
+
+ // Refresh the UI to reflect the updated metadata
+ this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
+ } else {
+ console.warn("Updated strategy not found in local records:", updatedStrategyKey);
+ }
+ } else {
+ console.error("Failed to update strategy:", data.message);
+ alert(`Strategy update failed: ${data.message}`);
+ }
}
+
+
/**
* Handles the deletion of a strategy.
* @param {Object} data - The data for the deleted strategy.
diff --git a/src/static/backtesting.js b/src/static/backtesting.js
index 44fcb22..c16c7f2 100644
--- a/src/static/backtesting.js
+++ b/src/static/backtesting.js
@@ -4,6 +4,7 @@ class Backtesting {
this.comms = ui.data.comms;
this.tests = []; // Stores the list of saved backtests
this.target_id = 'backtest_display'; // The container to display backtests
+ this.currentTest = null; // Tracks the currently open test
// Register handlers for backtesting messages
this.comms.on('backtest_error', this.handleBacktestError.bind(this));
@@ -14,174 +15,272 @@ class Backtesting {
this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this));
}
+ // Initialize method to cache DOM elements
+ initialize() {
+ this.cacheDOMElements();
+ // Optionally, fetch saved tests or perform other initialization
+ }
+
+ cacheDOMElements() {
+ this.progressContainer = document.getElementById('backtest-progress-container');
+ this.progressBar = document.getElementById('progress_bar');
+ this.formElement = document.getElementById('backtest_form');
+ this.statusMessage = document.getElementById('backtest-status-message');
+ this.resultsContainer = document.getElementById('backtest-results');
+ this.resultsDisplay = document.getElementById('results_display');
+ this.backtestDraggableHeader = document.getElementById('backtest-form-header'); // Updated to single h1
+ this.backtestDisplay = document.getElementById(this.target_id);
+ this.strategyDropdown = document.getElementById('strategy_select');
+ this.equityCurveChart = document.getElementById('equity_curve_chart');
+ }
+
+ // Utility Methods
+ showElement(element) {
+ if (element) element.classList.add('show');
+ }
+
+ hideElement(element) {
+ if (element) element.classList.remove('show');
+ }
+
+ setText(element, text) {
+ if (element) element.textContent = text;
+ }
+
+ displayMessage(message, color = 'blue') {
+ if (this.statusMessage) {
+ this.showElement(this.statusMessage);
+ this.statusMessage.style.color = color;
+ this.setText(this.statusMessage, message);
+ }
+ }
+
+ clearMessage() {
+ if (this.statusMessage) {
+ this.hideElement(this.statusMessage);
+ this.setText(this.statusMessage, '');
+ }
+ }
+
+ // Event Handlers
handleBacktestSubmitted(data) {
- console.log("Backtest response received:", data.status);
if (data.status === "started") {
- // Show the progress bar or any other UI updates
- this.showRunningAnimation();
+ const existingTest = this.tests.find(t => t.name === data.backtest_name);
+ const availableStrategies = this.getAvailableStrategies();
- // Add the new backtest to the tests array
- const newTest = {
- name: data.backtest_name,
- strategy: data.strategy_name,
- start_date: data.start_date,
- // Include any other relevant data from the response if available
- };
- this.tests.push(newTest);
+ 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'
+ });
+ } else {
+ const newTest = {
+ name: data.backtest_name,
+ strategy: availableStrategies.includes(data.strategy)
+ ? data.strategy
+ : availableStrategies[0] || 'default_strategy',
+ start_date: data.start_date,
+ status: 'running',
+ progress: 0,
+ results: null
+ };
+ this.tests.push(newTest);
+ }
- // Update the HTML to reflect the new backtest
+ // Set currentTest to the test name received from backend
+ this.currentTest = data.backtest_name;
+ console.log(`handleBacktestSubmitted: Backtest "${data.backtest_name}" started.`);
+
+ this.showRunningAnimation(); // Display progress container
this.updateHTML();
}
}
+
+
handleBacktestError(data) {
console.error("Backtest error:", data.message);
- // Display error message in the status message area
- const statusMessage = document.getElementById('backtest-status-message');
- if (statusMessage) {
- statusMessage.style.display = 'block';
- statusMessage.style.color = 'red'; // Set text color to red for errors
- statusMessage.textContent = `Backtest error: ${data.message}`;
- } else {
- // Fallback to alert if the element is not found
- alert(`Backtest error: ${data.message}`);
+ const test = this.tests.find(t => t.name === this.currentTest);
+ if (test) {
+ test.status = 'error'; // Update the test status
+ console.log(`Backtest "${test.name}" encountered an error.`);
+ this.updateHTML();
}
- // Optionally hide progress bar and results
- const progressContainer = document.getElementById('backtest-progress-container');
- if (progressContainer) {
- progressContainer.classList.remove('show');
- }
- const resultsContainer = document.getElementById('backtest-results');
- if (resultsContainer) {
- resultsContainer.style.display = 'none';
- }
+ this.displayMessage(`Backtest error: ${data.message}`, 'red');
+
+ // Hide progress bar and results
+ this.hideElement(this.progressContainer);
+ this.hideElement(this.resultsContainer);
}
+
handleBacktestResults(data) {
- console.log("Backtest results received:", data.results);
- // Logic to stop running animation and display results
- this.stopRunningAnimation(data.results);
+ const test = this.tests.find(t => t.name === data.test_id);
+ if (test) {
+ Object.assign(test, {
+ status: 'complete',
+ progress: 100,
+ results: data.results
+ });
+
+ // Validate strategy
+ if (!test.strategy) {
+ console.warn(`Test "${test.name}" is missing a strategy. Setting a default.`);
+ test.strategy = 'default_strategy'; // Use a sensible default
+ }
+
+ this.updateHTML();
+ this.stopRunningAnimation(data.results);
+ }
}
handleProgress(data) {
- console.log("Backtest progress:", data.progress);
- // Logic to update progress bar
- this.updateProgressBar(data.progress);
+ console.log("handleProgress: Backtest progress:", data.progress);
+
+ if (!this.progressContainer) {
+ console.error('handleProgress: Progress container not found.');
+ return;
+ }
+
+ // Find the test that matches the progress update
+ const test = this.tests.find(t => t.name === data.test_id);
+ if (!test) {
+ console.warn(`handleProgress: Progress update received for unknown test: ${data.test_id}`);
+ return;
+ }
+
+ // Update the progress for the correct test
+ test.progress = data.progress;
+ console.log(`handleProgress: Updated progress for "${test.name}" to ${data.progress}%.`);
+
+ // If the currently open test matches, update the dialog's progress bar
+ if (this.currentTest === test.name && this.formElement.style.display === "grid") {
+ this.showElement(this.progressContainer); // Adds 'show' class
+ this.updateProgressBar(data.progress);
+ this.displayMessage('Backtest in progress...', 'blue');
+ console.log(`handleProgress: Progress container updated for "${test.name}".`);
+ }
}
+
handleBacktestsList(data) {
console.log("Backtests list received:", data.tests);
- // Logic to update backtesting UI
- this.set_data(data.tests);
+ // Update the tests array
+ this.tests = data.tests;
+ this.updateHTML();
}
handleBacktestDeleted(data) {
console.log(`Backtest "${data.name}" was successfully deleted.`);
- // Logic to refresh list of backtests
- this.fetchSavedTests();
+ // Remove the deleted test from the tests array
+ this.tests = this.tests.filter(t => t.name !== data.name);
+ this.updateHTML();
+ }
+
+ // Helper Methods
+ getAvailableStrategies() {
+ return this.ui.strats.dataManager.getAllStrategies().map(s => s.name);
}
updateProgressBar(progress) {
- const progressBar = document.getElementById('progress_bar');
- if (progressBar) {
+ if (this.progressBar) {
console.log(`Updating progress bar to ${progress}%`);
- progressBar.style.width = `${progress}%`;
- progressBar.textContent = `${progress}%`;
+ this.progressBar.style.width = `${progress}%`;
+ this.setText(this.progressBar, `${progress}%`);
} else {
console.log('Progress bar element not found');
}
}
-
showRunningAnimation() {
- const resultsContainer = document.getElementById('backtest-results');
- const resultsDisplay = document.getElementById('results_display');
- const progressContainer = document.getElementById('backtest-progress-container');
- const progressBar = document.getElementById('progress_bar');
- const statusMessage = document.getElementById('backtest-status-message');
-
- resultsContainer.style.display = 'none';
- progressContainer.classList.add('show'); // Use class to control display
- progressBar.style.width = '0%';
- progressBar.textContent = '0%';
- resultsDisplay.innerHTML = '';
- statusMessage.style.display = 'block';
- statusMessage.style.color = 'blue'; // Reset text color to blue
- statusMessage.textContent = 'Backtest started...';
+ this.hideElement(this.resultsContainer);
+ this.showElement(this.progressContainer);
+ this.updateProgressBar(0);
+ this.setText(this.progressBar, '0%');
+ this.resultsDisplay.innerHTML = ''; // Clear previous results
+ this.displayMessage('Backtest started...', 'blue');
}
-
-
displayTestResults(results) {
- const resultsContainer = document.getElementById('backtest-results');
- const resultsDisplay = document.getElementById('results_display');
-
- resultsContainer.style.display = 'block';
-
- // Calculate total return
- const totalReturn = (((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2);
-
- // Create HTML content
+ this.showElement(this.resultsContainer);
let html = `
- Initial Capital: ${results.initial_capital}
- Final Portfolio Value: ${results.final_portfolio_value}
- Total Return: ${totalReturn}%
- Run Duration: ${results.run_duration.toFixed(2)} seconds
- `;
+ Initial Capital: ${results.initial_capital}
+ Final Portfolio Value: ${results.final_portfolio_value}
+ Total Return: ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%
+ Run Duration: ${results.run_duration.toFixed(2)} seconds
+ `;
- // Add a container for the chart
- html += `
| Trade ID | -Size | -Price | -P&L | -
|---|
| Trade ID | +Size | +Price | +P&L | +
|---|---|---|---|
| ${trade.ref} | ${trade.size} | ${trade.price} | ${trade.pnl} | -
No trades were executed.
`; } - resultsDisplay.innerHTML = html; - - // Generate the equity curve chart + this.resultsDisplay.innerHTML = html; this.drawEquityCurveChart(results.equity_curve); } + drawEquityCurveChart(equityCurve) { - const chartContainer = document.getElementById('equity_curve_chart'); - if (!chartContainer) { + const equityCurveChart = document.getElementById('equity_curve_chart'); + if (!equityCurveChart) { console.error('Chart container not found'); return; } - // Get the dimensions of the container - const width = chartContainer.clientWidth || 600; - const height = chartContainer.clientHeight || 300; + // Clear previous chart + equityCurveChart.innerHTML = ''; + + // Get container dimensions + const width = equityCurveChart.clientWidth || 600; + const height = equityCurveChart.clientHeight || 300; const padding = 40; - // Find min and max values + // Calculate min and max values const minValue = Math.min(...equityCurve); const maxValue = Math.max(...equityCurve); - - // Avoid division by zero if all values are the same const valueRange = maxValue - minValue || 1; // Normalize data points @@ -191,42 +290,47 @@ class Backtesting { return { x, y }; }); - // Create SVG content - let svgContent = ``; - - // Set SVG content - chartContainer.innerHTML = svgContent; + equityCurveChart.appendChild(svg); } - stopRunningAnimation(results) { - const progressContainer = document.getElementById('backtest-progress-container'); - progressContainer.classList.remove('show'); - - // Hide the status message - const statusMessage = document.getElementById('backtest-status-message'); - statusMessage.style.display = 'none'; - statusMessage.textContent = ''; - + this.hideElement(this.progressContainer); + this.clearMessage(); this.displayTestResults(results); } - - fetchSavedTests() { this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); } @@ -234,15 +338,68 @@ class Backtesting { updateHTML() { let html = ''; for (const test of this.tests) { + const statusClass = test.status || 'default'; // Use the status or fallback to 'default' html += ` -