I repaired the sinup and signin parts. I changed a bunch of javascript in strategies.js. there is an error that pops up when webtools is open.

This commit is contained in:
Rob 2024-11-18 10:25:08 -04:00
parent fe1e93c24b
commit 2c644147a4
14 changed files with 1074 additions and 366 deletions

View File

@ -1,4 +1,5 @@
import json import json
import logging
from typing import Any from typing import Any
from Users import Users from Users import Users
@ -12,6 +13,8 @@ from indicators import Indicators
from Signals import Signals from Signals import Signals
from trade import Trades from trade import Trades
# Configure logging
logger = logging.getLogger(__name__)
class BrighterTrades: class BrighterTrades:
def __init__(self, socketio): def __init__(self, socketio):
@ -340,18 +343,22 @@ class BrighterTrades:
if not user_id: if not user_id:
return {"success": False, "message": "User ID not found"} 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(): if not isinstance(data['name'], str) or not data['name'].strip():
return {"success": False, "message": "Invalid or empty strategy name"} return {"success": False, "message": "Invalid or empty strategy name"}
if not isinstance(data['workspace'], str) or not data['workspace'].strip(): if not isinstance(data['workspace'], str) or not data['workspace'].strip():
return {"success": False, "message": "Invalid or empty workspace data"} 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"} return {"success": False, "message": "Invalid or empty strategy code"}
# Serialize code to JSON string for storage try:
code_json = json.dumps(data['code']) # 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 # Prepare the strategy data for insertion
try:
strategy_data = { strategy_data = {
"creator": user_id, "creator": user_id,
"name": data['name'].strip(), "name": data['name'].strip(),
@ -361,11 +368,31 @@ class BrighterTrades:
"public": int(data.get('public', 0)), "public": int(data.get('public', 0)),
"fee": float(data.get('fee', 0.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. # The default source for undefined sources in the strategy
try:
default_source = self.users.get_chart_view(user_name=user_name) default_source = self.users.get_chart_view(user_name=user_name)
# Save the new strategy (in both cache and database) and return the result. except Exception as e:
return self.strategies.new_strategy(strategy_data, default_source) 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: def received_edit_strategy(self, data: dict) -> dict:
""" """
@ -377,6 +404,7 @@ class BrighterTrades:
# Extract user_name and strategy name from the data # Extract user_name and strategy name from the data
user_name = data.get('user_name') user_name = data.get('user_name')
strategy_name = data.get('name') strategy_name = data.get('name')
if not user_name: if not user_name:
return {"success": False, "message": "User not specified"} return {"success": False, "message": "User not specified"}
if not strategy_name: if not strategy_name:
@ -417,10 +445,28 @@ class BrighterTrades:
"tbl_key": tbl_key # Include the tbl_key to identify the strategy "tbl_key": tbl_key # Include the tbl_key to identify the strategy
} }
# The default source for undefined sources in the strategy. # Get the default source for undefined sources in the strategy
try:
default_source = self.users.get_chart_view(user_name=user_name) 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 # 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: def delete_strategy(self, data: dict) -> str | dict:
""" """
@ -593,10 +639,25 @@ class BrighterTrades:
return self.trades.get_trades('dict') return self.trades.get_trades('dict')
def delete_backtest(self, msg_data): 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') backtest_name = msg_data.get('name')
if backtest_name in self.backtests: user_name = msg_data.get('user_name')
del self.backtests[backtest_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 Backtesters 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): def adjust_setting(self, user_name: str, setting: str, params: Any):
""" """
@ -729,12 +790,21 @@ class BrighterTrades:
return standard_reply("signal_created", r_data) return standard_reply("signal_created", r_data)
if msg_type == 'new_strategy': if msg_type == 'new_strategy':
try:
if r_data := self.received_new_strategy(msg_data): if r_data := self.received_new_strategy(msg_data):
return standard_reply("strategy_created", r_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 msg_type == 'edit_strategy':
try:
if r_data := self.received_edit_strategy(msg_data): if r_data := self.received_edit_strategy(msg_data):
return standard_reply("strategy_created", r_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 msg_type == 'new_trade':
if r_data := self.received_new_trade(msg_data): if r_data := self.received_new_trade(msg_data):
@ -760,8 +830,8 @@ class BrighterTrades:
return standard_reply("backtest_submitted", resp) return standard_reply("backtest_submitted", resp)
if msg_type == 'delete_backtest': if msg_type == 'delete_backtest':
self.delete_backtest(msg_data) response = self.delete_backtest(msg_data)
return standard_reply("backtest_deleted", {}) return standard_reply("backtest_deleted", response)
if msg_type == 'reply': if msg_type == 'reply':
# If the message is a reply log the response to the terminal. # If the message is a reply log the response to the terminal.

View File

@ -340,6 +340,39 @@ class TableBasedCache:
except AttributeError as e: except AttributeError as e:
raise AttributeError(f"Error in metadata processing: {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: def query(self, conditions: List[Tuple[str, Any]]) -> pd.DataFrame:
"""Query rows based on conditions and return valid (non-expired) entries.""" """Query rows based on conditions and return valid (non-expired) entries."""
self._purge_expired() # Remove expired rows before querying self._purge_expired() # Remove expired rows before querying
@ -479,6 +512,37 @@ class CacheManager:
# No result return an empty Dataframe # No result return an empty Dataframe
return pd.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: def get_cache_item(self, item_name: str, cache_name: str, filter_vals: tuple[str, any]) -> any:
""" """
Retrieves a specific item from the cache. Retrieves a specific item from the cache.
@ -755,6 +819,56 @@ class DatabaseInteractions(SnapshotDataCache):
super().__init__() super().__init__()
self.db = Database() 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, 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: key: str = None, include_tbl_key: bool = False) -> pd.DataFrame | None:
""" """

View File

@ -61,7 +61,6 @@ class Strategies:
self.active_instances: dict[tuple[int, str], StrategyInstance] = {} # Key: (user_id, strategy_id) self.active_instances: dict[tuple[int, str], StrategyInstance] = {} # Key: (user_id, strategy_id)
def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict: def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict:
""" """
Saves a strategy to the cache and database. Handles both creation and editing. 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 is_edit = 'tbl_key' in strategy_data
try: try:
# Determine if this is an edit or a new creation
tbl_key = strategy_data.get('tbl_key', str(uuid.uuid4()))
if is_edit: if is_edit:
# Editing an existing strategy # Verify the existing strategy
tbl_key = strategy_data['tbl_key']
existing_strategy = self.data_cache.get_rows_from_datacache( existing_strategy = self.data_cache.get_rows_from_datacache(
cache_name='strategies', cache_name='strategies',
filter_vals=[('tbl_key', tbl_key)] filter_vals=[('tbl_key', tbl_key)]
@ -82,11 +83,7 @@ class Strategies:
if existing_strategy.empty: if existing_strategy.empty:
return {"success": False, "message": "Strategy not found."} return {"success": False, "message": "Strategy not found."}
else: else:
# Creating a new strategy # Check for duplicate strategy name
# Generate a unique identifier first
tbl_key = str(uuid.uuid4())
# Check if a strategy with the same name already exists for this user
filter_conditions = [ filter_conditions = [
('creator', strategy_data.get('creator')), ('creator', strategy_data.get('creator')),
('name', strategy_data['name']) ('name', strategy_data['name'])
@ -98,7 +95,7 @@ class Strategies:
if not existing_strategy.empty: if not existing_strategy.empty:
return {"success": False, "message": "A strategy with this name already exists"} return {"success": False, "message": "A strategy with this name already exists"}
# Validate and serialize 'workspace' (XML string) # Validate and serialize 'workspace'
workspace_data = strategy_data.get('workspace') workspace_data = strategy_data.get('workspace')
if not isinstance(workspace_data, str) or not workspace_data.strip(): if not isinstance(workspace_data, str) or not workspace_data.strip():
return {"success": False, "message": "Invalid or empty workspace data"} return {"success": False, "message": "Invalid or empty workspace data"}
@ -110,10 +107,7 @@ class Strategies:
except (TypeError, ValueError): except (TypeError, ValueError):
return {"success": False, "message": "Invalid stats data format"} return {"success": False, "message": "Invalid stats data format"}
default_source = default_source.copy() # Validate and parse 'code'
strategy_id = tbl_key
# Extract and validate 'code' as a dictionary
code = strategy_data.get('code') code = strategy_data.get('code')
if isinstance(code, str): if isinstance(code, str):
try: try:
@ -124,69 +118,23 @@ class Strategies:
return {"success": False, "message": "Invalid JSON format for 'code'."} return {"success": False, "message": "Invalid JSON format for 'code'."}
elif isinstance(code, dict): elif isinstance(code, dict):
strategy_json = code strategy_json = code
# Serialize 'code' to JSON string strategy_data['code'] = json.dumps(strategy_json) # Serialize for storage
try:
serialized_code = json.dumps(code)
strategy_data['code'] = serialized_code
except (TypeError, ValueError):
return {"success": False, "message": "Unable to serialize 'code' field."}
else: else:
return {"success": False, "message": "'code' must be a JSON string or dictionary."} return {"success": False, "message": "'code' must be a JSON string or dictionary."}
# Initialize PythonGenerator # Generate Python components using PythonGenerator
python_generator = PythonGenerator(default_source, strategy_id) python_generator = PythonGenerator(default_source.copy(), tbl_key)
# Generate strategy components (code, indicators, data_sources, flags)
strategy_components = python_generator.generate(strategy_json) 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) strategy_data['strategy_components'] = json.dumps(strategy_components)
if is_edit: # Prepare fields for database operations
# Editing existing strategy
tbl_key = strategy_data['tbl_key']
# Prepare the columns and values for the update
columns = ( columns = (
"creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components" "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components"
) )
values = ( values = (
strategy_data.get('creator'), strategy_data.get('creator'),
strategy_data['name'], strategy_data['name'],
workspace_data, # Use the validated workspace data 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
)
# Update the strategy in the database and cache
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
)
# Return success message
return {"success": True, "message": "Strategy updated successfully"}
else:
# Creating new strategy
# Insert the strategy into the database and cache
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'], strategy_data['code'],
stats_serialized, stats_serialized,
bool(strategy_data.get('public', 0)), bool(strategy_data.get('public', 0)),
@ -194,29 +142,41 @@ class Strategies:
tbl_key, tbl_key,
strategy_data['strategy_components'] strategy_data['strategy_components']
) )
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'
)
else:
# Insert a new strategy
self.data_cache.insert_row_into_datacache(
cache_name='strategies',
columns=columns,
values=values
) )
# Construct the saved strategy data to return # Prepare the response
saved_strategy = { response_strategy = strategy_data.copy()
"id": tbl_key, # Assuming tbl_key is used as a unique identifier response_strategy.pop("strategy_components", None) # Remove the sensitive field
"creator": strategy_data.get('creator'),
"name": strategy_data['name'], # Include `tbl_key` within the `strategy` object
"workspace": workspace_data, # Original workspace data response_strategy['tbl_key'] = tbl_key
"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 { return {
"success": True, "success": True,
"message": "Strategy created and saved successfully", "message": "Strategy saved successfully",
"strategy": saved_strategy # Include the strategy data "strategy": response_strategy,
"updated_at": dt.datetime.now(dt.timezone.utc).isoformat()
} }
except Exception as e: except Exception as e:
# Catch any exceptions and return a failure message # Handle exceptions and log errors
# Log the exception with traceback for debugging
logger.error(f"Failed to save strategy: {e}", exc_info=True) logger.error(f"Failed to save strategy: {e}", exc_info=True)
traceback.print_exc() traceback.print_exc()
operation = "update" if is_edit else "create" operation = "update" if is_edit else "create"

View File

@ -263,7 +263,7 @@ def signout():
@app.route('/login', methods=['POST']) @app.route('/login', methods=['POST'])
def login(): def login():
# Get the user_name and password from the form data # 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') password = request.form.get('password')
# Validate the input # Validate the input
@ -295,21 +295,23 @@ def signup_submit():
validate_email(email) validate_email(email)
except EmailNotValidError as e: except EmailNotValidError as e:
flash(message=f"Invalid email format: {e}") flash(message=f"Invalid email format: {e}")
return None return redirect('/signup') # Redirect back to signup page
# Validate user_name and password # Validate user_name and password
if not username or not password: if not username or not password:
flash(message="Missing user_name or password") flash(message="Missing user_name or password")
return None return redirect('/signup') # Redirect back to signup page
# Create a new user # Create a new user
success = brighter_trades.create_new_user(email=email, username=username, password=password) success = brighter_trades.create_new_user(email=email, username=username, password=password)
if success: if success:
session['user'] = username session['user'] = username
return redirect('/') flash(message="Signup successful! You are now logged in.")
return redirect('/') # Redirect to the main page
else: else:
flash(message="An error has occurred during the signup process.") 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']) @app.route('/api/indicator_init', methods=['POST', 'GET'])

View File

@ -148,6 +148,25 @@ class Backtester:
except Exception as e: except Exception as e:
logger.error(f"Error during cleanup of backtest '{backtest_key}': {e}", exc_info=True) 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: 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. Retrieves and validates the components of a user-defined strategy.
@ -360,7 +379,8 @@ class Backtester:
precomputed_indicators=precomputed_indicators, # Pass precomputed indicators precomputed_indicators=precomputed_indicators, # Pass precomputed indicators
socketio=self.socketio, # Pass SocketIO instance socketio=self.socketio, # Pass SocketIO instance
socket_conn_id=socket_conn_id, # Pass SocketIO connection ID 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 # Add data feed to Cerebro
@ -395,7 +415,10 @@ class Backtester:
trades = strategy.trade_list trades = strategy.trade_list
# Send 100% completion # 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 # Prepare the results to pass into the callback
backtest_results = { backtest_results = {
@ -531,36 +554,25 @@ class Backtester:
self.socketio.start_background_task(purge_task) self.socketio.start_background_task(purge_task)
def cleanup_orphaned_backtest_contexts(self) -> None: def cleanup_orphaned_backtest_contexts(self) -> None:
""" """
Identifies and removes orphaned backtest contexts that do not have corresponding entries in 'tests' cache. Identifies and removes orphaned backtest contexts that do not have corresponding entries in 'tests' cache.
""" """
try: try:
# Fetch all strategy_instance_ids from 'strategy_contexts' that start with 'test_' # 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', cache_name='strategy_contexts',
filter_vals=[] # Fetch all filter_vals=[('strategy_instance_id', 'LIKE', 'test_%')]
) )
if strategy_contexts_df.empty: 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.") logger.debug("No backtest contexts found for cleanup.")
return 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'] strategy_instance_id = row['strategy_instance_id']
# Check if this backtest exists in 'tests' cache # 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 found = False
tests_cache = self.data_cache.get_cache('tests') tests_cache = self.data_cache.get_cache('tests')
if isinstance(tests_cache, RowBasedCache): if isinstance(tests_cache, RowBasedCache):
@ -573,10 +585,11 @@ class Backtester:
# Orphaned context found; proceed to remove it # Orphaned context found; proceed to remove it
self.data_cache.remove_row_from_datacache( self.data_cache.remove_row_from_datacache(
cache_name='strategy_contexts', 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 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: except Exception as e:
logger.error(f"Error during cleanup of orphaned backtest contexts: {e}", exc_info=True) logger.error(f"Error during cleanup of orphaned backtest contexts: {e}", exc_info=True)

View File

@ -22,6 +22,7 @@ class MappedStrategy(bt.Strategy):
('socketio', None), # SocketIO instance for emitting progress ('socketio', None), # SocketIO instance for emitting progress
('socket_conn_id', None), # Socket connection ID for emitting progress ('socket_conn_id', None), # Socket connection ID for emitting progress
('data_length', None), # Total number of data points for progress calculation ('data_length', None), # Total number of data points for progress calculation
('backtest_name', None) # Name of the backtest_name
) )
def __init__(self): def __init__(self):
@ -48,6 +49,7 @@ class MappedStrategy(bt.Strategy):
# Initialize other needed variables # Initialize other needed variables
self.starting_balance = self.broker.getvalue() self.starting_balance = self.broker.getvalue()
self.last_progress = 0 # Initialize last_progress self.last_progress = 0 # Initialize last_progress
self.backtest_name = self.p.backtest_name
self.bar_executed = 0 # Initialize bar_executed self.bar_executed = 0 # Initialize bar_executed
def notify_order(self, order): def notify_order(self, order):
@ -140,7 +142,8 @@ class MappedStrategy(bt.Strategy):
if self.p.socketio and self.p.socket_conn_id: if self.p.socketio and self.p.socket_conn_id:
self.p.socketio.emit( self.p.socketio.emit(
'message', 'message',
{'reply': 'progress', 'data': {'progress': progress}}, {'reply': 'progress', 'data': { 'test_id': self.backtest_name,
'progress': progress}},
room=self.p.socket_conn_id room=self.p.socket_conn_id
) )
logger.debug(f"Emitted progress: {progress}%") logger.debug(f"Emitted progress: {progress}%")

View File

@ -216,6 +216,16 @@ class StratDataManager {
this.strategies.push(data); 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). * Handles updates to the strategy itself (e.g., configuration changes).
@ -635,12 +645,18 @@ class Strategies {
} }
/** /**
* Handles strategy-related error messages from the server. * Handles strategy-related errors sent from the server.
* @param {Object} errorData - The data containing error details. * @param {Object} errorData - The error message and additional details.
*/ */
handleStrategyError(errorData) { handleStrategyError(errorData) {
console.error("Strategy Error:", errorData.message); console.error("Strategy Error:", errorData.message);
// Display a user-friendly error message
if (errorData.message) {
alert(`Error: ${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. * Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy. * @param {Object} data - The data for the newly created strategy.
@ -681,15 +698,38 @@ class Strategies {
} }
} }
/** /**
* Handles updates to the strategy itself (e.g., configuration changes). * 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) { handleStrategyUpdated(data) {
this.dataManager.updateStrategyData(data); 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()); 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. * Handles the deletion of a strategy.

View File

@ -4,6 +4,7 @@ class Backtesting {
this.comms = ui.data.comms; this.comms = ui.data.comms;
this.tests = []; // Stores the list of saved backtests this.tests = []; // Stores the list of saved backtests
this.target_id = 'backtest_display'; // The container to display backtests this.target_id = 'backtest_display'; // The container to display backtests
this.currentTest = null; // Tracks the currently open test
// Register handlers for backtesting messages // Register handlers for backtesting messages
this.comms.on('backtest_error', this.handleBacktestError.bind(this)); this.comms.on('backtest_error', this.handleBacktestError.bind(this));
@ -14,174 +15,272 @@ class Backtesting {
this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this)); this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this));
} }
handleBacktestSubmitted(data) { // Initialize method to cache DOM elements
console.log("Backtest response received:", data.status); initialize() {
if (data.status === "started") { this.cacheDOMElements();
// Show the progress bar or any other UI updates // Optionally, fetch saved tests or perform other initialization
this.showRunningAnimation(); }
// Add the new backtest to the tests array 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) {
if (data.status === "started") {
const existingTest = this.tests.find(t => t.name === data.backtest_name);
const availableStrategies = this.getAvailableStrategies();
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 = { const newTest = {
name: data.backtest_name, name: data.backtest_name,
strategy: data.strategy_name, strategy: availableStrategies.includes(data.strategy)
? data.strategy
: availableStrategies[0] || 'default_strategy',
start_date: data.start_date, start_date: data.start_date,
// Include any other relevant data from the response if available status: 'running',
progress: 0,
results: null
}; };
this.tests.push(newTest); 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(); this.updateHTML();
} }
} }
handleBacktestError(data) { handleBacktestError(data) {
console.error("Backtest error:", data.message); console.error("Backtest error:", data.message);
// Display error message in the status message area const test = this.tests.find(t => t.name === this.currentTest);
const statusMessage = document.getElementById('backtest-status-message'); if (test) {
if (statusMessage) { test.status = 'error'; // Update the test status
statusMessage.style.display = 'block'; console.log(`Backtest "${test.name}" encountered an error.`);
statusMessage.style.color = 'red'; // Set text color to red for errors this.updateHTML();
statusMessage.textContent = `Backtest error: ${data.message}`;
} else {
// Fallback to alert if the element is not found
alert(`Backtest error: ${data.message}`);
} }
// Optionally hide progress bar and results this.displayMessage(`Backtest error: ${data.message}`, 'red');
const progressContainer = document.getElementById('backtest-progress-container');
if (progressContainer) { // Hide progress bar and results
progressContainer.classList.remove('show'); this.hideElement(this.progressContainer);
} this.hideElement(this.resultsContainer);
const resultsContainer = document.getElementById('backtest-results');
if (resultsContainer) {
resultsContainer.style.display = 'none';
}
} }
handleBacktestResults(data) { handleBacktestResults(data) {
console.log("Backtest results received:", data.results); const test = this.tests.find(t => t.name === data.test_id);
// Logic to stop running animation and display results 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); this.stopRunningAnimation(data.results);
} }
}
handleProgress(data) { handleProgress(data) {
console.log("Backtest progress:", data.progress); console.log("handleProgress: Backtest progress:", data.progress);
// Logic to update progress bar
this.updateProgressBar(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) { handleBacktestsList(data) {
console.log("Backtests list received:", data.tests); console.log("Backtests list received:", data.tests);
// Logic to update backtesting UI // Update the tests array
this.set_data(data.tests); this.tests = data.tests;
this.updateHTML();
} }
handleBacktestDeleted(data) { handleBacktestDeleted(data) {
console.log(`Backtest "${data.name}" was successfully deleted.`); console.log(`Backtest "${data.name}" was successfully deleted.`);
// Logic to refresh list of backtests // Remove the deleted test from the tests array
this.fetchSavedTests(); 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) { updateProgressBar(progress) {
const progressBar = document.getElementById('progress_bar'); if (this.progressBar) {
if (progressBar) {
console.log(`Updating progress bar to ${progress}%`); console.log(`Updating progress bar to ${progress}%`);
progressBar.style.width = `${progress}%`; this.progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress}%`; this.setText(this.progressBar, `${progress}%`);
} else { } else {
console.log('Progress bar element not found'); console.log('Progress bar element not found');
} }
} }
showRunningAnimation() { showRunningAnimation() {
const resultsContainer = document.getElementById('backtest-results'); this.hideElement(this.resultsContainer);
const resultsDisplay = document.getElementById('results_display'); this.showElement(this.progressContainer);
const progressContainer = document.getElementById('backtest-progress-container'); this.updateProgressBar(0);
const progressBar = document.getElementById('progress_bar'); this.setText(this.progressBar, '0%');
const statusMessage = document.getElementById('backtest-status-message'); this.resultsDisplay.innerHTML = ''; // Clear previous results
this.displayMessage('Backtest started...', 'blue');
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...';
} }
displayTestResults(results) { displayTestResults(results) {
const resultsContainer = document.getElementById('backtest-results'); this.showElement(this.resultsContainer);
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
let html = ` let html = `
<span><strong>Initial Capital:</strong> ${results.initial_capital}</span> <span><strong>Initial Capital:</strong> ${results.initial_capital}</span><br>
<span><strong>Final Portfolio Value:</strong> ${results.final_portfolio_value}</span> <span><strong>Final Portfolio Value:</strong> ${results.final_portfolio_value}</span><br>
<span><strong>Total Return:</strong> ${totalReturn}%</span> <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.toFixed(2)} seconds</span>
`; `;
// Add a container for the chart // Equity Curve
html += `<h4>Equity Curve</h4> html += `
<div id="equity_curve_chart" style="width: 100%; height: 300px;"></div>`; <h4>Equity Curve</h4>
<div id="equity_curve_chart" style="width: 100%; height: 300px;"></div>
`;
// If there are trades, display them // Trades Table
if (results.trades && results.trades.length > 0) { if (results.trades && results.trades.length > 0) {
html += `<h4>Trades Executed</h4> html += `
<h4>Trades Executed</h4>
<div style="max-height: 200px; overflow-y: auto;"> <div style="max-height: 200px; overflow-y: auto;">
<table border="1" cellpadding="5" cellspacing="0"> <table border="1" cellpadding="5" cellspacing="0">
<thead>
<tr> <tr>
<th>Trade ID</th> <th>Trade ID</th>
<th>Size</th> <th>Size</th>
<th>Price</th> <th>Price</th>
<th>P&L</th> <th>P&L</th>
</tr>`; </tr>
</thead>
<tbody>
`;
results.trades.forEach(trade => { results.trades.forEach(trade => {
html += `<tr> html += `
<tr>
<td>${trade.ref}</td> <td>${trade.ref}</td>
<td>${trade.size}</td> <td>${trade.size}</td>
<td>${trade.price}</td> <td>${trade.price}</td>
<td>${trade.pnl}</td> <td>${trade.pnl}</td>
</tr>`; </tr>
`;
}); });
html += `</table></div>`; html += `
</tbody>
</table>
</div>
`;
} else { } else {
html += `<p>No trades were executed.</p>`; html += `<p>No trades were executed.</p>`;
} }
resultsDisplay.innerHTML = html; this.resultsDisplay.innerHTML = html;
// Generate the equity curve chart
this.drawEquityCurveChart(results.equity_curve); this.drawEquityCurveChart(results.equity_curve);
} }
drawEquityCurveChart(equityCurve) { drawEquityCurveChart(equityCurve) {
const chartContainer = document.getElementById('equity_curve_chart'); const equityCurveChart = document.getElementById('equity_curve_chart');
if (!chartContainer) { if (!equityCurveChart) {
console.error('Chart container not found'); console.error('Chart container not found');
return; return;
} }
// Get the dimensions of the container // Clear previous chart
const width = chartContainer.clientWidth || 600; equityCurveChart.innerHTML = '';
const height = chartContainer.clientHeight || 300;
// Get container dimensions
const width = equityCurveChart.clientWidth || 600;
const height = equityCurveChart.clientHeight || 300;
const padding = 40; const padding = 40;
// Find min and max values // Calculate min and max values
const minValue = Math.min(...equityCurve); const minValue = Math.min(...equityCurve);
const maxValue = Math.max(...equityCurve); const maxValue = Math.max(...equityCurve);
// Avoid division by zero if all values are the same
const valueRange = maxValue - minValue || 1; const valueRange = maxValue - minValue || 1;
// Normalize data points // Normalize data points
@ -191,42 +290,47 @@ class Backtesting {
return { x, y }; return { x, y };
}); });
// Create SVG content // Create SVG element
let svgContent = `<svg width="${width}" height="${height}">`; const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", width);
svg.setAttribute("height", height);
// Draw axes // Draw axes
svgContent += `<line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="black"/>`; // X-axis const xAxis = document.createElementNS(svgNS, "line");
svgContent += `<line x1="${padding}" y1="${padding}" x2="${padding}" y2="${height - padding}" stroke="black"/>`; // Y-axis xAxis.setAttribute("x1", padding);
xAxis.setAttribute("y1", height - padding);
xAxis.setAttribute("x2", width - padding);
xAxis.setAttribute("y2", height - padding);
xAxis.setAttribute("stroke", "black");
svg.appendChild(xAxis);
const yAxis = document.createElementNS(svgNS, "line");
yAxis.setAttribute("x1", padding);
yAxis.setAttribute("y1", padding);
yAxis.setAttribute("x2", padding);
yAxis.setAttribute("y2", height - padding);
yAxis.setAttribute("stroke", "black");
svg.appendChild(yAxis);
// Draw equity curve // Draw equity curve
svgContent += `<polyline points="`; const polyline = document.createElementNS(svgNS, "polyline");
normalizedData.forEach(point => { const points = normalizedData.map(point => `${point.x},${point.y}`).join(' ');
svgContent += `${point.x},${point.y} `; polyline.setAttribute("points", points);
}); polyline.setAttribute("fill", "none");
svgContent += `" fill="none" stroke="blue" stroke-width="2"/>`; polyline.setAttribute("stroke", "blue");
polyline.setAttribute("stroke-width", "2");
svg.appendChild(polyline);
// Close SVG equityCurveChart.appendChild(svg);
svgContent += `</svg>`;
// Set SVG content
chartContainer.innerHTML = svgContent;
} }
stopRunningAnimation(results) { stopRunningAnimation(results) {
const progressContainer = document.getElementById('backtest-progress-container'); this.hideElement(this.progressContainer);
progressContainer.classList.remove('show'); this.clearMessage();
// Hide the status message
const statusMessage = document.getElementById('backtest-status-message');
statusMessage.style.display = 'none';
statusMessage.textContent = '';
this.displayTestResults(results); this.displayTestResults(results);
} }
fetchSavedTests() { fetchSavedTests() {
this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name });
} }
@ -234,15 +338,68 @@ class Backtesting {
updateHTML() { updateHTML() {
let html = ''; let html = '';
for (const test of this.tests) { for (const test of this.tests) {
const statusClass = test.status || 'default'; // Use the status or fallback to 'default'
html += ` html += `
<div class="backtest-item"> <div class="backtest-item ${statusClass}" onclick="UI.backtesting.openTestDialog('${test.name}')">
<button class="delete-button" onclick="this.ui.backtesting.deleteTest('${test.name}')">&#10008;</button> <button class="delete-button" onclick="UI.backtesting.deleteTest('${test.name}'); event.stopPropagation();"></button>
<div class="backtest-name" onclick="this.ui.backtesting.runTest('${test.name}')">${test.name}</div> <div class="backtest-name">${test.name}</div>
</div>`; </div>`;
} }
document.getElementById(this.target_id).innerHTML = html; this.backtestDisplay.innerHTML = html;
} }
openTestDialog(testName) {
const test = this.tests.find(t => t.name === testName);
if (!test) {
alert('Test not found.');
return;
}
this.currentTest = testName; // Set the currently open test
// Populate the strategy dropdown
this.populateStrategyDropdown();
// Validate and set strategy
const availableStrategies = this.getAvailableStrategies();
if (test.strategy && availableStrategies.includes(test.strategy)) {
this.strategyDropdown.value = test.strategy;
} else {
console.warn(`openTestDialog: Strategy "${test.strategy}" not found in dropdown. Defaulting to first available.`);
this.strategyDropdown.value = availableStrategies[0] || '';
}
// Populate other form fields
document.getElementById('start_date').value = test.start_date
? this.formatDateToLocalInput(new Date(test.start_date))
: this.formatDateToLocalInput(new Date(Date.now() - 60 * 60 * 1000)); // 1 hour ago
document.getElementById('initial_capital').value = test.results?.initial_capital || 10000;
document.getElementById('commission').value = test.results?.commission || 0.001;
console.log(`openTestDialog: Set start_date to ${document.getElementById('start_date').value}`);
// Display results or show progress
if (test.status === 'complete') {
this.displayTestResults(test.results);
this.hideElement(this.progressContainer);
} else {
this.hideElement(this.resultsContainer);
this.showElement(this.progressContainer);
this.updateProgressBar(test.progress);
this.displayMessage('Backtest in progress...', 'blue');
}
// Update header and show form
this.setText(this.backtestDraggableHeader, `Edit Backtest - ${test.name}`);
// Manage button visibility
this.showElement(document.getElementById('backtest-submit-edit'));
this.hideElement(document.getElementById('backtest-submit-create'));
this.formElement.style.display = "grid";
console.log(`openTestDialog: Opened dialog for backtest "${test.name}".`);
}
runTest(testName) { runTest(testName) {
const testData = { name: testName, user_name: this.ui.data.user_name }; const testData = { name: testName, user_name: this.ui.data.user_name };
this.comms.sendToApp('run_backtest', testData); this.comms.sendToApp('run_backtest', testData);
@ -254,70 +411,82 @@ class Backtesting {
} }
populateStrategyDropdown() { populateStrategyDropdown() {
const strategyDropdown = document.getElementById('strategy_select'); if (!this.strategyDropdown) {
strategyDropdown.innerHTML = ''; console.error('Strategy dropdown element not found.');
const strategies = this.ui.strats.dataManager.getAllStrategies();
console.log("Available strategies:", strategies);
strategies.forEach(strategy => {
const option = document.createElement('option');
option.value = strategy.name;
option.text = strategy.name;
strategyDropdown.appendChild(option);
});
if (strategies.length > 0) {
const firstStrategyName = strategies[0].name;
console.log("Setting default strategy to:", firstStrategyName);
strategyDropdown.value = firstStrategyName;
}
}
openForm(testName = null) {
const formElement = document.getElementById("backtest_form");
if (!formElement) {
console.error('Form element not found');
return; return;
} }
this.strategyDropdown.innerHTML = ''; // Clear existing options
const strategies = this.getAvailableStrategies();
if (!strategies || strategies.length === 0) {
console.warn('No strategies available to populate dropdown.');
return;
}
strategies.forEach(strategy => {
const option = document.createElement('option');
option.value = strategy;
option.text = strategy;
this.strategyDropdown.appendChild(option);
});
}
openForm(testName = null) {
if (testName) {
this.openTestDialog(testName);
} else {
this.currentTest = null; // Reset the currently open test
// Populate the strategy dropdown
this.populateStrategyDropdown(); this.populateStrategyDropdown();
if (testName) { // Update header and show form
const testData = this.tests.find(test => test.name === testName); this.setText(this.backtestDraggableHeader, "Create New Backtest");
if (testData) {
document.querySelector("#backtest_draggable_header h1").textContent = "Edit Backtest";
document.getElementById('strategy_select').value = testData.strategy;
document.getElementById('start_date').value = testData.start_date;
}
} else {
document.querySelector("#backtest_draggable_header h1").textContent = "Create New Backtest";
this.clearForm(); this.clearForm();
// Set default start_date to 1 hour ago
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); // Current time minus 1 hour
const formattedDate = this.formatDateToLocalInput(oneHourAgo);
document.getElementById('start_date').value = formattedDate;
console.log(`openForm: Set default start_date to ${formattedDate}`);
// Manage button visibility
this.showElement(document.getElementById('backtest-submit-create'));
this.hideElement(document.getElementById('backtest-submit-edit'));
this.formElement.style.display = "grid";
console.log('openForm: Opened form for creating a new backtest.');
}
} }
formElement.style.display = "grid";
}
closeForm() { closeForm() {
document.getElementById("backtest_form").style.display = "none"; this.formElement.style.display = "none";
// Hide and clear the status message this.currentTest = null; // Reset the currently open test
const statusMessage = document.getElementById('backtest-status-message');
statusMessage.style.display = 'none'; // Optionally hide progress/results to avoid stale UI
statusMessage.textContent = ''; this.hideElement(this.resultsContainer);
this.hideElement(this.progressContainer);
this.clearMessage();
} }
clearForm() { clearForm() {
document.getElementById('strategy_select').value = ''; if (this.strategyDropdown) this.strategyDropdown.value = '';
document.getElementById('start_date').value = ''; document.getElementById('start_date').value = '';
document.getElementById('initial_capital').value = 10000;
document.getElementById('commission').value = 0.001;
} }
submitTest() { submitTest() {
const strategy = document.getElementById('strategy_select').value; const strategy = this.strategyDropdown ? this.strategyDropdown.value : null;
const start_date = document.getElementById('start_date').value; const start_date = document.getElementById('start_date').value;
const capital = parseFloat(document.getElementById('initial_capital').value) || 10000; const capital = parseFloat(document.getElementById('initial_capital').value) || 10000;
const commission = parseFloat(document.getElementById('commission').value) || 0.001; const commission = parseFloat(document.getElementById('commission').value) || 0.001;
if (!strategy) { if (!strategy) {
alert("Please select a strategy."); alert("Please select a strategy.");
console.log('submitTest: Submission failed - No strategy selected.');
return; return;
} }
@ -326,6 +495,25 @@ class Backtesting {
if (startDate > now) { if (startDate > now) {
alert("Start date cannot be in the future."); alert("Start date cannot be in the future.");
console.log('submitTest: Submission failed - Start date is in the future.');
return;
}
let testName;
if (this.currentTest && this.tests.find(t => t.name === this.currentTest)) {
// Editing an existing test
testName = this.currentTest;
console.log(`submitTest: Editing existing backtest "${testName}".`);
} else {
// Creating a new test without timestamp
testName = `${strategy}_backtest`;
console.log(`submitTest: Creating new backtest "${testName}".`);
}
// Check if the test 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; return;
} }
@ -334,9 +522,41 @@ class Backtesting {
start_date, start_date,
capital, capital,
commission, commission,
user_name: this.ui.data.user_name user_name: this.ui.data.user_name,
backtest_name: testName,
}; };
this.comms.sendToApp('submit_backtest', testData); // Disable the submit button to prevent duplicate submissions
const submitButton = document.getElementById('backtest-submit-create');
if (submitButton) {
submitButton.disabled = true;
} }
// Submit the test
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)
setTimeout(() => {
if (submitButton) {
submitButton.disabled = false;
}
}, 2000); // Example: Re-enable after 2 seconds or on callback
}
// Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format
formatDateToLocalInput(date) {
const pad = (num) => num.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1); // Months are zero-based
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
} }

View File

@ -210,7 +210,7 @@ height: 500px;
border: none; border: none;
margin-bottom: 15px; margin-bottom: 15px;
color: #bfc0c0; color: #bfc0c0;
background: #262626; background: #246486;
padding: 20px; padding: 20px;
font-size: 16px; font-size: 16px;
border-radius: 10px; border-radius: 10px;
@ -246,15 +246,15 @@ height: 500px;
cursor: pointer; cursor: pointer;
} }
.form-popup .close-btn { .form-popup .close-btn {
color: white; border-color: black;
font-size: 30px; border-style: solid;
border-radius: 50%; border-radius: 50%;
background: #292929; background: #9f180f;
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 20px; top: 20px;
width: 30px; width: 30px;
padding: 2px 5px 7px 5px; font-size: larger;
height: 30px; height: 30px;
box-shadow: 5px 5px 15px #1e1e1e, box-shadow: 5px 5px 15px #1e1e1e,
-5px -5px 15px #1e1e1e; -5px -5px 15px #1e1e1e;

View File

@ -28,6 +28,9 @@ class User_Interface {
// Initialize other UI components here // Initialize other UI components here
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy"); this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy");
this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest"); this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest");
// Initialize Backtesting's DOM elements
this.backtesting.initialize();
} catch (error) { } catch (error) {
console.error('Initialization failed:', error); console.error('Initialization failed:', error);
} }

