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:
parent
fe1e93c24b
commit
2c644147a4
|
|
@ -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 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):
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
12
src/app.py
12
src/app.py
|
|
@ -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'])
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}%")
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}')">✘</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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>Don’t have an account? <a href="/signup">Sign Up</a></p>
|
<p>Don’t have an account? <a href="/signup">Sign Up</a></p>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue