diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 2b33103..5afcbc8 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -322,12 +322,31 @@ class BrighterTrades: :param data: A dictionary containing the attributes of the new strategy. :return: An error message if the required attribute is missing, or the incoming data for chaining on success. """ - if 'name' not in data: - return "The new strategy must have a 'name' attribute." + # 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.new_strategy(data) - self.config.set_setting('strategies', self.strategies.get_strategies('dict')) - return data + # 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"} + + # Prepare the strategy data for insertion + strategy_data = { + "creator": user_id, + "name": data['name'], + "workspace": data['workspace'], + "code": data['code'], + "stats": data.get('stats', {}), + "public": data.get('public', 0), # Default to private if not specified + "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) + + return {"success": True, "message": "Strategy created successfully", "data": strategy_data} def delete_strategy(self, strategy_name: str) -> None: """ diff --git a/src/Strategies.py b/src/Strategies.py index 850b519..81fe88f 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -1,5 +1,6 @@ import json -from DataCache_v2 import DataCache +from DataCache_v3 import DataCache +import datetime as dt class Strategy: @@ -169,59 +170,90 @@ class Strategy: class Strategies: def __init__(self, data: DataCache, trades): - # Handles database connections - self.data = data + """ + Initializes the Strategies class. - # Reference to the trades object that maintains all trading actions and data. + :param data: Instance of DataCache to manage cache and database interactions. + :param trades: Reference to the trades object that maintains trading actions and data. + """ + self.data = data # Database interaction instance self.trades = trades + self.strat_list = [] # List to hold strategy objects - self.strat_list = [] + # Create a cache for strategies with necessary columns + self.data.create_cache(name='strategies', + cache_type='table', + size_limit=100, + eviction_policy='deny', + default_expiration=dt.timedelta(hours=24), + columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee"]) - def get_all_strategy_names(self) -> list | None: - """Return a list of all strategies in the database""" - # # Load existing Strategies from file. - # loaded_strategies = self.data.get_setting('strategies') - # if loaded_strategies is None: - # # Populate the list and file with defaults defined in this class. - # loaded_strategies = self.get_strategy_defaults() - # config.set_setting('strategies', loaded_strategies) - # - # for entry in loaded_strategies: - # # Initialise all the strategy objects with data from file. - # self.strat_list.append(Strategy(**entry)) + def new_strategy(self, data: dict): + """ + Add a new strategy to the cache and database. - return None - - def new_strategy(self, data): - # Create an instance of the new Strategy. + :param data: A dictionary containing strategy data such as name, code, and workspace. + """ + # Create a new Strategy object and add it to the list self.strat_list.append(Strategy(**data)) - def delete_strategy(self, name): + # 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)) + ) + + def delete_strategy(self, name: str): + """ + Delete a strategy from the cache and database by name. + + :param name: The name of the strategy to delete. + """ obj = self.get_strategy_by_name(name) if obj: self.strat_list.remove(obj) - def get_strategies(self, form): - # Return a python object of all the strategies stored in this instance. + # Remove the strategy from cache and database + self.data.remove_row_from_datacache( + cache_name='strategies', + filter_vals=[('name', name)] + ) + + def get_all_strategy_names(self) -> 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=[]) + + if not strategies_df.empty: + return strategies_df['name'].tolist() + return None + + def get_strategies(self, form: str): + """ + Return strategies stored in this instance in various formats. + + :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 - # Return a JSON object of all the strategies stored in this instance. elif form == 'json': - strats = self.strat_list - json_str = [] - for strat in strats: - json_str.append(strat.to_json()) - return json_str - # Return a dictionary object of all the strategies stored in this instance. + return [strat.to_json() for strat in self.strat_list] elif form == 'dict': - strats = self.strat_list - s_list = [] - for st in strats: - dic = st.__dict__ - s_list.append(dic) - return s_list + return [strat.__dict__ for strat in self.strat_list] + return None - def get_strategy_by_name(self, name): + def get_strategy_by_name(self, name: str): + """ + Retrieve a strategy object by name. + + :param name: The name of the strategy to retrieve. + :return: The strategy object if found, otherwise False. + """ for obj in self.strat_list: if obj.name == name: return obj diff --git a/src/app.py b/src/app.py index ca577b8..b6bf3b8 100644 --- a/src/app.py +++ b/src/app.py @@ -105,39 +105,46 @@ def index(): @sock.route('/ws') def ws(socket_conn): """ - Open a websocket to handle two-way communication with UI without browser refreshes. + Open a WebSocket to handle two-way communication with UI without browser refreshes. """ def json_msg_received(msg_obj): """ - Handle incoming json msg's. + Handle incoming JSON messages with authentication. """ # Validate input - if 'message_type' not in msg_obj: + if 'message_type' not in msg_obj or 'data' not in msg_obj: return msg_type, msg_data = msg_obj['message_type'], msg_obj['data'] - print(f'\n[MSG_RECEIVED]\nMSG Type:{msg_type}\n{msg_data}') + # Extract user_name from the incoming message data + user_name = msg_data.get('user_name') + if not user_name: + socket_conn.send(json.dumps({"success": False, "message": "User not specified"})) + return + # Check if the user is logged in + if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): + socket_conn.send(json.dumps({"success": False, "message": "User not logged in"})) + return + + # Process the incoming message based on the type resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data) - socket_conn.send(json.dumps(resp)) + # Send the response back to the client + if resp: + socket_conn.send(json.dumps(resp)) - return - - # The rendered page connects to the exchange_interface and relays the candle data back here - # this socket also handles data and processing requests + # Main loop to receive messages and handle them while True: msg = socket_conn.receive() if msg: - # If msg is in json, convert the message into a dictionary then feed the message handler. - # Otherwise, log the output. try: json_msg = json.loads(msg) json_msg_received(json_msg) except json.JSONDecodeError: - print(f'Msg received from client: {msg}') + print(f'Msg received from client (not JSON): {msg}') @app.route('/settings', methods=['POST']) diff --git a/src/static/Strategies.js b/src/static/Strategies.js index 421aa90..2eeaccf 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -30,6 +30,13 @@ class Strategies { // Define Python generators after workspace initialization this.definePythonGenerators(); } + resizeWorkspace() { + const blocklyDiv = document.getElementById('blocklyDiv'); + if (this.workspace) { + // Adjust workspace dimensions to match the blocklyDiv + Blockly.svgResize(this.workspace); + } + } // Generate Python code from the Blockly workspace and return as JSON generateStrategyJson() { // Initialize Python generator with the current workspace @@ -38,15 +45,43 @@ class Strategies { // Generate Python code from the Blockly workspace const pythonCode = Blockly.Python.workspaceToCode(this.workspace); - // Create a strategy object with generated code + // Serialize the workspace to XML format + const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); + const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); + const json = { - name: document.getElementById('name_box').value, - code: pythonCode // This is the generated Python code + name: document.getElementById('name_box').value, + code: pythonCode, // This is the generated Python code + workspace: workspaceXmlText // Serialized workspace XML }; return JSON.stringify(json); } + restoreWorkspaceFromXml(workspaceXmlText) { + // Convert the text back into an XML DOM object + const workspaceXml = Blockly.Xml.textToDom(workspaceXmlText); + + // Clear the current workspace before loading a new one + if (this.workspace) { + this.workspace.clear(); + } + + // Load the workspace from the XML DOM object + Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); + } + 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)); + } + // Show the "Create New Strategy" form (open the workspace) open_form() { const formElement = document.getElementById("new_strat_form"); @@ -70,23 +105,40 @@ class Strategies { } } - // Submit the strategy and send it to the server submit() { // Generate the Python code as JSON const strategyJson = this.generateStrategyJson(); - // Send the strategy to the server - fetch('/new_strategy', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: strategyJson - }) - .then(response => response.json()) - .then(data => console.log('Strategy submitted successfully:', data)) - .catch(error => console.error('Error submitting strategy:', error)); + // 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 is_public = document.getElementById('public_checkbox').checked ? 1 : 0; + + // Merge additional fields into 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 + }; + + // 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); + } else { + console.error("Comms instance not available."); + } } - // Update the display of the created strategies + toggleFeeBox() { + const publicCheckbox = document.getElementById('public_checkbox'); + const feeBox = document.getElementById('fee_box'); + feeBox.disabled = !publicCheckbox.checked; // Enable feeBox only if publicCheckbox is checked + } + // Update the UI with saved strategies update_html() { let stratsHtml = ''; const onClick = "window.UI.strats.del(this.value);"; diff --git a/src/static/communication.js b/src/static/communication.js index 543d0ba..d7ec7e2 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -1,5 +1,7 @@ class Comms { constructor() { + this.connectionOpen = false; + this.appCon = null; // WebSocket connection for app communication // Callback collections that will receive various updates. this.candleUpdateCallbacks = []; @@ -170,33 +172,55 @@ class Comms { } /** - * Sends a message to the application server. + * Sends a message to the application server via WebSocket. + * Automatically includes the user_name from `window.UI.data.user_name` for authentication. * @param {string} messageType - The type of the message. * @param {Object} data - The data to be sent with the message. */ sendToApp(messageType, data) { - console.log('window.UI.data.comms.sendToApp(): Sending->'); - console.log(JSON.stringify({ message_type: messageType, data: data })); + const user_name = window.UI.data.user_name; // Access user_name from window.UI.data + + if (!user_name) { + console.error('User not logged in. Cannot send message.'); + return; + } + + const messageData = { + message_type: messageType, + data: { + ...data, // Include the existing data + user_name: user_name // Add user_name for authentication + } + }; + + console.log('Comms: Sending->', JSON.stringify(messageData)); if (this.connectionOpen) { - this.appCon.send(JSON.stringify({ message_type: messageType, data: data })); + this.appCon.send(JSON.stringify(messageData)); } else { setTimeout(() => { - window.UI.data.comms.appCon.send(JSON.stringify({ message_type: messageType, data: data })); + if (this.appCon) { + this.appCon.send(JSON.stringify(messageData)); + } }, 1000); } } + /** * Sets up the WebSocket connection to the application server. */ setAppCon() { this.appCon = new WebSocket('ws://localhost:5000/ws'); + + // On connection open this.appCon.onopen = () => { + console.log("WebSocket connection established"); this.appCon.send("Connection OK"); this.connectionOpen = true; }; + // Handle incoming messages this.appCon.addEventListener('message', (event) => { if (event.data) { const message = JSON.parse(event.data); @@ -207,24 +231,29 @@ class Comms { } if (message && message.reply !== undefined) { + // Handle different reply types from the server if (message.reply === 'updates') { const { i_updates, s_updates, stg_updts, trade_updts } = message.data; + // Handle indicator updates if (i_updates) { this.indicatorUpdate(i_updates); window.UI.signals.i_update(i_updates); } + + // Handle signal updates if (s_updates) { - const updates = s_updates; - window.UI.signals.update_signal_states(updates); - window.UI.alerts.publish_alerts('signal_changes', updates); + window.UI.signals.update_signal_states(s_updates); + window.UI.alerts.publish_alerts('signal_changes', s_updates); } + + // Handle strategy updates if (stg_updts) { - const stg_updts = stg_updts; window.UI.strats.update_received(stg_updts); } + + // Handle trade updates if (trade_updts) { - const trade_updts = trade_updts; window.UI.trade.update_received(trade_updts); } } else if (message.reply === 'signals') { @@ -248,8 +277,20 @@ class Comms { } } }); + + // On connection close + this.appCon.onclose = () => { + console.log("WebSocket connection closed"); + this.connectionOpen = false; + }; + + // On WebSocket error + this.appCon.onerror = (error) => { + console.error("WebSocket error:", error); + }; } + /** * 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/general.js b/src/static/general.js index 5400ae2..55278e2 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -1,15 +1,15 @@ class User_Interface { constructor() { // Initialize all components needed by the user interface - this.strats = new Strategies('strats_display'); // Handles strategies display and interaction - this.exchanges = new Exchanges(); // Manages exchange-related data - this.data = new Data(); // Stores and maintains application-wide data - this.controls = new Controls(); // Handles user controls - this.signals = new Signals(this.data.indicators); // Manages signals used in strategies - this.alerts = new Alerts("alert_list"); // Manages the alerts system - this.trade = new Trade(); // Manages trade-related data and operations - this.users = new Users(); // Handles user login and account details - this.indicators = new Indicators(this.data.comms); // Manages indicators and interaction with charts + this.strats = new Strategies('strats_display'); + this.exchanges = new Exchanges(); + this.data = new Data(); + this.controls = new Controls(); + this.signals = new Signals(this.data.indicators); + this.alerts = new Alerts("alert_list"); + this.trade = new Trade(); + this.users = new Users(); + this.indicators = new Indicators(this.data.comms); // Register a callback function for when indicator updates are received from the data object this.data.registerCallback_i_updates(this.indicators.update); @@ -18,41 +18,124 @@ class User_Interface { this.initializeAll(); } - /** - * Initializes all components after the DOM has loaded. - * This includes initializing charts, signals, strategies, and other interface components. - * Components that rely on external libraries like Blockly are initialized after they are ready. - */ initializeAll() { - // Use an arrow function to preserve the value of 'this' (User_Interface instance) window.addEventListener('load', () => { - // Initialize the Charts component with the required data - let chart_init_data = { - chart1_id: this.data.chart1_id, // ID of the first chart element - chart2_id: this.data.chart2_id, // ID of the second chart element - chart3_id: this.data.chart3_id, // ID of the third chart element - trading_pair: this.data.trading_pair, // Active trading pair - price_history: this.data.price_history // Historical price data - }; - this.charts = new Charts(chart_init_data); // Initialize the charts - - // Initialize indicators and link them to the charts - let ind_init_data = { - indicators: this.data.indicators, // List of indicators - indicator_data: this.data.indicator_data // Data for rendering indicators - }; - this.indicators.addToCharts(this.charts, ind_init_data); // Add indicators to the charts - - // Initialize various components that don't depend on external libraries - this.signals.request_signals(); // Request and initialize trading signals - this.alerts.set_target(); // Set up alert notifications - this.controls.init_TP_selector(); // Initialize trade parameter selectors - this.trade.initialize(); // Initialize trade-related data and UI elements - this.exchanges.initialize(); // Initialize exchange-related data - - this.strats.initialize(); // Initialize strategies once Blockly is ready + this.initializeChartsAndIndicators(); + this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats)); }); } + + initializeChartsAndIndicators() { + let chart_init_data = { + chart1_id: this.data.chart1_id, + chart2_id: this.data.chart2_id, + chart3_id: this.data.chart3_id, + trading_pair: this.data.trading_pair, + price_history: this.data.price_history + }; + this.charts = new Charts(chart_init_data); + + let ind_init_data = { + indicators: this.data.indicators, + indicator_data: this.data.indicator_data + }; + this.indicators.addToCharts(this.charts, ind_init_data); + + this.signals.request_signals(); + this.alerts.set_target(); + this.controls.init_TP_selector(); + this.trade.initialize(); + this.exchanges.initialize(); + this.strats.initialize(); + } + + /** + * Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces). + * @param {string} popupId - The ID of the popup to make resizable. + * @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements). + */ + initializeResizablePopup(popupId, resizeCallback = null) { + const popupElement = document.getElementById(popupId); + this.dragElement(popupElement); + this.makeResizable(popupElement, resizeCallback); + } + + /** + * Make an element draggable by dragging its header. + * @param {HTMLElement} elm - The element to make draggable. + */ + dragElement(elm) { + const header = document.getElementById("draggable_header"); + let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + + if (header) { + header.onmousedown = dragMouseDown; + } else { + elm.onmousedown = dragMouseDown; + } + + function dragMouseDown(e) { + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + elm.style.top = (elm.offsetTop - pos2) + "px"; + elm.style.left = (elm.offsetLeft - pos1) + "px"; + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } + } + + /** + * Make an element resizable and optionally call a resize callback (like for Blockly workspace). + * @param {HTMLElement} elm - The element to make resizable. + * @param {function|null} resizeCallback - Optional callback to resize specific content. + */ + makeResizable(elm, resizeCallback = null) { + const resizer = document.getElementById("resize-br"); + let originalWidth = 0; + let originalHeight = 0; + let originalMouseX = 0; + let originalMouseY = 0; + + resizer.addEventListener('mousedown', function(e) { + e.preventDefault(); + originalWidth = parseFloat(getComputedStyle(elm, null).getPropertyValue('width').replace('px', '')); + originalHeight = parseFloat(getComputedStyle(elm, null).getPropertyValue('height').replace('px', '')); + originalMouseX = e.pageX; + originalMouseY = e.pageY; + + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResize); + }); + + const resize = (e) => { + elm.style.width = originalWidth + (e.pageX - originalMouseX) + 'px'; + elm.style.height = originalHeight + (e.pageY - originalMouseY) + 'px'; + + // Optionally call the resize callback (for Blockly or other resizable components) + if (resizeCallback) { + resizeCallback(); + } + }; + + const stopResize = () => { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResize); + }; + } } // Instantiate the User_Interface class and assign it to a global variable for access in other parts of the app diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html index b7ba966..23caafd 100644 --- a/src/templates/new_strategy_popup.html +++ b/src/templates/new_strategy_popup.html @@ -1,24 +1,111 @@ - -