The strategies class is broken into smaller classes. I believe the ui is mostly functional at this point. It is time to get the tests working.

This commit is contained in:
Rob 2024-10-10 17:05:07 -03:00
parent 89e0f8b849
commit 4eda0b6f81
10 changed files with 762 additions and 405 deletions

View File

@ -1,3 +1,4 @@
import json
from typing import Any
from Users import Users
@ -319,7 +320,7 @@ class BrighterTrades:
self.config.set_setting('signals_list', self.signals.get_signals('dict'))
return data
def received_new_strategy(self, data: dict) -> str | dict:
def received_new_strategy(self, data: dict) -> dict:
"""
Handles the creation of a new strategy based on the provided data.
@ -426,7 +427,7 @@ class BrighterTrades:
return {"success": False, "message": "strategy_name not found"}
self.strategies.delete_strategy(user_id=user_id, name=strategy_name)
return {"success": True, "message": "Strategy {strategy_name} deleted"}
return {"success": True, "message": "Strategy deleted", "strategy_name": strategy_name}
def delete_signal(self, signal_name: str) -> None:
"""
@ -450,13 +451,13 @@ class BrighterTrades:
"""
return self.signals.get_signals('json')
def get_strategies_json(self, user_id) -> str:
def get_strategies_json(self, user_id) -> list:
"""
Retrieve all the strategies from the strategies instance and return them as a JSON object.
Retrieve all the strategies from the strategies instance and return them as a list of dictionaries.
:return: str - A JSON object containing all the strategies.
:return: list - A list of dictionaries, each representing a strategy.
"""
return self.strategies.get_all_strategies(user_id, 'json')
return self.strategies.get_all_strategies(user_id, 'dict')
def connect_or_config_exchange(self, user_name: str, exchange_name: str, api_keys: dict = None) -> dict:
"""
@ -626,7 +627,12 @@ class BrighterTrades:
user_name=user_name, default_market=market)
elif setting == 'toggle_indicator':
indicators_to_toggle = params.getlist('indicator')
# Parse the indicator field as a JSON array
try:
indicators_to_toggle = json.loads(params.get('indicator', '[]'))
except json.JSONDecodeError:
indicators_to_toggle = []
user_id = self.get_user_info(user_name=user_name, info='User_id')
self.indicators.toggle_indicators(user_id=user_id, indicator_names=indicators_to_toggle)
@ -643,9 +649,6 @@ class BrighterTrades:
print(f'ERROR SETTING VALUE')
print(f'The string received by the server was: /n{params}')
# Todo this doesn't seem necessary anymore, because the cache now updates per request.
# Now that the state is changed reload price history.
# self.candles.set_cache(user_name=user_name)
return
def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None:
@ -692,7 +695,12 @@ class BrighterTrades:
self.delete_signal(msg_data)
if msg_type == 'delete_strategy':
self.delete_strategy(msg_data)
result = self.delete_strategy(msg_data)
if result.get('success'):
return standard_reply("strategy_deleted",
{"message": result.get('message'), "strategy_name": result.get('strategy_name')})
else:
return standard_reply("strategy_error", {"message": result.get('message')})
if msg_type == 'close_trade':
self.close_trade(msg_data)

View File

@ -734,6 +734,7 @@ class DatabaseInteractions(SnapshotDataCache):
: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 and the value(s) to filter by.
If a value is a list, it will use the SQL 'IN' clause.
: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.
"""
@ -748,17 +749,62 @@ class DatabaseInteractions(SnapshotDataCache):
if key:
filter_vals.insert(0, ('tbl_key', key))
# Convert filter values that are lists to 'IN' clauses for cache filtering
result = self.get_rows_from_cache(cache_name, filter_vals)
# Fallback to database if no result found in cache
if result.empty:
# Fallback: Fetch from the database and cache the result if necessary
result = self._fetch_from_database(cache_name, filter_vals)
# Only use _fetch_from_database_with_list_support if any filter values are lists
if result.empty and any(isinstance(val, list) for _, val in filter_vals):
result = self._fetch_from_database_with_list_support(cache_name, filter_vals)
# Remove 'tbl_key' unless include_tbl_key is True
if not include_tbl_key:
result = result.drop(columns=['tbl_key'], errors='ignore')
return result
def _fetch_from_database_with_list_support(self, cache_name: str,
filter_vals: List[tuple[str, Any]]) -> pd.DataFrame:
"""
Fetch rows from the database, supporting list values that require SQL 'IN' clauses.
:param cache_name: The name of the table or key used to store/retrieve data.
:param filter_vals: A list of tuples with the filter column and value, supporting lists for 'IN' clause.
:return: A DataFrame with the fetched rows, or None if no data is found.
"""
where_clauses = []
params = []
for col, val in filter_vals:
if isinstance(val, list):
placeholders = ', '.join('?' for _ in val)
where_clauses.append(f"{col} IN ({placeholders})")
params.extend(val)
else:
where_clauses.append(f"{col} = ?")
params.append(val)
where_clause = " AND ".join(where_clauses)
sql_query = f"SELECT * FROM {cache_name} WHERE {where_clause}"
# Execute the SQL query with the prepared parameters
rows = self.db.get_rows_where(sql_query, params)
# Cache the result (either row or table based)
if rows is not None and not rows.empty:
cache = self.get_cache(cache_name)
if isinstance(cache, RowBasedCache):
for _, row in rows.iterrows():
cache.add_entry(key=row['tbl_key'], data=row)
else:
cache.add_table(rows, overwrite='tbl_key')
return rows
def _fetch_from_database(self, cache_name: str, filter_vals: List[tuple[str, Any]]) -> pd.DataFrame:
"""
Fetch rows from the database and cache the result.
@ -929,6 +975,71 @@ class DatabaseInteractions(SnapshotDataCache):
# Execute the SQL update to modify the database
self.db.execute_sql(sql_update, params)
def modify_multiple_datacache_items(self, cache_name: str, filter_vals: List[Tuple[str, any]],
field_names: Tuple[str, ...], new_values: Tuple[Any, ...],
key: str = None, overwrite: str = None) -> None:
"""
Modifies specific fields in multiple rows within the cache and updates the database accordingly.
:param cache_name: The name used to identify the cache.
:param filter_vals: A list of tuples containing column names and values to filter by.
If a filter value is a list, it will be used with the 'IN' clause.
:param field_names: A tuple of field names to be updated.
:param new_values: A tuple of new values corresponding to field_names.
:param key: Optional key to identify the entry.
:param overwrite: Column name(s) to use for overwriting in the cache.
:raises ValueError: If no rows are found.
"""
if key:
filter_vals.insert(0, ('tbl_key', key))
# Prepare the SQL query
where_clauses = []
query_params = []
for col, val in filter_vals:
if isinstance(val, list):
# Use the 'IN' clause if the value is a list
placeholders = ', '.join('?' for _ in val)
where_clauses.append(f"{col} IN ({placeholders})")
query_params.extend(val)
else:
where_clauses.append(f"{col} = ?")
query_params.append(val)
# Build the SQL query string
where_clause = " AND ".join(where_clauses)
set_clause = ", ".join([f"{field} = ?" for field in field_names])
sql_update = f"UPDATE {cache_name} SET {set_clause} WHERE {where_clause}"
# Add the new values to the parameters list
query_params = list(new_values) + query_params
# Execute the SQL update to modify the database
self.db.execute_sql(sql_update, query_params)
# Retrieve the rows from the cache to update the cache
rows = self.get_rows_from_datacache(cache_name=cache_name, filter_vals=filter_vals)
if rows is None or rows.empty:
raise ValueError(f"Rows not found in cache or database for {filter_vals}")
# Update the cache with the new values
for field_name, new_value in zip(field_names, new_values):
rows[field_name] = new_value
# Get the cache instance
cache = self.get_cache(cache_name)
if isinstance(cache, RowBasedCache):
for _, row in rows.iterrows():
key_value = row['tbl_key']
cache.add_entry(key=key_value, data=row)
elif isinstance(cache, TableBasedCache):
cache.add_table(rows, overwrite=overwrite)
else:
raise ValueError(f"Unsupported cache type for {cache_name}")
def serialized_datacache_insert(self, cache_name: str, data: Any, key: str = None,
do_not_overwrite: bool = False):
"""

View File

@ -129,11 +129,22 @@ class Database:
"""
try:
with SQLite(self.db_file) as con:
# Construct the WHERE clause with multiple conditions
where_clause = " AND ".join([f"{col} = ?" for col, _ in filter_vals])
params = [val for _, val in filter_vals]
where_clauses = []
params = []
# Construct the WHERE clause, handling lists for 'IN' conditions
for col, val in filter_vals:
if isinstance(val, list):
# If the value is a list, use the 'IN' clause
placeholders = ', '.join('?' for _ in val)
where_clauses.append(f"{col} IN ({placeholders})")
params.extend(val) # Extend the parameters with the list values
else:
where_clauses.append(f"{col} = ?")
params.append(val)
# Prepare and execute the query with the constructed WHERE clause
where_clause = " AND ".join(where_clauses)
qry = f"SELECT * FROM {table} WHERE {where_clause}"
result = pd.read_sql(qry, con, params=params)

View File

@ -230,9 +230,24 @@ class Strategies:
tbl_key
)
)
# Construct the saved strategy data to return
saved_strategy = {
"id": tbl_key, # Assuming tbl_key is used as a unique identifier
"creator": data.get('creator'),
"name": data['name'],
"workspace": data['workspace'], # Original workspace data
"code": data['code'],
"stats": data.get('stats', {}),
"public": data.get('public', False),
"fee": data.get('fee', 0)
}
# If everything is successful, return a success message
return {"success": True, "message": "Strategy created and saved successfully"}
# along with the saved strategy data
return {
"success": True,
"message": "Strategy created and saved successfully",
"strategy": saved_strategy # Include the strategy data
}
except Exception as e:
# Catch any exceptions and return a failure message

View File