View File

@ -3,8 +3,7 @@
<!-- Draggable Header Section --> <!-- Draggable Header Section -->
<div id="backtest_draggable_header" style="cursor: move; padding: 10px; background-color: #3E3AF2; color: white; border-bottom: 1px solid #ccc;"> <div id="backtest_draggable_header" style="cursor: move; padding: 10px; background-color: #3E3AF2; color: white; border-bottom: 1px solid #ccc;">
<h1 id="backtest-form-header-create">Create New Backtest</h1> <h1 id="backtest-form-header">Create New Backtest</h1>
<h1 id="backtest-form-header-edit" style="display: none;">Edit Backtest</h1>
</div> </div>
<!-- Main Content (Scrollable) --> <!-- Main Content (Scrollable) -->
@ -36,8 +35,8 @@
<!-- Buttons --> <!-- Buttons -->
<div style="text-align: center;"> <div style="text-align: center;">
<button type="button" class="btn cancel" onclick="UI.backtesting.closeForm()">Close</button> <button type="button" class="btn cancel" onclick="UI.backtesting.closeForm()">Close</button>
<button id="backtest-submit-create" type="button" class="btn next" onclick="UI.backtesting.submitTest('new')">Run Test</button> <button id="backtest-submit-create" type="button" class="btn next" onclick="UI.backtesting.submitTest()">Run Test</button>
<button id="backtest-submit-edit" type="button" class="btn next" onclick="UI.backtesting.submitTest('edit')" style="display:none;">Edit Test</button> <button id="backtest-submit-edit" type="button" class="btn next" onclick="UI.backtesting.submitTest()" style="display:none;">Edit Test</button>
</div> </div>
<!-- Status message (Initially Hidden) --> <!-- Status message (Initially Hidden) -->
@ -51,7 +50,7 @@
</div> </div>
<!-- Results section (Initially Hidden) --> <!-- Results section (Initially Hidden) -->
<div id="backtest-results" style="display: none; margin-top: 10px;"> <div id="backtest-results">
<h4>Test Results</h4> <h4>Test Results</h4>
<pre id="results_display"></pre> <pre id="results_display"></pre>
</div> </div>
@ -71,15 +70,41 @@
.backtest-item { .backtest-item {
position: relative; position: relative;
width: 150px; width: 100px;
height: 120px; height: 100px;
text-align: center; text-align: center;
transition: transform 0.3s ease; transition: transform 0.3s ease;
background-size: cover; /* Ensure the background image covers the entire div */
background-position: center; /* Center the image */
border: 1px solid #ccc; /* Optional: Add a border for better visibility */
border-radius: 8px; /* Optional: Rounded corners */
} }
.backtest-item:hover { .backtest-item:hover {
transform: scale(1.05); transform: scale(1.05);
} }
/* Add a default background image */
.backtest-item.default {
background-image: url('/static/test_running_icon.webp');
}
/* Add background for specific statuses if needed */
.backtest-item.running {
background-image: url('/static/test_running_icon.webp');
background-color: #f3f981; /* Highlight running tests with a yellow background */
border-color: #ffa500; /* Add an orange border for emphasis */
}
.backtest-item.complete {
background-image: url('/static/test_complete_icon.webp');
background-color: #d4edda; /* Green for completed tests */
border-color: #28a745;
}
.backtest-item.error {
background-color: #f8d7da; /* Red for tests with errors */
border-color: #dc3545;
}
.backtest-name { .backtest-name {
position: absolute; position: absolute;
@ -92,6 +117,7 @@
color: black; color: black;
text-align: center; text-align: center;
width: 100px; width: 100px;
font-size: 12px;
} }
.delete-button { .delete-button {
@ -130,5 +156,12 @@
#results_display th { #results_display th {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
#backtest-results {
margin-top: 10px;
display: none; /* Initially hidden */
}
#backtest-results.show {
display: block; /* Show when backtest is complete */
}
</style> </style>

