From adedaaa5400f6cb4830cbc3beb286742c6ab8783 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 24 Sep 2024 23:43:13 -0300 Subject: [PATCH] Getting closer to a beta. woot woot. there is a problem with created strategies only displaying two of the ones in the database --- src/BrighterTrades.py | 52 +-- src/Strategies.py | 451 ++++++++++++++------------ src/static/Strategies.js | 204 ++++++++---- src/static/communication.js | 26 +- src/static/signals.js | 7 +- src/static/trade.js | 13 +- src/templates/new_strategy_popup.html | 58 ++-- src/templates/strategies_hud.html | 106 +++++- 8 files changed, 586 insertions(+), 331 deletions(-) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 5afcbc8..ee39ed7 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -98,7 +98,7 @@ class BrighterTrades: Returns specified user info. :param user_name: The user_name. - :param info: The information being requested. + :param info: The information being requested.('Chart View','Is logged in?', 'User_id') :return: The requested info or None. :raises ValueError: If the provided info is invalid. """ @@ -343,29 +343,33 @@ class BrighterTrades: "fee": data.get('fee', None) # Default to None if not specified } - # Save the new strategy (in both cache and database) - self.strategies.new_strategy(strategy_data) + # Save the new strategy (in both cache and database) and return the result. + return self.strategies.new_strategy(strategy_data) - return {"success": True, "message": "Strategy created successfully", "data": strategy_data} - - def delete_strategy(self, strategy_name: str) -> None: + def delete_strategy(self, data: dict) -> str | dict: """ Deletes the specified strategy from the strategies instance and the configuration file. - :param strategy_name: The name of the strategy to delete. :return: None :raises ValueError: If the strategy does not exist or there are issues with removing it from the configuration file. """ - # if not self.strategies.has_strategy(strategy_name): - # raise ValueError(f"The strategy '{strategy_name}' does not exist.") + # Extract user_name from the data and get user_id + user_name = data.get('user_name') + if not user_name: + return {"success": False, "message": "User not specified"} - self.strategies.delete_strategy(strategy_name) - try: - # self.config.remove('strategies', strategy_name)TODO - pass - except Exception as e: - raise ValueError(f"Failed to remove the strategy '{strategy_name}' from the configuration file: {str(e)}") + # Fetch the user_id using the user_name + user_id = self.get_user_info(user_name=user_name, info='User_id') + if not user_id: + return {"success": False, "message": "User ID not found"} + + strategy_name = data.get('strategy_name') + if not strategy_name: + 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"} def delete_signal(self, signal_name: str) -> None: """ @@ -389,13 +393,13 @@ class BrighterTrades: """ return self.signals.get_signals('json') - def get_strategies_json(self) -> str: + def get_strategies_json(self, user_id) -> str: """ Retrieve all the strategies from the strategies instance and return them as a JSON object. :return: str - A JSON object containing all the strategies. """ - return self.strategies.get_strategies('json') + return self.strategies.get_all_strategies(user_id, 'json') def connect_or_config_exchange(self, user_name: str, exchange_name: str, api_keys: dict = None) -> dict: """ @@ -587,7 +591,9 @@ class BrighterTrades: :param msg_type: The type of the incoming message. :param msg_data: The data associated with the incoming message. - :return: dict|None - A dictionary containing the response message and data, or None if no response is needed. + :return: dict|None - A dictionary containing the response message and data, or None if no response is needed or + no data is found to ensure the WebSocket channel isn't burdened with unnecessary + communication. """ def standard_reply(reply_msg: str, reply_data: Any) -> dict: @@ -599,15 +605,17 @@ class BrighterTrades: return standard_reply("updates", r_data) if msg_type == 'request': - if msg_data == 'signals': + request_for = msg_data.get('request') + if request_for == 'signals': if signals := self.get_signals_json(): return standard_reply("signals", signals) - elif msg_data == 'strategies': - if strategies := self.get_strategies_json(): + elif request_for == 'strategies': + user_id = self.get_user_info(msg_data['user_name'], 'User_id') + if strategies := self.get_strategies_json(user_id): return standard_reply("strategies", strategies) - elif msg_data == 'trades': + elif request_for == 'trades': if trades := self.get_trades(): return standard_reply("trades", trades) else: diff --git a/src/Strategies.py b/src/Strategies.py index 81fe88f..2b72af3 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -1,171 +1,174 @@ import json + +import pandas as pd + from DataCache_v3 import DataCache import datetime as dt -class Strategy: - def __init__(self, **args): - """ - :param args: An object containing key_value pairs representing strategy attributes. - Strategy format is defined in strategies.js - """ - self.active = None - self.type = None - self.trade_amount = None - self.max_position = None - self.side = None - self.trd_in_conds = None - self.merged_loss = None - self.gross_loss = None - self.stop_loss = None - self.take_profit = None - self.gross_profit = None - self.merged_profit = None - self.name = None - self.current_value = None - self.opening_value = None - self.gross_pl = None - self.net_pl = None - self.combined_position = None - - # A strategy is defined in Strategies.js it is received from the client, - # then unpacked and converted into a python object here. - for name, value in args.items(): - # Make each keyword-argument a specific_property of the class. - setattr(self, name, value) - - # A container to hold previous state of signals. - self.last_states = {} - - # A list of all the trades made by this strategy. - self.trades = [] - - def get_position(self): - return self.combined_position - - def get_pl(self): - self.update_pl() - return self.net_pl - - def update_pl(self): - # sum the pl of all the trades. - position_sum = 0 - pl_sum = 0 - opening_value_sum = 0 - value_sum = 0 - for trade in self.trades: - pl_sum += trade.profit_loss - position_sum += trade.position_size - value_sum += trade.value - opening_value_sum += trade.opening_value - self.combined_position = position_sum - self.gross_pl = pl_sum - self.opening_value = opening_value_sum - self.current_value = value_sum - - def to_json(self): - return json.dumps(self, default=lambda o: o.__dict__, - sort_keys=True, indent=4) - - def evaluate_strategy(self, signals): - """ - :param signals: Signals: A reference to an object that handles current signal states. - :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int} - """ - - def condition_satisfied(sig_name, value): - """ - Check if a signal has a state of value. - :param sig_name: str: The name of a signal object to compare states. - :param value: The state value to compare. - :return bool: True: == . - """ - signal = signals.get_signal_by_name(sig_name) - # Evaluate for a state change - if value == 'changed': - # Store the state if it hasn't been stored yet. - if sig_name not in self.last_states: - self.last_states.update({sig_name: signal.state}) - # Store the new state and return true if the state has changed. - if self.last_states[sig_name] != signal.state: - self.last_states.update({sig_name: signal.state}) - return True - else: - # Else return true if the values match. - return value == json.dumps(signal.state) - - def all_conditions_met(conditions): - # Loops through a lists of signal names and states. - # Returns True if all combinations are true. - if len(conditions) < 1: - print(f"no trade-in conditions supplied: {self.name}") - return False - # Evaluate all conditions and return false if any are un-met. - for trigger_signal in conditions.keys(): - trigger_value = conditions[trigger_signal] - # Compare this signal's state with the trigger_value - print(f'evaluating :({trigger_signal, trigger_value})') - if not condition_satisfied(trigger_signal, trigger_value): - print('returning false') - return False - print('all conditions met!!!') - return True - - def trade_out_condition_met(condition_type): - # Retrieve the condition from either the 'stop_loss' or 'take_profit' obj. - condition = getattr(self, condition_type) - # Subtypes of conditions are 'conditional' or 'value'. - if condition.typ == 'conditional': - signal_name = condition.trig - signal_value = condition.val - return condition_satisfied(signal_name, signal_value) - else: - if condition_type == 'take_profit': - if self.merged_profit: - # If the profit condition is met send command to take profit. - return self.gross_profit > self.take_profit.val - else: - # Loop through each associated trade and test - for trade in self.trades: - return trade.profit_loss > self.take_profit.val - elif condition_type == 'value': - if self.merged_loss: - # If the loss condition is met, return a trade-out command. - return self.gross_loss < self.stop_loss.val - else: - # Loop through each associated trade and test - for trade in self.trades: - return trade.profit_loss < self.stop_loss.val - else: - raise ValueError('trade_out_condition_met: invalid condition_type') - - trade_in_cmd = self.side - if self.side == 'buy': - trade_out_cmd = 'sell' - else: - trade_out_cmd = 'buy' - if self.type == 'in-out': - print('evaluating trade in conditions for in / out') - # If trade-in conditions are met. - if all_conditions_met(self.trd_in_conds): - # If the new trade wouldn't exceed max_position. Return a trade-in command. - proposed_position_size = int(self.combined_position) + int(self.trade_amount) - if proposed_position_size < int(self.max_position): - return 'open_position', trade_in_cmd - - # If strategy is active test the take-profit or stop-loss conditions. - if self.active: - # Conditional take-profit trades-out if a signals equals a set value. - if trade_out_condition_met('take_profit'): - return 'take_profit', trade_out_cmd - - # Conditional stop-loss trades-outs if a signals value equals a set value. - if trade_out_condition_met('stop_loss'): - return 'stop_loss', trade_out_cmd - - # No conditions were met. - print('Strategies were updated and nothing to do.') - return 'do_nothing', 'nothing' +# class Strategy: +# def __init__(self, **args): +# """ +# :param args: An object containing key_value pairs representing strategy attributes. +# Strategy format is defined in strategies.js +# """ +# self.active = None +# self.type = None +# self.trade_amount = None +# self.max_position = None +# self.side = None +# self.trd_in_conds = None +# self.merged_loss = None +# self.gross_loss = None +# self.stop_loss = None +# self.take_profit = None +# self.gross_profit = None +# self.merged_profit = None +# self.name = None +# self.current_value = None +# self.opening_value = None +# self.gross_pl = None +# self.net_pl = None +# self.combined_position = None +# +# # A strategy is defined in Strategies.js it is received from the client, +# # then unpacked and converted into a python object here. +# for name, value in args.items(): +# # Make each keyword-argument a specific_property of the class. +# setattr(self, name, value) +# +# # A container to hold previous state of signals. +# self.last_states = {} +# +# # A list of all the trades made by this strategy. +# self.trades = [] +# +# def get_position(self): +# return self.combined_position +# +# def get_pl(self): +# self.update_pl() +# return self.net_pl +# +# def update_pl(self): +# # sum the pl of all the trades. +# position_sum = 0 +# pl_sum = 0 +# opening_value_sum = 0 +# value_sum = 0 +# for trade in self.trades: +# pl_sum += trade.profit_loss +# position_sum += trade.position_size +# value_sum += trade.value +# opening_value_sum += trade.opening_value +# self.combined_position = position_sum +# self.gross_pl = pl_sum +# self.opening_value = opening_value_sum +# self.current_value = value_sum +# +# def to_json(self): +# return json.dumps(self, default=lambda o: o.__dict__, +# sort_keys=True, indent=4) +# +# def evaluate_strategy(self, signals): +# """ +# :param signals: Signals: A reference to an object that handles current signal states. +# :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int} +# """ +# +# def condition_satisfied(sig_name, value): +# """ +# Check if a signal has a state of value. +# :param sig_name: str: The name of a signal object to compare states. +# :param value: The state value to compare. +# :return bool: True: == . +# """ +# signal = signals.get_signal_by_name(sig_name) +# # Evaluate for a state change +# if value == 'changed': +# # Store the state if it hasn't been stored yet. +# if sig_name not in self.last_states: +# self.last_states.update({sig_name: signal.state}) +# # Store the new state and return true if the state has changed. +# if self.last_states[sig_name] != signal.state: +# self.last_states.update({sig_name: signal.state}) +# return True +# else: +# # Else return true if the values match. +# return value == json.dumps(signal.state) +# +# def all_conditions_met(conditions): +# # Loops through a lists of signal names and states. +# # Returns True if all combinations are true. +# if len(conditions) < 1: +# print(f"no trade-in conditions supplied: {self.name}") +# return False +# # Evaluate all conditions and return false if any are un-met. +# for trigger_signal in conditions.keys(): +# trigger_value = conditions[trigger_signal] +# # Compare this signal's state with the trigger_value +# print(f'evaluating :({trigger_signal, trigger_value})') +# if not condition_satisfied(trigger_signal, trigger_value): +# print('returning false') +# return False +# print('all conditions met!!!') +# return True +# +# def trade_out_condition_met(condition_type): +# # Retrieve the condition from either the 'stop_loss' or 'take_profit' obj. +# condition = getattr(self, condition_type) +# # Subtypes of conditions are 'conditional' or 'value'. +# if condition.typ == 'conditional': +# signal_name = condition.trig +# signal_value = condition.val +# return condition_satisfied(signal_name, signal_value) +# else: +# if condition_type == 'take_profit': +# if self.merged_profit: +# # If the profit condition is met send command to take profit. +# return self.gross_profit > self.take_profit.val +# else: +# # Loop through each associated trade and test +# for trade in self.trades: +# return trade.profit_loss > self.take_profit.val +# elif condition_type == 'value': +# if self.merged_loss: +# # If the loss condition is met, return a trade-out command. +# return self.gross_loss < self.stop_loss.val +# else: +# # Loop through each associated trade and test +# for trade in self.trades: +# return trade.profit_loss < self.stop_loss.val +# else: +# raise ValueError('trade_out_condition_met: invalid condition_type') +# +# trade_in_cmd = self.side +# if self.side == 'buy': +# trade_out_cmd = 'sell' +# else: +# trade_out_cmd = 'buy' +# if self.type == 'in-out': +# print('evaluating trade in conditions for in / out') +# # If trade-in conditions are met. +# if all_conditions_met(self.trd_in_conds): +# # If the new trade wouldn't exceed max_position. Return a trade-in command. +# proposed_position_size = int(self.combined_position) + int(self.trade_amount) +# if proposed_position_size < int(self.max_position): +# return 'open_position', trade_in_cmd +# +# # If strategy is active test the take-profit or stop-loss conditions. +# if self.active: +# # Conditional take-profit trades-out if a signals equals a set value. +# if trade_out_condition_met('take_profit'): +# return 'take_profit', trade_out_cmd +# +# # Conditional stop-loss trades-outs if a signals value equals a set value. +# if trade_out_condition_met('stop_loss'): +# return 'stop_loss', trade_out_cmd +# +# # No conditions were met. +# print('Strategies were updated and nothing to do.') +# return 'do_nothing', 'nothing' class Strategies: @@ -178,7 +181,7 @@ class Strategies: """ self.data = data # Database interaction instance self.trades = trades - self.strat_list = [] # List to hold strategy objects + # self.strat_list = [] # List to hold strategy objects # Create a cache for strategies with necessary columns self.data.create_cache(name='strategies', @@ -188,76 +191,124 @@ class Strategies: default_expiration=dt.timedelta(hours=24), columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee"]) - def new_strategy(self, data: dict): + def new_strategy(self, data: dict) -> dict: """ Add a new strategy to the cache and database. :param data: A dictionary containing strategy data such as name, code, and workspace. + :return: A dictionary containing success or failure information. """ - # Create a new Strategy object and add it to the list - self.strat_list.append(Strategy(**data)) + try: + # # Create a new Strategy object and add it to the list + # self.strat_list.append(Strategy(**data)) - # Insert the strategy into the database and cache - self.data.insert_row_into_datacache( - cache_name='strategies', - columns=("creator", "name", "workspace", "code", "stats", "public", "fee"), - values=(data.get('creator'), data['name'], data['workspace'], data['code'], data.get('stats', {}), - data.get('public', False), data.get('fee', 0)) - ) + # Serialize complex data fields like workspace and stats + workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ + 'workspace'] + stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string - def delete_strategy(self, name: str): + # Insert the strategy into the database and cache + self.data.insert_row_into_datacache( + cache_name='strategies', + columns=("creator", "name", "workspace", "code", "stats", "public", "fee"), + values=( + data.get('creator'), + data['name'], + workspace_serialized, # Serialized workspace + data['code'], + stats_serialized, # Serialized stats + data.get('public', False), + data.get('fee', 0) + ) + ) + + # If everything is successful, return a success message + return {"success": True, "message": "Strategy created and saved successfully"} + + except Exception as e: + # Catch any exceptions and return a failure message + return {"success": False, "message": f"Failed to create strategy: {str(e)}"} + + def delete_strategy(self, user_id, name: str): """ Delete a strategy from the cache and database by name. + :param user_id: The id of the user making the request. :param name: The name of the strategy to delete. """ - obj = self.get_strategy_by_name(name) - if obj: - self.strat_list.remove(obj) - # Remove the strategy from cache and database - self.data.remove_row_from_datacache( - cache_name='strategies', - filter_vals=[('name', name)] - ) + # Remove the strategy from cache and database + self.data.remove_row_from_datacache( + cache_name='strategies', + filter_vals=[('creator', user_id), ('name', name)] + ) - def get_all_strategy_names(self) -> list | None: + def get_all_strategy_names(self, user_id) -> list | None: """ Return a list of all strategy names stored in the cache or database. """ # Fetch all strategy names from the cache or database - strategies_df = self.data.get_rows_from_datacache(cache_name='strategies', filter_vals=[]) + strategies_df = self.get_all_strategies(user_id, 'df') if not strategies_df.empty: return strategies_df['name'].tolist() return None - def get_strategies(self, form: str): + def get_all_strategies(self, user_id: int, form: str): """ Return strategies stored in this instance in various formats. + :param user_id: the id of the user making the request. :param form: The desired format ('obj', 'json', or 'dict'). :return: A list of strategies in the requested format. """ - if form == 'obj': - return self.strat_list + # Fetch all public strategies and user's strategies from the cache or database + public_df = self.data.get_rows_from_datacache(cache_name='strategies', filter_vals=[('public', 1)]) + user_df = self.data.get_rows_from_datacache(cache_name='strategies', filter_vals=[('creator', user_id), + ('public', 0)]) + + # Concatenate the two DataFrames (rows from public and user-created strategies) + strategies_df = pd.concat([public_df, user_df], ignore_index=True) + + # Return None if no strategies found + if strategies_df.empty: + return None + + # Return the strategies in the requested format + if form == 'df': + return strategies_df elif form == 'json': - return [strat.to_json() for strat in self.strat_list] + return strategies_df.to_json(orient='records') elif form == 'dict': - return [strat.__dict__ for strat in self.strat_list] + return strategies_df.to_dict('records') + return None - def get_strategy_by_name(self, name: str): + def get_strategy_by_name(self, user_id, name: str): """ Retrieve a strategy object by name. + :param user_id: The ID of the user making the request. :param name: The name of the strategy to retrieve. - :return: The strategy object if found, otherwise False. + :return: The strategy DataFrame row if found, otherwise None. """ - for obj in self.strat_list: - if obj.name == name: - return obj - return False + # Fetch all strategies (public and user-specific) as a DataFrame + strategies_df = self.get_all_strategies(user_id, 'df') + + # Ensure that strategies_df is not None + if strategies_df is None: + return None + + # Filter the DataFrame to find the strategy by name (exact match) + name = name + filtered_strategies = strategies_df.query('name == @name') + + # Return None if no matching strategy is found + if filtered_strategies.empty: + return None + + # Return the filtered strategy row (or a dictionary if needed) + return filtered_strategies.iloc[0].to_dict() def execute_cmd(self, strategy, action, cmd): order_type = 'LIMIT' @@ -316,10 +367,10 @@ class Strategies: # Data object returned to function caller. return_obj = {} # Loop through all the published strategies. - for strategy in self.strat_list: - actions = process_strategy(strategy) - stat_updates = get_stats(strategy) - return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates} + # for strategy in self.strat_list: + # actions = process_strategy(strategy) + # stat_updates = get_stats(strategy) + # return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates} if len(return_obj) == 0: return False else: diff --git a/src/static/Strategies.js b/src/static/Strategies.js index 2eeaccf..c7caab7 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -11,6 +11,11 @@ class Strategies { } // Create the Blockly workspace and define custom blocks createWorkspace() { + if (!document.getElementById('blocklyDiv')) { + console.error("blocklyDiv is not loaded."); + return; + } + // Dispose of the existing workspace before creating a new one if (this.workspace) { this.workspace.dispose(); @@ -30,68 +35,69 @@ class Strategies { // Define Python generators after workspace initialization this.definePythonGenerators(); } + + // Resize the Blockly workspace resizeWorkspace() { const blocklyDiv = document.getElementById('blocklyDiv'); - if (this.workspace) { - // Adjust workspace dimensions to match the blocklyDiv + if (blocklyDiv && this.workspace) { Blockly.svgResize(this.workspace); + } else { + console.error("Cannot resize workspace: Blockly or blocklyDiv is not loaded."); } } + // Generate Python code from the Blockly workspace and return as JSON generateStrategyJson() { - // Initialize Python generator with the current workspace + if (!this.workspace) { + console.error("Workspace is not available."); + return; + } + Blockly.Python.init(this.workspace); - - // Generate Python code from the Blockly workspace const pythonCode = Blockly.Python.workspaceToCode(this.workspace); - - // Serialize the workspace to XML format const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); - const json = { + return JSON.stringify({ name: document.getElementById('name_box').value, - code: pythonCode, // This is the generated Python code - workspace: workspaceXmlText // Serialized workspace XML - }; - - return JSON.stringify(json); + code: pythonCode, + workspace: workspaceXmlText + }); } + // Restore the Blockly workspace from XML restoreWorkspaceFromXml(workspaceXmlText) { - // Convert the text back into an XML DOM object - const workspaceXml = Blockly.Xml.textToDom(workspaceXmlText); + try { + if (!this.workspace) { + console.error("Cannot restore workspace: Blockly workspace is not initialized."); + return; + } - // Clear the current workspace before loading a new one - if (this.workspace) { - this.workspace.clear(); + const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText); + + this.workspace.clear(); // Clear the current workspace + Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); // Load the new XML into the workspace + } catch (error) { + console.error('Error restoring workspace from XML:', error); } - - // Load the workspace from the XML DOM object - Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); } + + // Fetch saved strategies fetchSavedStrategies() { - fetch('/get_strategies', { - method: 'GET' - }) - .then(response => response.json()) - .then(data => { - this.strategies = data.strategies; - this.update_html(); - }) - .catch(error => console.error('Error fetching strategies:', error)); + if (window.UI.data.comms) { + window.UI.data.comms.sendToApp('request', { request: 'strategies', user_name: window.UI.data.user_name }); + } else { + console.error('Comms instance not available.'); + } } - // Show the "Create New Strategy" form (open the workspace) - open_form() { - const formElement = document.getElementById("new_strat_form"); - - // Check if the form element exists - if (formElement) { - formElement.style.display = "grid"; // Open the form - } else { - console.error('Form element "new_strat_form" not found.'); + // Set data received from server + set_data(data) { + if (typeof data === 'string') { + data = JSON.parse(data); } + this.strategies = data; + this.update_html(); // Refresh the strategies display } // Hide the "Create New Strategy" form @@ -105,71 +111,137 @@ class Strategies { } } - submit() { - // Generate the Python code as JSON + // Submit or edit strategy + submitStrategy(action) { const strategyJson = this.generateStrategyJson(); - // Get the fee and public checkbox values const fee = parseFloat(document.getElementById('fee_box').value) || 0; if (fee < 0) { alert("Fee cannot be negative"); return; } + const strategyName = document.getElementById('name_box').value.trim(); + if (!strategyName) { + alert("Please provide a name for the strategy."); + return; + } + const is_public = document.getElementById('public_checkbox').checked ? 1 : 0; - // Merge additional fields into the strategy data + // Prepare the strategy data const strategyData = { - ...JSON.parse(strategyJson), // Spread the existing strategy JSON data - fee, // Add the fee - public: is_public // Add the public status + user_name: window.UI.data.user_name, // Include user_name + ...JSON.parse(strategyJson), + fee, + public: is_public }; - // Send the strategy to the server via WebSocket through the Comms class - if (window.UI.data.comms) { // Assuming the Comms instance is accessible via window.UI.data.comms - window.UI.data.comms.sendToApp('new_strategy', strategyData); + // 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 (window.UI.data.comms && messageType) { + // Adjust here to pass the messageType and data separately + window.UI.data.comms.sendToApp(messageType, strategyData); + this.close_form(); } else { - console.error("Comms instance not available."); + console.error("Comms instance not available or invalid action type."); } } + // Toggle fee input based on public checkbox toggleFeeBox() { const publicCheckbox = document.getElementById('public_checkbox'); const feeBox = document.getElementById('fee_box'); - feeBox.disabled = !publicCheckbox.checked; // Enable feeBox only if publicCheckbox is checked + feeBox.disabled = !publicCheckbox.checked; } - // Update the UI with saved strategies + + // Update strategies UI update_html() { let stratsHtml = ''; - const onClick = "window.UI.strats.del(this.value);"; for (let strat of this.strategies) { - let deleteButton = ``; - stratsHtml += `
  • ${deleteButton}
    ${JSON.stringify(strat, null, 2)}
  • `; + stratsHtml += ` +
    + +
    +
    ${strat.name}
    +
    +
    + ${strat.name} +
    Stats: ${JSON.stringify(strat.stats, null, 2)} +
    +
    `; } document.getElementById(this.target_id).innerHTML = stratsHtml; } + // Open form for creating or editing a strategy + openForm(action, strategyName = null) { + const formElement = document.getElementById("new_strat_form"); - // Open the form and create the Blockly workspace - open_stg_form() { - this.open_form(); - this.createWorkspace(); + if (formElement) { + if (action === 'new') { + document.querySelector("#draggable_header h1").textContent = "Create New Strategy"; + document.getElementById("submit-create").style.display = "inline-block"; + document.getElementById("submit-edit").style.display = "none"; + document.getElementById('name_box').value = ''; + document.getElementById('public_checkbox').checked = false; + document.getElementById('fee_box').value = 0; + + // Always create a fresh workspace for new strategy + this.createWorkspace(); + + // Ensure the workspace is resized after being displayed + setTimeout(() => this.resizeWorkspace(), 0); + + } else if (action === 'edit' && strategyName) { + // Ensure workspace is created if not already initialized + if (!this.workspace) { + this.createWorkspace(); + } + + const strategyData = this.strategies.find(s => s.name === strategyName); + if (strategyData) { + document.querySelector("#draggable_header h1").textContent = "Edit Strategy"; + document.getElementById("submit-create").style.display = "none"; + document.getElementById("submit-edit").style.display = "inline-block"; + + // Populate the form with the strategy data + document.getElementById('name_box').value = strategyData.name; + document.getElementById('public_checkbox').checked = strategyData.public === 1; + document.getElementById('fee_box').value = strategyData.fee || 0; + + // Restore the Blockly workspace from the saved XML + this.restoreWorkspaceFromXml(strategyData.workspace); + } + } + + formElement.style.display = "grid"; // Display the form + } else { + console.error('Form element "new_strat_form" not found.'); + } } - // Delete a strategy by its name del(name) { - window.UI.data.comms.sendToApp('delete_strategy', name); - // Remove the strategy from the UI - let child = document.getElementById(name + '_item'); - child.parentNode.removeChild(child); + const deleteData = { + user_name: window.UI.data.user_name, // Include the user_name + strategy_name: name // Strategy name to be deleted + }; + + // Corrected call to send message type and data separately + window.UI.data.comms.sendToApp('delete_strategy', deleteData); } - // Initialize the Strategies UI component + // Initialize strategies initialize() { this.target = document.getElementById(this.target_id); if (!this.target) { console.error('Target element', this.target_id, 'not found.'); + return; } + this.fetchSavedStrategies(); } + // Define Blockly blocks dynamically based on indicators defineIndicatorBlocks() { const indicatorOutputs = window.UI.indicators.getIndicatorOutputs(); @@ -521,8 +593,6 @@ class Strategies { // Trade Action block to Python code Blockly.Python['trade_action'] = Blockly.Python.forBlock['trade_action'] = function(block) { - alert('Generating Python for trade_action block'); // Debugging alert - var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); var tradeType = block.getFieldValue('TRADE_TYPE'); var stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC) || 'None'; diff --git a/src/static/communication.js b/src/static/communication.js index d7ec7e2..964fc23 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -207,9 +207,6 @@ class Comms { } - /** - * Sets up the WebSocket connection to the application server. - */ setAppCon() { this.appCon = new WebSocket('ws://localhost:5000/ws'); @@ -256,20 +253,42 @@ class Comms { if (trade_updts) { window.UI.trade.update_received(trade_updts); } + } else if (message.reply === 'signals') { window.UI.signals.set_data(message.data); + } else if (message.reply === 'strategies') { window.UI.strats.set_data(message.data); + } else if (message.reply === 'trades') { window.UI.trade.set_data(message.data); + } else if (message.reply === 'signal_created') { const list_of_one = [message.data]; window.UI.signals.set_data(list_of_one); + } else if (message.reply === 'trade_created') { const list_of_one = [message.data]; window.UI.trade.set_data(list_of_one); + } else if (message.reply === 'Exchange_connection_result') { window.UI.exchanges.postConnection(message.data); + + } else if (message.reply === 'strategy_created') { + // Handle the strategy creation response + if (message.data.success) { + // Success - Notify the user and update the UI + alert(message.data.message); // Display a success message + console.log("New strategy data:", message.data); // Log or handle the new strategy data + + // Optionally, refresh the list of strategies + window.UI.strats.fetchSavedStrategies(); + } else { + // Failure - Notify the user of the error + alert(`Error: ${message.data.message}`); + console.error("Strategy creation error:", message.data.message); + } + } else { console.log(message.reply); console.log(message.data); @@ -291,6 +310,7 @@ class Comms { } + /** * Sets up a WebSocket connection to the exchange for receiving candlestick data. * @param {string} interval - The interval of the candlestick data. diff --git a/src/static/signals.js b/src/static/signals.js index 000d7f8..da5d4e3 100644 --- a/src/static/signals.js +++ b/src/static/signals.js @@ -10,8 +10,13 @@ class Signals { request_signals(){ // Requests a list of all the signals from the server. - window.UI.data.comms.sendToApp('request', 'signals'); + if (window.UI.data.comms) { + window.UI.data.comms.sendToApp('request', { request: 'signals', user_name: window.UI.data.user_name }); + } else { + console.error('Comms instance not available.'); + } } + delete_signal(signal_name){ // Requests that the server remove a specific signal. window.UI.data.comms.sendToApp('delete_signal', signal_name); diff --git a/src/static/trade.js b/src/static/trade.js index 7f575bd..9478d40 100644 --- a/src/static/trade.js +++ b/src/static/trade.js @@ -82,13 +82,22 @@ class Trade { // Update the trade value display everytime the quantity input changes. this.qtyInput_el.addEventListener('change', update_tradeValue); // Send a request to the server for any loaded data. - window.UI.data.comms.sendToApp('request', 'trades'); + this.fetchTrades(); } // Call to display the 'Create new trade' dialog. open_tradeForm() { this.formDialog_el.style.display = "grid"; } - // Call to hide the 'Create new signal' dialog. + + // Call to hide the 'Create trade' dialog. close_tradeForm() { this.formDialog_el.style.display = "none"; } + fetchTrades(){ + if (window.UI.data.comms) { + window.UI.data.comms.sendToApp('request', { request: 'trades', user_name: window.UI.data.user_name }); + } else { + console.error('Comms instance not available.'); + } + } + // Call to display the 'Trade details' dialog. open_tradeDetails_Form() { document.getElementById("trade_details_form").style.display = "grid"; } // Call to hide the 'Create new signal' dialog. diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html index 23caafd..e3b0616 100644 --- a/src/templates/new_strategy_popup.html +++ b/src/templates/new_strategy_popup.html @@ -3,44 +3,44 @@
    -

    Create New Strategy

    +

    Create New Strategy

    +

    Edit Strategy

    -
    - -
    + + +
    - -
    + +
    + + +
    - -
    - - -
    + +
    + + +
    - -
    - - -
    + +
    + + +
    - -
    - - -
    - - -
    - - -
    + +
    + + + + +
    - +
    @@ -69,7 +69,6 @@ /* Style the form container to be scrollable and take up remaining space */ .form-container { - height: calc(100% - 60px); /* Takes up the remaining height minus header height */ overflow-y: auto; /* Makes it scrollable */ -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ @@ -78,7 +77,6 @@ .form-container::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ } - /* Label and input field styling */ .form_panels label { display: inline-block; diff --git a/src/templates/strategies_hud.html b/src/templates/strategies_hud.html index 4dc5283..46ec524 100644 --- a/src/templates/strategies_hud.html +++ b/src/templates/strategies_hud.html @@ -1,6 +1,100 @@ -
    - -
    -

    Strategies

    -
      -
      +
      + +
      +

      Strategies

      +
      +
      + +