@ -362,7 +362,7 @@ class Indicators:
return
# Set visibility for all indicators off
self.cache_manager.modify_datacache_item(
self.cache_manager.modify_multiple_datacache_items(
cache_name='indicators',
filter_vals=[('creator', str(user_id))],
field_names=('visible',),
@ -371,13 +371,14 @@ class Indicators:
)
# Set visibility for the specified indicators on
self.cache_manager.modify_datacache_item(
cache_name='indicators',
filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
field_names=('visible',),
new_values=(True,),
overwrite='name'
)
if indicator_names:
self.cache_manager.modify_multiple_datacache_items(
cache_name='indicators',
filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
field_names=('visible',),
new_values=(True,),
overwrite='name'
)
def edit_indicator(self, user_name: str, params: dict):
"""
@ -385,64 +386,74 @@ class Indicators:
:param user_name: The name of the user.
:param params: The updated properties of the indicator.
"""
indicator_name = params.get('name')
if not indicator_name:
raise ValueError("Indicator name is required for editing.")
try:
indicator_name = params.get('name')
if not indicator_name:
raise ValueError("Indicator name is required for editing.")
# Get the indicator from the user's indicator list
user_id = self.users.get_id(user_name)
indicator = self.cache_manager.get_rows_from_datacache('indicators',
[('name', indicator_name), ('creator', str(user_id))])
if indicator.empty:
raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
# Compare existing_properties and new_properties as strings
new_properties = params.get('properties')
if new_properties is not None:
new_properties_str = json.dumps(new_properties, sort_keys=True) # Convert new properties to a JSON string
existing_properties_str = indicator['properties'].iloc[0] # Existing properties are already a JSON string
# Compare the strings directly
if existing_properties_str != new_properties_str:
self.cache_manager.modify_datacache_item(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
field_name=('properties',),
new_data=(new_properties,),
overwrite='name'
)
# Compare existing_source and new_source as strings
new_source = params.get('source')
if new_source is not None:
new_source_str = json.dumps(new_source, sort_keys=True) # Convert new source to a JSON string
existing_source_str = indicator['source'].iloc[
0] if 'source' in indicator else None # Existing source as JSON string
# Compare the strings directly
if existing_source_str != new_source_str and new_source_str is not None:
self.cache_manager.modify_datacache_item(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
field_name=('source',),
new_data=(new_source,),
overwrite='name'
)
# Convert current_visible to boolean and check if it has changed
current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes']
new_visible = bool(params.get('visible'))
if current_visible != new_visible:
self.cache_manager.modify_datacache_item(
# Get user ID and retrieve the indicator
user_id = self.users.get_id(user_name)
indicator = self.cache_manager.get_rows_from_datacache(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
field_name=('visible',),
new_data=(new_visible,),
overwrite='name'
[('name', indicator_name), ('creator', str(user_id))]
)
if indicator.empty:
raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
# Validate and update 'properties'
new_properties = params.get('properties')
if new_properties is not None:
if not isinstance(new_properties, dict):
raise ValueError("'properties' must be a dictionary.")
# Serialize the dictionary to a JSON string
new_properties_json = json.dumps(new_properties, sort_keys=True)
# Retrieve existing properties as JSON string
existing_properties_str = indicator['properties'].iloc[0]
if existing_properties_str != new_properties_json:
self.cache_manager.modify_datacache_item(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
('properties',), # field_names as a single-element tuple
(new_properties_json,), # new_values as a single-element tuple
overwrite='name' # overwrite remains as a keyword argument
)
# Validate and update 'source'
new_source = params.get('source')
if new_source is not None:
if not isinstance(new_source, dict):
raise ValueError("'source' must be a dictionary.")
# Serialize the dictionary to a JSON string
new_source_json = json.dumps(new_source, sort_keys=True)
# Retrieve existing source as JSON string
existing_source_str = indicator['source'].iloc[0] if 'source' in indicator else None
if existing_source_str != new_source_json and new_source_json is not None:
self.cache_manager.modify_datacache_item(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
('source',), # field_names as a single-element tuple
(new_source_json,), # new_values as a single-element tuple
overwrite='name' # overwrite remains as a keyword argument
)
# Validate and update 'visible'
current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes']
new_visible = bool(params.get('visible'))
if current_visible != new_visible:
self.cache_manager.modify_datacache_item(
'indicators',
[('creator', str(user_id)), ('name', indicator_name)],
('visible',), # field_names as a single-element tuple
(new_visible,), # new_values as a single-element tuple
overwrite='name' # overwrite remains as a keyword argument
)
except Exception as e:
raise e
def new_indicator(self, user_name: str, params) -> None:
"""
Appends a new indicator to a user-specific collection of Indicator definitions.

View File

@ -1,85 +1,215 @@
class Strategies {
class StratUIManager {
constructor() {
// The HTML element that displays the strategies.
this.target_el = null;
// The HTML element that displays the creation form.
this.targetEl = null;
this.formElement = null;
// The class responsible for handling server communications.
this.comms = null;
// The class responsible for keeping user data.
this.data = null;
// The list of strategies.
this.strategies = [];
// The Blockly workspace.
this.workspace = null;
// Flag to indicate if the instance is initialized.
this._initialized = false;
}
/**
* Initializes the Strategies instance with necessary dependencies.
* @param {string} target_id - The ID of the HTML element where strategies will be displayed.
* Initializes the UI elements with provided IDs.
* @param {string} targetId - The ID of the HTML element where strategies will be displayed.
* @param {string} formElId - The ID of the HTML element for the strategy creation form.
* @param {Object} data - An object containing user data and communication instances.
*/
initialize(target_id, formElId, data) {
try {
// Get the target element for displaying strategies
this.target_el = document.getElementById(target_id);
if (!this.target_el) {
throw new Error(`Element for displaying strategies "${target_id}" not found.`);
}
initUI(targetId, formElId) {
// Get the target element for displaying strategies
this.targetEl = document.getElementById(targetId);
if (!this.targetEl) {
throw new Error(`Element for displaying strategies "${targetId}" not found.`);
}
// Get the form element for strategy creation
this.formElement = document.getElementById(formElId);
if (!this.formElement) {
throw new Error(`Strategies form element "${formElId}" not found.`);
}
if (!data || typeof data !== 'object') {
throw new Error("Invalid data object provided for initialization.");
}
this.data = data;
if (!this.data.user_name || typeof this.data.user_name !== 'string') {
throw new Error("Invalid user_name provided in data object.");
}
this.comms = this.data?.comms;
if (!this.comms) {
throw new Error('Communications instance not provided in data.');
}
// Register handlers with Comms for specific message types
this.comms.on('strategy_created', this.handleStrategyCreated.bind(this));
this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this));
this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this));
this.comms.on('updates', this.handleUpdates.bind(this));
// Fetch saved strategies from the server
this.fetchSavedStrategies();
this._initialized = true;
} catch (error) {
console.error("Error initializing Strategies instance:", error.message);
// Get the form element for strategy creation
this.formElement = document.getElementById(formElId);
if (!this.formElement) {
throw new Error(`Strategies form element "${formElId}" not found.`);
}
}
/**
* Displays the form for creating or editing a strategy.
* @param {string} action - The action to perform ('new' or 'edit').
* @param {string|null} strategyData - The data of the strategy to edit (only applicable for 'edit' action).
*/
displayForm(action, strategyData = null) {
console.log(`Opening form for action: ${action}, strategy: ${strategyData?.name}`);
if (this.formElement) {
const headerTitle = this.formElement.querySelector("#draggable_header h1");
const submitCreateBtn = this.formElement.querySelector("#submit-create");
const submitEditBtn = this.formElement.querySelector("#submit-edit");
const nameBox = this.formElement.querySelector('#name_box');
const publicCheckbox = this.formElement.querySelector('#public_checkbox');
const feeBox = this.formElement.querySelector('#fee_box');
if (!headerTitle || !submitCreateBtn || !submitEditBtn || !nameBox || !publicCheckbox || !feeBox) {
console.error('One or more form elements were not found.');
return;
}
// Update form based on action
if (action === 'new') {
headerTitle.textContent = "Create New Strategy";
submitCreateBtn.style.display = "inline-block";
submitEditBtn.style.display = "none";
nameBox.value = '';
publicCheckbox.checked = false;
feeBox.value = 0;
} else if (action === 'edit' && strategyData) {
headerTitle.textContent = "Edit Strategy";
submitCreateBtn.style.display = "none";
submitEditBtn.style.display = "inline-block";
nameBox.value = strategyData.name;
publicCheckbox.checked = strategyData.public === 1;
feeBox.value = strategyData.fee || 0;
}
// Display the form
this.formElement.style.display = "grid";
// Call the workspace manager to initialize the Blockly workspace after the form becomes visible
if (UI.strats && UI.strats.workspaceManager) {
setTimeout(() => {
UI.strats.workspaceManager.initWorkspace();
}, 100); // Delay slightly to allow the form to render properly
} else {
console.error("Workspace manager is not initialized or is unavailable.");
}
} else {
console.error(`Form element "${this.formElement.id}" not found.`);
}
}
/**
* Hides the "Create New Strategy" form by adding a 'hidden' class.
*/
hideForm() {
if (this.formElement) {
this.formElement.style.display = 'none'; // Hide the form
}
}
/**
* Updates the HTML representation of the strategies.
* @param {Object[]} strategies - The list of strategies to display.
*/
updateStrategiesHtml(strategies) {
if (this.targetEl) {
// Clear existing content
while (this.targetEl.firstChild) {
this.targetEl.removeChild(this.targetEl.firstChild);
}
// Create and append new elements for all strategies
for (let strat of strategies) {
const strategyItem = document.createElement('div');
strategyItem.className = 'strategy-item';
// Delete button
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.innerHTML = '✘';
deleteButton.addEventListener('click', () => {
if (this.onDeleteStrategy) {
this.onDeleteStrategy(strat.name); // Call the callback set by Strategies
} else {
console.error("Delete strategy callback is not set.");
}
});
strategyItem.appendChild(deleteButton);
// Strategy icon
const strategyIcon = document.createElement('div');
strategyIcon.className = 'strategy-icon';
strategyIcon.addEventListener('click', () => {
this.displayForm('edit', strat); // Open the form with strategy data when clicked
});
// Strategy name
const strategyName = document.createElement('div');
strategyName.className = 'strategy-name';
strategyName.textContent = strat.name || 'Unnamed Strategy'; // Fallback for undefined
strategyIcon.appendChild(strategyName);
strategyItem.appendChild(strategyIcon);
// Strategy hover details
const strategyHover = document.createElement('div');
strategyHover.className = 'strategy-hover';
strategyHover.innerHTML = `<strong>${strat.name || 'Unnamed Strategy'}</strong><br>Stats: ${JSON.stringify(strat.stats, null, 2)}`;
strategyItem.appendChild(strategyHover);
// Append to target element
this.targetEl.appendChild(strategyItem);
}
} else {
console.error("Target element for updating strategies is not set.");
}
}
/**
* Toggles the fee input field based on the state of the public checkbox.
*/
toggleFeeInput() {
const publicCheckbox = document.getElementById('public_checkbox');
const feeBox = document.getElementById('fee_box');
if (publicCheckbox && feeBox) {
feeBox.disabled = !publicCheckbox.checked;
}
}
/**
* Sets the callback function for deleting a strategy.
* @param {Function} callback - The callback function to call when deleting a strategy.
*/
registerDeleteStrategyCallback(callback) {
this.onDeleteStrategy = callback;
}
}
class StratDataManager {
constructor() {
this.strategies = [];
}
/**
* Fetches the saved strategies from the server.
* @param {Object} comms - The communications instance to interact with the server.
* @param {Object} data - An object containing user data.
*/
fetchSavedStrategies(comms, data) {
if (comms) {
try {
const requestData = {
request: 'strategies',
user_name: data?.user_name
};
comms.sendToApp('request', requestData);
} catch (error) {
console.error("Error fetching saved strategies:", error.message);
alert('Unable to connect to the server. Please check your connection or try reinitializing the application.');
}
} else {
throw new Error('Communications instance not available.');
}
}
/**
* Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy.
*/
handleStrategyCreated(data) {
console.log("New strategy created:", data);
// Add the new strategy to the list without fetching from the server again
addNewStrategy(data) {
console.log("Adding new strategy. Data:", data);
if (!data.name) {
console.error("Strategy data missing 'name' field:", data);
}
this.strategies.push(data);
// Update the UI
this.updateHtml();
}
/**
* Handles updates to the strategy itself (e.g., configuration changes).
* @param {Object} data - The updated strategy data.
*/
handleStrategyUpdated(data) {
updateStrategyData(data) {
console.log("Strategy updated:", data);
const index = this.strategies.findIndex(strategy => strategy.id === data.id);
if (index !== -1) {
@ -87,22 +217,16 @@ class Strategies {
} else {
this.strategies.push(data); // Add if not found
}
this.updateHtml();
}
/**
* Handles the deletion of a strategy.
* @param {Object} data - The data for the deleted strategy.
* @param {Object} data - The name for the deleted strategy.
*/
handleStrategyDeleted(data) {
removeStrategy(name) {
try {
console.log("Strategy deleted:", data);
// Remove the strategy from the local array
this.strategies = this.strategies.filter(strat => strat.name !== data.strategy_name);
// Update the UI
this.updateHtml();
console.log("Strategy deleted:", name);
this.strategies = this.strategies.filter(strat => strat.name !== name);
} catch (error) {
console.error("Error handling strategy deletion:", error.message);
}
@ -112,10 +236,10 @@ class Strategies {
* Handles batch updates for strategies, such as multiple configuration or performance updates.
* @param {Object} data - The data containing batch updates for strategies.
*/
handleUpdates(data) {
applyBatchUpdates(data) {
const { stg_updts } = data;
if (stg_updts) {
stg_updts.forEach(strategy => this.handleStrategyUpdated(strategy));
stg_updts.forEach(strategy => this.updateStrategyData(strategy));
}
}
@ -123,55 +247,66 @@ class Strategies {
* Returns all available strategies.
* @returns {Object[]} - The list of available strategies.
*/
getAvailableStrategies() {
getAllStrategies() {
return this.strategies;
}
}
class StratWorkspaceManager {
constructor() {
this.workspace = null;
this.blocksDefined = false;
}
/**
* Creates the Blockly workspace with custom blocks and generators.
* Initializes the Blockly workspace with custom blocks and generators.
* Ensures required elements are present in the DOM and initializes the workspace.
* @async
* @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found.
*/
async createWorkspace() {
// Ensure 'blocklyDiv' exists in the DOM
initWorkspace() {
if (!document.getElementById('blocklyDiv')) {
console.error("blocklyDiv is not loaded.");
return;
}
// Dispose of the existing workspace if it exists
if (this.workspace) {
this.workspace.dispose();
}
try {
// Load all modules concurrently to reduce loading time
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
import('./custom_blocks.js'),
import('./indicator_blocks.js'),
import('./python_generators.js'),
import('./json_generators.js')
]);
// Initialize custom blocks and Blockly workspace
this._loadModulesAndInitWorkspace();
}
// Define custom blocks
customBlocksModule.defineCustomBlocks();
indicatorBlocksModule.defineIndicatorBlocks();
pythonGeneratorsModule.definePythonGenerators();
jsonGeneratorsModule.defineJsonGenerators();
} catch (error) {
console.error("Error loading Blockly modules: ", error);
return;
async _loadModulesAndInitWorkspace() {
if (!this.blocksDefined) {
try {
// Load all modules concurrently to reduce loading time
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
import('./custom_blocks.js'),
import('./indicator_blocks.js'),
import('./python_generators.js'),
import('./json_generators.js')
]);
// Define custom blocks
customBlocksModule.defineCustomBlocks();
indicatorBlocksModule.defineIndicatorBlocks();
pythonGeneratorsModule.definePythonGenerators();
jsonGeneratorsModule.defineJsonGenerators();
} catch (error) {
console.error("Error loading Blockly modules: ", error);
return;
}
this.blocksDefined = true;
}
// Ensure 'toolbox' exists in the DOM
const toolboxElement = document.getElementById('toolbox');
if (!toolboxElement) {
console.error("toolbox is not loaded.");
return;
}
// Initialize the Blockly workspace
this.workspace = Blockly.inject('blocklyDiv', {
toolbox: toolboxElement,
scrollbars: true,
@ -193,8 +328,10 @@ class Strategies {
});
}
// Resize the Blockly workspace
resizeWorkspace() {
/**
* Adjusts the Blockly workspace dimensions to fit within the container.
*/
adjustWorkspace() {
const blocklyDiv = document.getElementById('blocklyDiv');
if (blocklyDiv && this.workspace) {
Blockly.svgResize(this.workspace);
@ -207,7 +344,7 @@ class Strategies {
* Generates the strategy data including Python code, JSON representation, and workspace XML.
* @returns {string} - A JSON string containing the strategy data.
*/
generateStrategyJson() {
compileStrategyJson() {
if (!this.workspace) {
console.error("Workspace is not available.");
return;
@ -230,7 +367,6 @@ class Strategies {
const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace);
const workspaceXmlText = Blockly.Xml.domToText(workspaceXml);
// Compile and return all information as a JSON string
return JSON.stringify({
name: strategyName,
code: pythonCode,
@ -239,7 +375,6 @@ class Strategies {
});
}
/**
* Generates a JSON representation of the strategy from the workspace.
* @private
@ -247,8 +382,7 @@ class Strategies {
*/
_generateStrategyJsonFromWorkspace() {
const topBlocks = this.workspace.getTopBlocks(true);
const strategyJson = topBlocks.map(block => this._blockToJson(block));
return strategyJson;
return topBlocks.map(block => this._blockToJson(block));
}
/**
@ -265,7 +399,6 @@ class Strategies {
statements: {}
};
// Get field values
block.inputList.forEach(input => {
if (input.fieldRow) {
input.fieldRow.forEach(field => {
@ -282,13 +415,11 @@ class Strategies {
} else if (input.type === Blockly.NEXT_STATEMENT) {
json.statements[input.name] = this._blockToJson(targetBlock);
}
}
});
// Handle next blocks (in statements)
if (block.getNextBlock()) {
json.next = this.blockToJson(block.getNextBlock());
json.next = this._blockToJson(block.getNextBlock());
}
return json;
@ -298,30 +429,23 @@ class Strategies {
* Restores the Blockly workspace from an XML string.
* @param {string} workspaceXmlText - The XML text representing the workspace.
*/
_restoreWorkspaceFromXml(workspaceXmlText) {
loadWorkspaceFromXml(workspaceXmlText) {
try {
if (!this.workspace) {
console.error("Cannot restore workspace: Blockly workspace is not initialized.");
return;
}
// Parse the XML text into an XML DOM object
const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText);
// Validate that the XML is not empty and has child nodes
if (!workspaceXml || !workspaceXml.hasChildNodes()) {
console.error('Invalid workspace XML provided.');
alert('The provided workspace data is invalid and cannot be loaded.');
return;
}
// Clear the current workspace
this.workspace.clear();
// Load the XML DOM into the workspace
Blockly.Xml.domToWorkspace(workspaceXml, this.workspace);
} catch (error) {
// Handle specific errors if possible
if (error instanceof SyntaxError) {
console.error('Syntax error in workspace XML:', error.message);
alert('There was a syntax error in the workspace data. Please check the data and try again.');
@ -331,48 +455,173 @@ class Strategies {
}
}
}
}
class Strategies {
constructor() {
this.uiManager = new StratUIManager();
this.dataManager = new StratDataManager();
this.workspaceManager = new StratWorkspaceManager();
this.comms = null;
this.data = null;
this._initialized = false;
// Set the delete callback
this.uiManager.registerDeleteStrategyCallback(this.deleteStrategy.bind(this));
}
/**
* Fetches the saved strategies from the server.
* Initializes the Strategies instance with necessary dependencies.
* @param {string} targetId - The ID of the HTML element where strategies will be displayed.
* @param {string} formElId - The ID of the HTML element for the strategy creation form.
* @param {Object} data - An object containing user data and communication instances.
*/
fetchSavedStrategies() {
if (this.comms) {
try {
// Prepare request data, including user name if available
const requestData = {
request: 'strategies',
user_name: this.data?.user_name
};
// Send request to application server
this.comms.sendToApp('request', requestData);
} catch (error) {
console.error("Error fetching saved strategies:", error.message);
alert('Unable to connect to the server. Please check your connection or try reinitializing the application.');
initialize(targetId, formElId, data) {
try {
// Initialize UI Manager
this.uiManager.initUI(targetId, formElId);
if (!data || typeof data !== 'object') {
throw new Error("Invalid data object provided for initialization.");
}
this.data = data;
if (!this.data.user_name || typeof this.data.user_name !== 'string') {
throw new Error("Invalid user_name provided in data object.");
}
this.comms = this.data?.comms;
if (!this.comms) {
throw new Error('Communications instance not provided in data.');
}
// Register handlers with Comms for specific message types
this.comms.on('strategy_created', this.handleStrategyCreated.bind(this));
this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this));
this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this));
this.comms.on('updates', this.handleUpdates.bind(this));
this.comms.on('strategies', this.handleStrategies.bind(this));
// Register the handler for 'strategy_error' reply
this.comms.on('strategy_error', this.handleStrategyError.bind(this));
// Fetch saved strategies using DataManager
this.dataManager.fetchSavedStrategies(this.comms, this.data);
this._initialized = true;
} catch (error) {
console.error("Error initializing Strategies instance:", error.message);
}
}
/**
* Handles strategy-related error messages from the server.
* @param {Object} errorData - The data containing error details.
*/
handleStrategyError(errorData) {
console.error("Strategy Error:", errorData.message);
alert(`Error: ${errorData.message}`);
}
/**
* Handles the reception of existing strategies from the server.
* @param {Array} data - The data containing the list of existing strategies.
*/
handleStrategies(data) {
console.log("Received strategies data:", data);
if (Array.isArray(data)) {
console.log(`Number of strategies received: ${data.length}`);
// Update the DataManager with the received strategies
this.dataManager.strategies = data;
// Update the UI to display the strategies
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
console.log("Successfully loaded strategies.");
} else {
throw new Error('Communications instance not available.');
console.error("Failed to load strategies: Invalid data format");
alert("Failed to load strategies: Invalid data format");
}
}
/**
* Updates the strategies with the data received from the server and refreshes the UI.
* @param {string|Array} data - The strategies data as a JSON string or an array of strategy objects.
* Handles the creation of a new strategy.
* @param {Object} data - The data for the newly created strategy.
*/
set_data(data) {
if (typeof data === 'string') {
data = JSON.parse(data);
handleStrategyCreated(data) {
console.log("handleStrategyCreated received data:", data);
if (data.success && data.strategy) {
this.dataManager.addNewStrategy(data.strategy);
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
} else {
console.error("Failed to create strategy:", data.message);
alert(`Strategy creation failed: ${data.message}`);
}
this.strategies = data;
this.updateHtml(); // Refresh the strategies display
}
/**
* Handles updates to the strategy itself (e.g., configuration changes).
* @param {Object} data - The updated strategy data.
*/
handleStrategyUpdated(data) {
this.dataManager.updateStrategyData(data);
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
}
/**
* Hides the "Create New Strategy" form by adding a 'hidden' class.
* Handles the deletion of a strategy.
* @param {Object} data - The data for the deleted strategy.
*/
close_form() {
if (this.formElement) {
this.formElement.classList.add('hidden');
}
handleStrategyDeleted(data) {
const strategyName = data.strategy_name; // Extract the strategy name
// Remove the strategy using the provided strategy_name
this.dataManager.removeStrategy(strategyName);
// Update the UI to reflect the deletion
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
}
/**
* Handles batch updates for strategies, such as multiple configuration or performance updates.
* @param {Object} data - The data containing batch updates for strategies.
*/
handleUpdates(data) {
this.dataManager.applyBatchUpdates(data);
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
}
/**
* Creates the Blockly workspace using StratWorkspaceManager.
* @async
*/
async createWorkspace() {
await this.workspaceManager.initializeWorkspace();
}
/**
* Resizes the Blockly workspace using StratWorkspaceManager.
*/
resizeWorkspace() {
this.workspaceManager.adjustWorkspace();
}
/**
* Generates the strategy data including Python code, JSON representation, and workspace XML.
* @returns {string} - A JSON string containing the strategy data.
*/
generateStrategyJson() {
return this.workspaceManager.compileStrategyJson();
}
/**
* Restores the Blockly workspace from an XML string using StratWorkspaceManager.
* @param {string} workspaceXmlText - The XML text representing the workspace.
*/
restoreWorkspaceFromXml(workspaceXmlText) {
this.workspaceManager.loadWorkspaceFromXml(workspaceXmlText);
}
/**
@ -384,19 +633,9 @@ class Strategies {
const nameBox = document.getElementById('name_box');
const publicCheckbox = document.getElementById('public_checkbox');
if (!feeBox) {
console.error("fee_box element not found.");
alert("An error occurred: fee input element is missing.");
return;
}
if (!nameBox) {
console.error("name_box element not found.");
alert("An error occurred: name input element is missing.");
return;
}
if (!publicCheckbox) {
console.error("public_checkbox element not found.");
alert("An error occurred: public checkbox element is missing.");
if (!feeBox || !nameBox || !publicCheckbox) {
console.error("One or more form elements are missing.");
alert("An error occurred: Required form elements are missing.");
return;
}
@ -435,147 +674,32 @@ class Strategies {
// Determine if this is a new strategy or an edit
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
// Format the message and send it using the existing sendToApp function
if (this.comms) {
this.comms.sendToApp(messageType, strategyData);
this.close_form();
this.uiManager.hideForm();
} else {
console.error("Comms instance not available or invalid action type.");
}
}
/**
* Toggles the fee input field based on the state of the public checkbox.
* Disables the fee input if the public checkbox is unchecked.
*/
toggleFeeBox() {
const publicCheckbox = document.getElementById('public_checkbox');
const feeBox = document.getElementById('fee_box');
if (publicCheckbox && feeBox) {
feeBox.disabled = !publicCheckbox.checked;
}
}
/**
* Updates the HTML representation of the strategies.
*/
updateHtml() {
// Logic to update the UI with the current list of strategies
if (this.target_el) {
// Clear existing event listeners
while (this.target_el.firstChild) {
this.target_el.removeChild(this.target_el.firstChild);
}
// Create and append new elements for all strategies
for (let strat of this.strategies) {
const strategyItem = document.createElement('div');
strategyItem.className = 'strategy-item';
// Delete button
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.innerHTML = '&#10008;';
deleteButton.addEventListener('click', () => this.del(strat.name));
strategyItem.appendChild(deleteButton);
// Strategy icon
const strategyIcon = document.createElement('div');
strategyIcon.className = 'strategy-icon';
strategyIcon.addEventListener('click', () => this.openForm('edit', strat.name));
// Strategy name
const strategyName = document.createElement('div');
strategyName.className = 'strategy-name';
strategyName.textContent = strat.name;
strategyIcon.appendChild(strategyName);
strategyItem.appendChild(strategyIcon);
// Strategy hover details
const strategyHover = document.createElement('div');
strategyHover.className = 'strategy-hover';
strategyHover.innerHTML = `<strong>${strat.name}</strong><br>Stats: ${JSON.stringify(strat.stats, null, 2)}`;
strategyItem.appendChild(strategyHover);
// Append to target element
this.target_el.appendChild(strategyItem);
}
}
}
/**
* Opens the form for creating or editing a strategy.
* @param {string} action - The action to perform ('new' or 'edit').
* @param {string|null} strategyName - The name of the strategy to edit (only applicable for 'edit' action).
*/
openForm(action, strategyName = null) {
console.log(`Opening form for action: ${action}, strategy: ${strategyName}`);
if (this.formElement) {
const headerTitle = this.formElement.querySelector("#draggable_header h1");
const submitCreateBtn = this.formElement.querySelector("#submit-create");
const submitEditBtn = this.formElement.querySelector("#submit-edit");
const nameBox = this.formElement.querySelector('#name_box');
const publicCheckbox = this.formElement.querySelector('#public_checkbox');
const feeBox = this.formElement.querySelector('#fee_box');
if (!headerTitle || !submitCreateBtn || !submitEditBtn || !nameBox || !publicCheckbox || !feeBox) {
console.error('One or more form elements were not found.');
return;
}
if (action === 'new') {
headerTitle.textContent = "Create New Strategy";
submitCreateBtn.style.display = "inline-block";
submitEditBtn.style.display = "none";
nameBox.value = '';
publicCheckbox.checked = false;
feeBox.value = 0;
// Create a fresh workspace
this.createWorkspace();
requestAnimationFrame(() => this.resizeWorkspace());
} else if (action === 'edit' && strategyName) {
if (!this.workspace) {
this.createWorkspace();
}
const strategyData = this.strategies.find(s => s.name === strategyName);
if (strategyData) {
headerTitle.textContent = "Edit Strategy";
submitCreateBtn.style.display = "none";
submitEditBtn.style.display = "inline-block";
// Populate the form with strategy data
nameBox.value = strategyData.name;
publicCheckbox.checked = strategyData.public === 1;
feeBox.value = strategyData.fee || 0;
// Restore workspace from saved XML
this._restoreWorkspaceFromXml(strategyData.workspace);
} else {
console.error(`Strategy "${strategyName}" not found.`);
}
}
// Display the form
this.formElement.style.display = "grid";
} else {
console.error(`Form element "${this.formElement.id}" not found.`);
}
}
/**
* Deletes a strategy by its name.
* @param {string} name - The name of the strategy to be deleted.
*/
del(name) {
deleteStrategy(name) {
console.log(`Deleting strategy: ${name}`);
// Prepare data for server request
const deleteData = {
user_name: this.data.user_name, // Include the user_name
strategy_name: name // Strategy name to be deleted
user_name: this.data.user_name,
strategy_name: name
};
// Send delete request to the server
this.comms.sendToApp('delete_strategy', deleteData);
if (this.comms) {
this.comms.sendToApp('delete_strategy', deleteData);
} else {
console.error("Comms instance not available.");
}
}
}

View File

@ -20,6 +20,11 @@ class Indicator_Output {
}
set_legend_text(priceValue, name) {
// Ensure the legend for the name exists
if (!this.legend[name]) {
console.warn(`Legend for ${name} not found, skipping legend update.`);
return;
}
// Callback assigned to fire on crosshair movements.
let val = 'n/a';
if (priceValue !== undefined) {
@ -564,15 +569,20 @@ class Indicators {
Receives indicator data, creates and stores the indicator
objects, then inserts the data into the charts.
*/
this.create_indicators(idata.indicators, charts);
// Initialize each indicator with the data directly
if (idata.indicator_data) {
this.init_indicators(idata.indicator_data);
if (idata.indicators && Object.keys(idata.indicators).length > 0) {
this.create_indicators(idata.indicators, charts);
// Initialize each indicator with the data directly
if (idata.indicator_data && Object.keys(idata.indicator_data).length > 0) {
this.init_indicators(idata.indicator_data);
} else {
console.warn('Indicator data is empty. No indicators to initialize.');
}
} else {
console.error('Indicator data is not available.');
console.log('No indicators defined for this user.');
}
}
init_indicators(data){
// Loop through all the indicators.
for (name in data){
@ -583,6 +593,11 @@ class Indicators {
}
this.i_objs[name].init(data[name]);
}
// Set up the visibility form event handler
const form = document.getElementById('indicator-form');
// Add a submit event listener to the form
form.addEventListener('submit', this.handleFormSubmit.bind(this));
}
// This updates all the indicator data
@ -627,49 +642,72 @@ class Indicators {
}
// This updates a specific indicator
updateIndicator(event) {
const row = event.target.closest('.indicator-row');
const inputs = row.querySelectorAll('input, select');
updateIndicator(event) {
event.preventDefault(); // Prevent default form submission behavior
// Gather the indicator name from the row
const nameDiv = row.querySelector('div:nth-child(2)'); // Second <div> contains the name
const indicatorName = nameDiv.innerText.trim(); // Get the indicator name
const row = event.target.closest('.indicator-row');
const inputs = row.querySelectorAll('input, select');
// Initialize formObj with the name of the indicator
const formObj = {
name: indicatorName,
visible: false, // Default value for visible (will be updated based on the checkbox input)
source: {}, // Initialize the source object directly at the top level
properties: {} // Initialize the properties object
};
// Gather the indicator name from the row
const nameDiv = row.querySelector('div:nth-child(2)'); // Second <div> contains the name
const indicatorName = nameDiv.innerText.trim(); // Get the indicator name
// Iterate over each input (text, checkbox, select) and add its name and value to formObj
inputs.forEach(input => {
if (input.name === 'visible') {
// Handle the visible checkbox separately
formObj.visible = input.checked;
} else if (input.name === 'market' || input.name === 'timeframe' || input.name === 'exchange') {
// Directly map inputs to source object fields
formObj.source[input.name] = input.value;
} else {
// Add all other inputs (type, period, color) to the properties object
formObj.properties[input.name] = input.type === 'checkbox' ? input.checked : input.value;
}
});
// Initialize formObj with the name of the indicator
const formObj = {
name: indicatorName,
visible: false, // Default value for visible (will be updated based on the checkbox input)
source: {}, // Initialize the source object directly at the top level
properties: {} // Initialize the properties object
};
// Define an exclusion list for properties that should not be parsed as numbers
const exclusionFields = ['custom_field_name']; // Add field names that should NOT be parsed as numbers
// Function to check if a value contains mixed data (e.g., 'abc123')
const isMixedData = (value) => /\D/.test(value) && /\d/.test(value);
// Iterate over each input (text, checkbox, select) and add its name and value to formObj
inputs.forEach(input => {
let value = input.value;
// Handle the 'visible' checkbox separately
if (input.name === 'visible') {
formObj.visible = input.checked;
} else if (['market', 'timeframe', 'exchange'].includes(input.name)) {
// Directly map inputs to source object fields
formObj.source[input.name] = input.value;
} else {
// Check if the value should be parsed as a number
if (!exclusionFields.includes(input.name) && !isMixedData(value)) {
const parsedValue = parseFloat(value);
value = isNaN(parsedValue) ? value : parsedValue;
} else if (input.type === 'checkbox') {
value = input.checked;
}
// Add the processed value to the properties object
formObj.properties[input.name] = value;
}
});
// Call comms to send data to the server
this.comms.updateIndicator(formObj).then(response => {
if (response.success) {
window.location.reload(); // This triggers a full page refresh
} else {
alert('Failed to update the indicator.');
}
}).catch(error => {
console.error('Error updating indicator:', error);
alert('An unexpected error occurred while updating the indicator.');
});
}
// Call comms to send data to the server
this.comms.updateIndicator(formObj).then(response => {
if (response.success) {
window.location.reload(); // This triggers a full page refresh
} else {
alert('Failed to update the indicator.');
}
});
}
add_to_list(){
// Adds user input to a list and displays it in a HTML element.
// called from html button click add property
// Collect the property name and value input by the user
let n = document.getElementById("new_prop_name").value.trim();
let v = document.getElementById("new_prop_value").value.trim();
@ -802,5 +840,43 @@ updateIndicator(event) {
});
}
// Method to handle form submission
handleFormSubmit(event) {
event.preventDefault(); // Prevent default form submission behavior
// Get the form element
const form = event.target;
// Get all the checked checkboxes (indicators)
const checkboxes = form.querySelectorAll('input[name="indicator"]:checked');
let selectedIndicators = [];
checkboxes.forEach(function (checkbox) {
selectedIndicators.push(checkbox.value);
});
// Prepare the form data
const formData = new FormData(form);
formData.delete('indicator'); // Remove the single value (from original HTML behavior)
// Append all selected indicators as a single array-like structure
formData.append('indicator', JSON.stringify(selectedIndicators));
// Send form data via AJAX (fetch)
fetch(form.action, {
method: form.method,
body: formData
}).then(response => {
if (response.ok) {
// Handle success (you can reload the page or update the UI)
window.location.reload();
} else {
alert('Failed to update indicators.');
}
}).catch(error => {
console.error('Error:', error);
alert('An error occurred while updating the indicators.');
});
}
}

View File

@ -1,10 +1,11 @@
<div id='indicators' onmouseleave="document.getElementById('indicators').style.display = 'none' ">
<form action="/settings" method="post">
<form id="indicator-form" action="/settings" method="post">
<input type="hidden" name="setting" value="toggle_indicator"/>
{% for indicator in indicator_list %}
<input type="checkbox" id="{{ indicator }}" name="indicator" value="{{ indicator }}"{% if indicator in checked %} checked{%endif%}>
<label for = "{{ indicator }}">{{ indicator }}</label><br>
<label for="{{ indicator }}">{{ indicator }}</label><br>
{% endfor %}
<input type="submit" value="Submit Changes">
</form>
</div>

View File

@ -21,7 +21,7 @@
<!-- Public checkbox -->
<div style="grid-column: 1;">
<label for="public_checkbox" style="display: inline-block; width: 20%;">Public:</label>
<input type="checkbox" id="public_checkbox" name="public_checkbox" style="display: inline-block;" onclick="UI.strats.toggleFeeBox()">
<input type="checkbox" id="public_checkbox" name="public_checkbox" style="display: inline-block;" onclick="UI.strats.uiManager.toggleFeeInput()">
</div>
<!-- Fee input field -->
@ -32,7 +32,7 @@
<!-- Buttons -->
<div style="grid-column: 1; text-align: center;">
<button type="button" class="btn cancel" onclick="UI.strats.close_form()">Close</button>
<button type="button" class="btn cancel" onclick="UI.strats.uiManager.hideForm()">Close</button>
<!-- Create Button -->
<button id="submit-create" type="button" class="btn next" onclick="UI.strats.submitStrategy('new')">Create Strategy</button>
<!-- Edit Button -->

View File

@ -1,5 +1,5 @@
<div class="content" id="strats_content">
<button class="btn" id="new_strats_btn" onclick="UI.strats.openForm('new')">New Strategy</button>
<button class="btn" id="new_strats_btn" onclick="UI.strats.uiManager.displayForm('new')">New Strategy</button>
<hr>
<h3>Strategies</h3>
<div class="strategies-container" id="strats_display"></div>