View File

@ -1,7 +1,6 @@
<div class="content" id="backtesting_hud"> <div class="content" id="backtesting_hud">
<button class="btn" id="new_backtest_btn" onclick="UI.backtesting.openForm('new')">New Backtest</button> <button class="btn" id="new_backtest_btn" onclick="UI.backtesting.openForm()">New Backtest</button>
<hr> <hr>
<h3>Back Tests</h3> <h3>Back Tests</h3>
<div class="backtests-container" id="backtest_display"></div> <div class="backtests-container" id="backtest_display"></div>
</div> </div>

View File

@ -16,7 +16,7 @@
<form name="signup_form" action="/login" method="POST"> <form name="signup_form" action="/login" method="POST">
<div class="input-field"><input id='username' name='username' placeholder="Username" class="validate"></div> <div class="input-field"><input id='username' name='username' placeholder="Username" class="validate"></div>
<div class="input-field"><input type="password" id='password' name='password' placeholder="Password" class="validate"></div> <div class="input-field"><input type="password" id='password' name='password' placeholder="Password" class="validate"></div>
<button class="second-button" onclick="UI.users.validate_input({username, email, password})">Sign in</button> <button class="btn" onclick="UI.users.validate_input({username, email, password})">Sign in</button>
</form> </form>
<p>Dont have an account? <a href="/signup">Sign Up</a></p> <p>Dont have an account? <a href="/signup">Sign Up</a></p>

View File

@ -2,25 +2,276 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- Load style sheets and set the title --> <!-- Responsive Meta Tag -->
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='brighterStyles.css') }}"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<script src="{{ url_for('static', filename='user.js') }}"></script> <!-- Bootstrap 5 CSS -->
<script type="text/javascript"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
users = new Users('signup_form');
<!-- Custom CSS for BrighterTrades -->
<link rel="stylesheet" href="{{ url_for('static', filename='brighterStyles.css') }}">
<title>{{ title }} | BrighterTrades</title>
<!-- Google Fonts for Modern Typography -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<!-- Font Awesome for Icons -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- AOS (Animate On Scroll) CSS for Animations -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
<!-- Custom JavaScript (Deferred) -->
<script src="{{ url_for('static', filename='user.js') }}" defer></script>
</head>
<body class="bg-dark text-light">
<!-- Background Animation -->
<div class="bg-animation"></div>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8">
<!-- Signup Card -->
<div class="card bg-transparent border-0" data-aos="fade-up" data-aos-duration="1000">
<div class="card-body p-4">
<!-- BrighterTrades Logo -->
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='logo_BrighterTrades.webp') }}" alt="BrighterTrades Logo" class="img-fluid logo" style="max-width: 150px; margin-top: 20px;">
</div>
<h2 class="card-title text-center mb-4">Join BrighterTrades</h2>
<!-- Flash Messages for Feedback -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
<i class="fas fa-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Signup Form -->
<form id="signup_form" name="signup_form" action="/signup_submit" method="post" novalidate>
<!-- Username Field -->
<div class="mb-3">
<label for="username" class="form-label">User Name</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="username" name="user_name" placeholder="Enter your user name" required>
<div class="invalid-feedback">
Please enter a user name.
</div>
</div>
</div>
<!-- Email Field -->
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input type="email" class="form-control" id="email" name="email" placeholder="Enter your email" required>
<div class="invalid-feedback">
Please enter a valid email address.
</div>
</div>
</div>
<!-- Password Field -->
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter your password"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters"
required>
<div class="invalid-feedback">
Password must be at least 8 characters long and include at least one number, one uppercase letter, and one lowercase letter.
</div>
</div>
<small class="form-text text-muted">
Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special characters, or emoji.
</small>
</div>
<!-- Submit Button -->
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="fas fa-arrow-right me-2"></i> Sign Up
</button>
</form>
<!-- Divider -->
<hr class="my-4 text-light">
<!-- Social Signup Options -->
<div class="text-center">
<p class="mb-2">Or sign up with</p>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-google"></i></a>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-linkedin"></i></a>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-github"></i></a>
</div>
<!-- Link to Login Page -->
<p class="mt-4 text-center">
Already have an account? <a href="/login" class="text-primary">Log in here</a>.
</p>
</div>
</div>
<!-- End of Signup Card -->
</div>
</div>
</div>
<!-- AOS (Animate On Scroll) JS for Animations -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js"></script>
<script>
// Initialize AOS
AOS.init({
offset: 100, // Adjust the distance to start the animation (default: 120px)
duration: 600, // Reduce animation duration (default: 400ms)
easing: 'ease-out-cubic', // Use a smoother easing function
anchorPlacement: 'top-center', // Adjust the trigger point for the animation
});
</script> </script>
</head> <!-- Bootstrap 5 JS and Dependencies (Popper) -->
<body id="sign_up_body"> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/dist/umd/popper.min.js" integrity="sha384-..." crossorigin="anonymous"></script>
<div id="signup_content" class="input_form"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js" integrity="sha384-..." crossorigin="anonymous"></script>
<h1>Please enter your email and choose a password.</h1>
<form name="signup_form" action="/signup_submit" method="post">
<div class="input-field"><input id='username' name='username' placeholder="User name" class="validate"></div>
<div class="input-field"><input id='email' name='email' placeholder="Email" class="validate"></div>
<div class="input-field"><input type="password" id='password' name='password' placeholder="Password" class="validate"pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" title="Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters" required></div>
<button type="button" onclick="users.validateInput({username, email, password})">Submit</button>
</form>
</div>
<!-- Custom JavaScript for Form Validation -->
<script>
// Example JavaScript for Disabling Form Submissions if There Are Invalid Fields
(function () {
'use strict'
// Fetch the Signup Form
var form = document.getElementById('signup_form')
// Add a 'submit' Event Listener
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})()
</script>
</body> </body>
</html> </html>
<style>
/* brighterStyles.css */
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
/* Global Styles */
body {
font-family: 'Roboto', sans-serif;
background-color: #121212;
color: #e0e0e0;
position: relative;
overflow: hidden;
padding-top: 80px; /* Adds space to prevent sliding too far */
}
/* Background Animation */
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('{{ url_for('static', filename='images/trading-bg.jpg') }}') no-repeat center center fixed;
background-size: cover;
filter: brightness(0.5);
z-index: -1;
}
/* Card Styles */
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
}
/* Input Group Styles */
.input-group-text {
background-color: #1f1f1f;
border: none;
color: #e0e0e0;
}
/* Button Styles */
.btn-primary {
background-color: #0d6efd;
border: none;
transition: background-color 0.3s ease;
height: 40px;
}
.btn-primary:hover {
background-color: #0b5ed7;
}
/* Social Buttons */
.btn-outline-light {
color: #e0e0e0;
border-color: #e0e0e0;
transition: background-color 0.3s ease, color 0.3s ease;
}
.btn-outline-light:hover {
background-color: #e0e0e0;
color: #121212;
}
/* Footer Link */
a.text-primary {
color: #0d6efd !important;
}
/* Responsive Adjustments */
@media (max-width: 576px) {
.card-body {
padding: 2rem;
}
.btn-primary {
padding: 0.75rem;
}
}
/* Center the logo and ensure proper spacing */
.logo {
display: block;
margin: 0 auto; /* Center horizontally */
max-height: 100px; /* Prevent the logo from being too large */
margin-top: 20px; /* Add space from the top */
}
/* Optional: Add spacing for mobile responsiveness */
@media (max-width: 576px) {
.logo {
max-height: 80px;
margin-top: 10px;
}
}
/* Adjust the AOS fade-up animation */
[data-aos="fade-up"] {
transform: translateY(20px); /* Move only 20px upwards */
}
[data-aos="fade-up"].aos-animate {
transform: translateY(0); /* Reset to the original position after animation */
}
.container {
margin-top: 50px; /* Ensures the container doesn't slide out of view */
}
</style>