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

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

View File

@ -1,4 +1,5 @@
import json
import logging
from typing import Any
from Users import Users
@ -12,6 +13,8 @@ from indicators import Indicators
from Signals import Signals
from trade import Trades
# Configure logging
logger = logging.getLogger(__name__)
class BrighterTrades:
def __init__(self, socketio):
@ -340,18 +343,22 @@ class BrighterTrades:
if not user_id:
return {"success": False, "message": "User ID not found"}
# Validate data types
# Validate data types and contents
if not isinstance(data['name'], str) or not data['name'].strip():
return {"success": False, "message": "Invalid or empty strategy name"}
if not isinstance(data['workspace'], str) or not data['workspace'].strip():
return {"success": False, "message": "Invalid or empty workspace data"}
if not isinstance(data['code'], dict) or not data['code']:
if not isinstance(data['code'], (dict, str)) or not data['code']:
return {"success": False, "message": "Invalid or empty strategy code"}
# Serialize code to JSON string for storage
code_json = json.dumps(data['code'])
try:
# Ensure 'code' is serialized as a JSON string
code_json = json.dumps(data['code']) if isinstance(data['code'], dict) else data['code']
except (TypeError, ValueError) as e:
return {"success": False, "message": f"Invalid strategy code: {str(e)}"}
# Prepare the strategy data for insertion
try:
strategy_data = {
"creator": user_id,
"name": data['name'].strip(),
@ -361,11 +368,31 @@ class BrighterTrades:
"public": int(data.get('public', 0)),
"fee": float(data.get('fee', 0.0))
}
except Exception as e:
return {"success": False, "message": f"Error preparing strategy data: {str(e)}"}
# The default source for undefined sources in the strategy.
# The default source for undefined sources in the strategy
try:
default_source = self.users.get_chart_view(user_name=user_name)
# Save the new strategy (in both cache and database) and return the result.
return self.strategies.new_strategy(strategy_data, default_source)
except Exception as e:
return {"success": False, "message": f"Error fetching chart view: {str(e)}"}
# Save the new strategy (in both cache and database) and handle the result
try:
result = self.strategies.new_strategy(strategy_data, default_source)
if result.get("success"):
return {
"success": True,
"strategy": result.get("strategy"), # Strategy object without `strategy_components`
"updated_at": result.get("updated_at"),
"message": result.get("message", "Strategy created successfully")
}
else:
return {"success": False, "message": result.get("message", "Failed to create strategy")}
except Exception as e:
# Log unexpected exceptions for debugging
logger.error(f"Error creating new strategy: {e}", exc_info=True)
return {"success": False, "message": "An unexpected error occurred while creating the strategy"}
def received_edit_strategy(self, data: dict) -> dict:
"""
@ -377,6 +404,7 @@ class BrighterTrades:
# Extract user_name and strategy name from the data
user_name = data.get('user_name')
strategy_name = data.get('name')
if not user_name:
return {"success": False, "message": "User not specified"}
if not strategy_name:
@ -417,10 +445,28 @@ class BrighterTrades:
"tbl_key": tbl_key # Include the tbl_key to identify the strategy
}
# The default source for undefined sources in the strategy.
# Get the default source for undefined sources in the strategy
try:
default_source = self.users.get_chart_view(user_name=user_name)
except Exception as e:
return {"success": False, "message": f"Error fetching chart view: {str(e)}"}
# Call the edit_strategy method to update the strategy
return self.strategies.edit_strategy(strategy_data, default_source)
try:
result = self.strategies.edit_strategy(strategy_data, default_source)
if result.get("success"):
return {
"success": True,
"strategy": result.get("strategy"), # Strategy object without `strategy_components`
"updated_at": result.get("updated_at"),
"message": result.get("message", "Strategy updated successfully")
}
else:
return {"success": False, "message": result.get("message", "Failed to update strategy")}
except Exception as e:
# Log unexpected exceptions
logger.error(f"Error editing strategy: {e}", exc_info=True)
return {"success": False, "message": "An unexpected error occurred while editing the strategy"}
def delete_strategy(self, data: dict) -> str | dict:
"""
@ -593,10 +639,25 @@ class BrighterTrades:
return self.trades.get_trades('dict')
def delete_backtest(self, msg_data):
""" Delete an existing backtest. """
""" Delete an existing backtest by interacting with the Backtester. """
backtest_name = msg_data.get('name')
if backtest_name in self.backtests:
del self.backtests[backtest_name]
user_name = msg_data.get('user_name')
if not backtest_name or not user_name:
return {"success": False, "message": "Missing backtest name or user name."}
# Construct the backtest_key based on Backtesters naming convention
backtest_key = f"backtest:{user_name}:{backtest_name}"
try:
# Delegate the deletion to the Backtester
self.backtester.remove_backtest(backtest_key)
return {"success": True, "message": f"Backtest '{backtest_name}' deleted successfully.",
"name": backtest_name}
except KeyError:
return {"success": False, "message": f"Backtest '{backtest_name}' not found."}
except Exception as e:
return {"success": False, "message": f"Error deleting backtest: {str(e)}"}
def adjust_setting(self, user_name: str, setting: str, params: Any):
"""
@ -729,12 +790,21 @@ class BrighterTrades:
return standard_reply("signal_created", r_data)
if msg_type == 'new_strategy':
try:
if r_data := self.received_new_strategy(msg_data):
return standard_reply("strategy_created", r_data)
except Exception as e:
logger.error(f"Error processing new_strategy: {e}", exc_info=True)
return standard_reply("strategy_error", {"message": "Failed to create strategy."})
if msg_type == 'edit_strategy':
try:
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 r_data := self.received_new_trade(msg_data):
@ -760,8 +830,8 @@ class BrighterTrades:
return standard_reply("backtest_submitted", resp)
if msg_type == 'delete_backtest':
self.delete_backtest(msg_data)
return standard_reply("backtest_deleted", {})
response = self.delete_backtest(msg_data)
return standard_reply("backtest_deleted", response)
if msg_type == 'reply':
# If the message is a reply log the response to the terminal.

View File

@ -340,6 +340,39 @@ class TableBasedCache:
except AttributeError as e:
raise AttributeError(f"Error in metadata processing: {e}")
def query_with_operator(self, conditions: List[Tuple[str, str, Any]]) -> pd.DataFrame:
"""
Query rows based on conditions and return valid (non-expired) entries.
Todo: test and merge with query().
:param conditions: List of tuples containing column name, operator, and value.
:return: Filtered DataFrame.
"""
self._purge_expired() # Remove expired rows before querying
# Start with the entire cache
result = self.cache.copy()
# Apply conditions using pandas filtering
if not result.empty and conditions:
for col, op, val in conditions:
if op.upper() == 'LIKE':
# Convert SQL LIKE pattern to regex
regex = '^' + val.replace('%', '.*') + '$'
result = result[result[col].astype(str).str.match(regex)]
elif op.upper() == 'IN' and isinstance(val, list):
result = result[result[col].isin(val)]
elif op == '=':
result = result[result[col] == val]
else:
# Add support for other operators as needed
raise ValueError(f"Unsupported operator '{op}' in filter conditions.")
# Remove the metadata and tbl_key columns for the result
if 'metadata' in result.columns:
result = result.drop(columns=['metadata'], errors='ignore')
return result
def query(self, conditions: List[Tuple[str, Any]]) -> pd.DataFrame:
"""Query rows based on conditions and return valid (non-expired) entries."""
self._purge_expired() # Remove expired rows before querying
@ -479,6 +512,37 @@ class CacheManager:
# No result return an empty Dataframe
return pd.DataFrame()
def get_rows_from_cache_with_operator(
self,
cache_name: str,
filter_vals: list[tuple[str, str, Any]]
) -> pd.DataFrame | None:
"""
Retrieves rows from the cache if available.
Todo: merge this with the one above and test.
:param cache_name: The key used to identify the cache.
:param filter_vals: A list of tuples, each containing a column name, an operator, and the value to filter by.
:return: A DataFrame containing the requested rows, or None if no matching rows are found.
:raises ValueError: If the cache is not a DataFrame or does not contain DataFrames in the 'data' column.
"""
# Check if the cache exists
if cache_name in self.caches:
cache = self.get_cache(cache_name)
# Ensure the cache contains DataFrames (required for querying)
if not isinstance(cache, (TableBasedCache, RowBasedCache)):
raise ValueError(f"Cache '{cache_name}' does not contain TableBasedCache or RowBasedCache.")
# Perform the query on the cache using filter_vals
filtered_cache = cache.query_with_operator(filter_vals) # Pass the list of filters
# If data is found in the cache, return it
if not filtered_cache.empty:
return filtered_cache
# No result return an empty DataFrame
return pd.DataFrame()
def get_cache_item(self, item_name: str, cache_name: str, filter_vals: tuple[str, any]) -> any:
"""
Retrieves a specific item from the cache.
@ -755,6 +819,56 @@ class DatabaseInteractions(SnapshotDataCache):
super().__init__()
self.db = Database()
def get_rows_from_datacache_with_operator(
self,
cache_name: str,
filter_vals: list[tuple[str, str, Any]] = None,
key: str = None,
include_tbl_key: bool = False
) -> pd.DataFrame | None:
"""
Retrieves rows from the cache if available; otherwise, queries the database and caches the result.
:param include_tbl_key: If True, includes 'tbl_key' in the returned DataFrame.
:param key: Optional key to filter by 'tbl_key'.
:param cache_name: The key used to identify the cache (also the name of the database table).
:param filter_vals: A list of tuples, each containing a column name, an operator, and the value to filter by.
Example: [('strategy_instance_id', 'LIKE', 'test_%')]
:return: A DataFrame containing the requested rows, or None if no matching rows are found.
:raises ValueError: If the cache is not a DataFrame or does not contain DataFrames in the 'data' column.
"""
# Ensure at least one of filter_vals or key is provided
if not filter_vals and not key:
raise ValueError("At least one of 'filter_vals' or 'key' must be provided.")
# Use an empty list if filter_vals is None
filter_vals = filter_vals or []
# Insert the key if provided
if key:
filter_vals.insert(0, ('tbl_key', '=', key))
# Perform the query on the cache using filter_vals
result = self.get_rows_from_cache_with_operator(cache_name, filter_vals)
# Fallback to database only if all operators are '='
if result.empty and all(op == '=' for _, op, _ in filter_vals):
# Extract (column, value) tuples for equality filters
equality_filters = [(col, val) for col, op, val in filter_vals if op == '=']
result = self._fetch_from_database(cache_name, equality_filters)
# Only use _fetch_from_database_with_list_support if any filter values are lists and all operators are '='
if result.empty and any(isinstance(val, list) for _, op, val in filter_vals) and all(
op == '=' for _, op, _ in filter_vals):
equality_filters = [(col, val) for col, op, val in filter_vals if op == '=']
result = self._fetch_from_database_with_list_support(cache_name, equality_filters)
# Remove 'tbl_key' unless include_tbl_key is True
if not include_tbl_key and 'tbl_key' in result.columns:
result = result.drop(columns=['tbl_key'], errors='ignore')
return result
def get_rows_from_datacache(self, cache_name: str, filter_vals: list[tuple[str, Any]] = None,
key: str = None, include_tbl_key: bool = False) -> pd.DataFrame | None:
"""

View File

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

View File

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

View File

@ -148,6 +148,25 @@ class Backtester:
except Exception as e:
logger.error(f"Error during cleanup of backtest '{backtest_key}': {e}", exc_info=True)
def remove_backtest(self, backtest_key: str):
"""
Remove a backtest from the 'tests' cache.
:param backtest_key: The unique key identifying the backtest.
"""
try:
self.data_cache.remove_row_from_datacache(
cache_name='tests',
filter_vals=[('tbl_key', backtest_key)],
remove_from_db = False # Assuming tests are transient and not stored in DB
)
logger.info(f"Backtest '{backtest_key}' removed from 'tests' cache.")
except KeyError:
logger.error(f"Backtest '{backtest_key}' not found in 'tests' cache.")
raise KeyError(f"Backtest '{backtest_key}' not found.")
except Exception as e:
logger.error(f"Error removing backtest '{backtest_key}': {e}", exc_info=True)
raise
def validate_strategy_components(self, user_id: str, strategy_name: str, user_name: str) -> dict:
"""
Retrieves and validates the components of a user-defined strategy.
@ -360,7 +379,8 @@ class Backtester:
precomputed_indicators=precomputed_indicators, # Pass precomputed indicators
socketio=self.socketio, # Pass SocketIO instance
socket_conn_id=socket_conn_id, # Pass SocketIO connection ID
data_length=len(data_feed) # Pass data length for progress updates
data_length=len(data_feed), # Pass data length for progress updates
backtest_name=backtest_name # Pass backtest_name for progress updates
)
# Add data feed to Cerebro
@ -395,7 +415,10 @@ class Backtester:
trades = strategy.trade_list
# Send 100% completion
self.socketio.emit('message', {'reply': 'progress', 'data': {'progress': 100}}, room=socket_conn_id)
self.socketio.emit('message', {'reply': 'progress',
'data': { 'test_id': backtest_name,
'progress': 100}}
, room=socket_conn_id)
# Prepare the results to pass into the callback
backtest_results = {
@ -531,36 +554,25 @@ class Backtester:
self.socketio.start_background_task(purge_task)
def cleanup_orphaned_backtest_contexts(self) -> None:
"""
Identifies and removes orphaned backtest contexts that do not have corresponding entries in 'tests' cache.
"""
try:
# Fetch all strategy_instance_ids from 'strategy_contexts' that start with 'test_'
strategy_contexts_df = self.data_cache.get_rows_from_datacache(
strategy_contexts_df = self.data_cache.get_rows_from_datacache_with_operator(
cache_name='strategy_contexts',
filter_vals=[] # Fetch all
filter_vals=[('strategy_instance_id', 'LIKE', 'test_%')]
)
if strategy_contexts_df.empty:
logger.debug("No strategy contexts found for cleanup.")
return
# Filter contexts that are backtests (strategy_instance_id starts with 'test_')
backtest_contexts = strategy_contexts_df[
strategy_contexts_df['strategy_instance_id'].astype(str).str.startswith('test_')
]
if backtest_contexts.empty:
logger.debug("No backtest contexts found for cleanup.")
return
for _, row in backtest_contexts.iterrows():
# Iterate through each backtest context
for _, row in strategy_contexts_df.iterrows():
strategy_instance_id = row['strategy_instance_id']
# Check if this backtest exists in 'tests' cache
# Since 'tests' cache uses 'backtest_key' as 'tbl_key', and it maps to 'strategy_instance_id'
# We'll need to search 'tests' cache for the corresponding 'strategy_instance_id'
found = False
tests_cache = self.data_cache.get_cache('tests')
if isinstance(tests_cache, RowBasedCache):
@ -573,10 +585,11 @@ class Backtester:
# Orphaned context found; proceed to remove it
self.data_cache.remove_row_from_datacache(
cache_name='strategy_contexts',
filter_vals=[('strategy_instance_id', strategy_instance_id)],
filter_vals=[('strategy_instance_id', strategy_instance_id)], # Correct format
remove_from_db=True
)
logger.info(f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.")
logger.info(
f"Orphaned backtest context '{strategy_instance_id}' removed from 'strategy_contexts' cache.")
except Exception as e:
logger.error(f"Error during cleanup of orphaned backtest contexts: {e}", exc_info=True)

View File

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

View File

@ -216,6 +216,16 @@ class StratDataManager {
this.strategies.push(data);
}
/**
* Retrieves a strategy by its tbl_key.
* @param {string} tbl_key - The tbl_key of the strategy to find.
* @returns {Object|null} - The strategy object or null if not found.
*/
getStrategyById(tbl_key) {
return this.strategies.find(strategy => strategy.tbl_key === tbl_key) || null;
}
/**
* Handles updates to the strategy itself (e.g., configuration changes).
@ -635,12 +645,18 @@ class Strategies {
}
/**
* Handles strategy-related error messages from the server.
* @param {Object} errorData - The data containing error details.
* Handles strategy-related errors sent from the server.
* @param {Object} errorData - The error message and additional details.
*/
handleStrategyError(errorData) {
console.error("Strategy Error:", errorData.message);
// Display a user-friendly error message
if (errorData.message) {
alert(`Error: ${errorData.message}`);
} else {
alert("An unknown error occurred while processing the strategy.");
}
}
/**
@ -665,6 +681,7 @@ class Strategies {
}
/**
* Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy.
@ -681,15 +698,38 @@ class Strategies {
}
}
/**
* Handles updates to the strategy itself (e.g., configuration changes).
* @param {Object} data - The updated strategy data.
* @param {Object} data - The server response containing strategy update metadata.
*/
handleStrategyUpdated(data) {
this.dataManager.updateStrategyData(data);
if (data.success) {
console.log("Strategy updated successfully:", data);
// Locate the strategy in the local state by its tbl_key
const updatedStrategyKey = data.strategy.tbl_key;
const updatedAt = data.updated_at;
const strategy = this.dataManager.getStrategyById(updatedStrategyKey);
if (strategy) {
// Update the relevant strategy data
Object.assign(strategy, data.strategy);
// Update the `updated_at` field
strategy.updated_at = updatedAt;
// Refresh the UI to reflect the updated metadata
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
} else {
console.warn("Updated strategy not found in local records:", updatedStrategyKey);
}
} else {
console.error("Failed to update strategy:", data.message);
alert(`Strategy update failed: ${data.message}`);
}
}
/**
* Handles the deletion of a strategy.

View File

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

View File

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

View File

@ -28,6 +28,9 @@ class User_Interface {
// Initialize other UI components here
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");
// Initialize Backtesting's DOM elements
this.backtesting.initialize();
} catch (error) {
console.error('Initialization failed:', error);
}

View File

@ -3,8 +3,7 @@
<!-- Draggable Header Section -->
<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-edit" style="display: none;">Edit Backtest</h1>
<h1 id="backtest-form-header">Create New Backtest</h1>
</div>
<!-- Main Content (Scrollable) -->
@ -36,8 +35,8 @@
<!-- Buttons -->
<div style="text-align: center;">
<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-edit" type="button" class="btn next" onclick="UI.backtesting.submitTest('edit')" style="display:none;">Edit 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()" style="display:none;">Edit Test</button>
</div>
<!-- Status message (Initially Hidden) -->
@ -51,7 +50,7 @@
</div>
<!-- Results section (Initially Hidden) -->
<div id="backtest-results" style="display: none; margin-top: 10px;">
<div id="backtest-results">
<h4>Test Results</h4>
<pre id="results_display"></pre>
</div>
@ -71,15 +70,41 @@
.backtest-item {
position: relative;
width: 150px;
height: 120px;
width: 100px;
height: 100px;
text-align: center;
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 {
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 {
position: absolute;
@ -92,6 +117,7 @@
color: black;
text-align: center;
width: 100px;
font-size: 12px;
}
.delete-button {
@ -130,5 +156,12 @@
#results_display th {
background-color: #f2f2f2;
}
#backtest-results {
margin-top: 10px;
display: none; /* Initially hidden */
}
#backtest-results.show {
display: block; /* Show when backtest is complete */
}
</style>

View File

@ -1,7 +1,6 @@
<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>
<h3>Back Tests</h3>
<div class="backtests-container" id="backtest_display"></div>
</div>

View File

@ -16,7 +16,7 @@
<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 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>
<p>Dont have an account? <a href="/signup">Sign Up</a></p>

View File

@ -2,25 +2,276 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Load style sheets and set the title -->
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='brighterStyles.css') }}">
<title>{{ title }}</title>
<script src="{{ url_for('static', filename='user.js') }}"></script>
<script type="text/javascript">
users = new Users('signup_form');
<!-- Responsive Meta Tag -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 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>
</head>
<body id="sign_up_body">
<div id="signup_content" class="input_form">
<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>
<!-- Bootstrap 5 JS and Dependencies (Popper) -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/dist/umd/popper.min.js" integrity="sha384-..." crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js" integrity="sha384-..." crossorigin="anonymous"></script>
<!-- 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>
</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>