diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index e5d3bcd..6360c12 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -44,7 +44,7 @@ class BrighterTrades: self.indicators = Indicators(self.candles, self.users, self.data) # Object that maintains the trades data - self.trades = Trades(self.users) + self.trades = Trades(self.users, data_cache=self.data) # The Trades object needs to connect to an exchange_interface. self.trades.connect_exchanges(exchanges=self.exchanges) @@ -349,6 +349,12 @@ class BrighterTrades: price_updates = {symbol: float(cdata['close'])} trade_updates = self.trades.update(price_updates) + # Debug: log trade updates + if self.trades.active_trades: + logger.debug(f"Active trades: {list(self.trades.active_trades.keys())}") + logger.debug(f"Price updates for symbol '{symbol}': {price_updates}") + logger.debug(f"Trade updates returned: {trade_updates}") + # Update all active strategy instances with new candle data stg_updates = self.strategies.update(candle_data=cdata) @@ -1088,69 +1094,105 @@ class BrighterTrades: return result - def close_trade(self, trade_id): + def close_trade(self, trade_id: str, current_price: float = None) -> dict: """ Closes a trade identified by the given trade ID. :param trade_id: The ID of the trade to be closed. + :param current_price: Optional current price for settlement. + :return: Dict with success status and trade info. """ - if self.trades.is_valid_trade_id(trade_id): - pass - # self.trades.close_trade(trade_id)TODO - # self.config.remove('trades', trade_id) - print(f"Trade {trade_id} has been closed.") - else: - print(f"Invalid trade ID: {trade_id}. Unable to close the trade.") + if not self.trades.is_valid_trade_id(trade_id): + logger.warning(f"Invalid trade ID: {trade_id}. Unable to close the trade.") + return {"success": False, "message": f"Invalid trade ID: {trade_id}"} - def received_new_trade(self, data: dict) -> dict | None: + result = self.trades.close_trade(trade_id, current_price=current_price) + if result.get('success'): + logger.info(f"Trade {trade_id} has been closed.") + else: + logger.warning(f"Failed to close trade {trade_id}: {result.get('message')}") + + return result + + def received_new_trade(self, data: dict, user_id: int = None) -> dict: """ Called when a new trade has been defined and created in the UI. :param data: A dictionary containing the attributes of the trade. - :return: The details of the trade as a dictionary, or None on failure. + :param user_id: The ID of the user creating the trade. + :return: Dict with success status and trade data. """ - def vld(attr): + def get_value(attr, default=None): """ - Casts numeric strings to float before returning the attribute. - Returns None if the attribute is absent in the data. + Gets a value from data, casting numeric strings to float where appropriate. """ - if attr in data and data[attr] != '': + val = data.get(attr, default) + if val is None or val == '': + return default + # Try to cast to float for numeric fields + if attr in ['price', 'quantity']: try: - return float(data[attr]) - except ValueError: - return data[attr] - else: - return None + return float(val) + except (ValueError, TypeError): + return val + return val + + # Get trade parameters + target = get_value('target') or get_value('exchange_name', 'test_exchange') + symbol = get_value('symbol') or get_value('trading_pair') + price = get_value('price', 0.0) + side = get_value('side', 'buy') + order_type = get_value('orderType') or get_value('order_type', 'MARKET') + quantity = get_value('quantity', 0.0) + strategy_id = get_value('strategy_id') + + # Validate required fields + if not symbol: + return {"success": False, "message": "Symbol is required."} + if not quantity or float(quantity) <= 0: + return {"success": False, "message": "Quantity must be greater than 0."} + + # Forward the request to trades + status, result = self.trades.new_trade( + target=target, + symbol=symbol, + price=price, + side=side, + order_type=order_type, + qty=quantity, + user_id=user_id, + strategy_id=strategy_id + ) - # Forward the request to trades. - status, result = self.trades.new_trade(target=vld('exchange_name'), symbol=vld('symbol'), price=vld('price'), - side=vld('side'), order_type=vld('orderType'), - qty=vld('quantity')) if status == 'Error': - print(f'Error placing the trade: {result}') - return None + logger.warning(f'Error placing the trade: {result}') + return {"success": False, "message": result} - print(f'Trade order received: exchange_name={vld("exchange_name")}, ' - f'symbol={vld("symbol")}, ' - f'side={vld("side")}, ' - f'type={vld("orderType")}, ' - f'quantity={vld("quantity")}, ' - f'price={vld("price")}') - - # Update config's list of trades and save to file.TODO - # self.config.update_data('trades', self.trades.get_trades('dict')) + logger.info(f'Trade order received: target={target}, symbol={symbol}, ' + f'side={side}, type={order_type}, quantity={quantity}, price={price}') + # Get the created trade trade_obj = self.trades.get_trade_by_id(result) if trade_obj: - # Return the trade object that was created in a form that can be converted to json. - return trade_obj.__dict__ + return { + "success": True, + "message": "Trade created successfully.", + "trade": trade_obj.to_json() + } else: - return None + return {"success": False, "message": "Trade created but could not be retrieved."} - def get_trades(self): - """ Return a JSON object of all the trades in the trades instance.""" - return self.trades.get_trades('dict') + def get_trades(self, user_id: int = None): + """ + Return a JSON object of all the trades in the trades instance. + + :param user_id: Optional user ID to filter trades. + :return: List of trade dictionaries. + """ + if user_id is not None: + return self.trades.get_trades_for_user(user_id, 'json') + return self.trades.get_trades('json') def delete_backtest(self, msg_data): """ Delete an existing backtest by interacting with the Backtester. """ @@ -1296,8 +1338,8 @@ class BrighterTrades: return standard_reply("strategies", strategies) elif request_for == 'trades': - if trades := self.get_trades(): - return standard_reply("trades", trades) + trades = self.get_trades(user_id) + return standard_reply("trades", trades if trades else []) else: print('Warning: Unhandled request!') print(msg_data) @@ -1331,7 +1373,14 @@ class BrighterTrades: }) if msg_type == 'close_trade': - self.close_trade(msg_data) + trade_id = msg_data.get('trade_id') or msg_data.get('unique_id') or msg_data + if isinstance(trade_id, dict): + trade_id = trade_id.get('trade_id') or trade_id.get('unique_id') + result = self.close_trade(str(trade_id)) + if result.get('success'): + return standard_reply("trade_closed", result) + else: + return standard_reply("trade_error", result) if msg_type == 'new_signal': result = self.received_new_signal(msg_data, user_id) @@ -1365,8 +1414,11 @@ class BrighterTrades: return standard_reply("strategy_error", {"message": "Failed to edit strategy."}) if msg_type == 'new_trade': - if r_data := self.received_new_trade(msg_data): - return standard_reply("trade_created", r_data) + result = self.received_new_trade(msg_data, user_id=user_id) + if result.get('success'): + return standard_reply("trade_created", result) + else: + return standard_reply("trade_error", result) if msg_type == 'config_exchange': user = msg_data.get('user') or user_name diff --git a/src/app.py b/src/app.py index 35fd9d8..c1bef3f 100644 --- a/src/app.py +++ b/src/app.py @@ -23,6 +23,13 @@ from utils import sanitize_for_json # noqa: E402 # Set up logging log_level_name = os.getenv('BRIGHTER_LOG_LEVEL', 'INFO').upper() log_level = getattr(logging, log_level_name, logging.INFO) + +# Debug file logger for execution loop +_loop_debug = logging.getLogger('loop_debug') +_loop_debug.setLevel(logging.DEBUG) +_loop_handler = logging.FileHandler('/home/rob/PycharmProjects/BrighterTrading/trade_debug.log', mode='a') +_loop_handler.setFormatter(logging.Formatter('%(asctime)s - [LOOP] %(message)s')) +_loop_debug.addHandler(_loop_handler) logging.basicConfig(level=log_level) logging.getLogger('ccxt.base.exchange').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) @@ -145,6 +152,44 @@ def strategy_execution_loop(): except Exception as e: logger.error(f"Error executing strategy {instance_key}: {e}", exc_info=True) + # Update active trades (runs every iteration, regardless of active strategies) + _loop_debug.debug(f"Checking active_trades: {len(brighter_trades.trades.active_trades)} trades") + if brighter_trades.trades.active_trades: + _loop_debug.debug(f"Has active trades, getting prices...") + try: + symbols = set(trade.symbol for trade in brighter_trades.trades.active_trades.values()) + _loop_debug.debug(f"Symbols to fetch: {symbols}") + price_updates = {} + for symbol in symbols: + try: + price = brighter_trades.exchanges.get_price(symbol) + _loop_debug.debug(f"Got price for {symbol}: {price}") + if price: + price_updates[symbol] = price + except Exception as e: + _loop_debug.debug(f"Failed to get price for {symbol}: {e}") + logger.warning(f"Could not get price for {symbol}: {e}") + + _loop_debug.debug(f"price_updates: {price_updates}") + if price_updates: + _loop_debug.debug(f"Calling brighter_trades.trades.update()") + trade_updates = brighter_trades.trades.update(price_updates) + _loop_debug.debug(f"trade_updates returned: {trade_updates}") + if trade_updates: + logger.debug(f"Trade updates (no active strategies): {trade_updates}") + for update in trade_updates: + trade_id = update.get('id') + trade = brighter_trades.trades.active_trades.get(trade_id) + _loop_debug.debug(f"Emitting update for trade_id={trade_id}, creator={trade.creator if trade else None}") + if trade and trade.creator: + user_name = brighter_trades.users.get_username(user_id=trade.creator) + if user_name: + socketio.emit('trade_update', sanitize_for_json(update), room=user_name) + _loop_debug.debug(f"Emitted trade_update to room={user_name}") + except Exception as e: + _loop_debug.debug(f"Exception in trade update: {e}") + logger.error(f"Error updating trades (no strategies): {e}", exc_info=True) + except Exception as e: logger.error(f"Strategy execution loop error: {e}") diff --git a/src/static/communication.js b/src/static/communication.js index ea05de5..bfa73f0 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -78,6 +78,12 @@ class Comms { console.log('Strategy events received:', data); this.emit('strategy_events', data); }); + + // Handle trade update events from execution loop + this.socket.on('trade_update', (data) => { + console.log('Trade update received:', data); + this.emit('trade_update', data); + }); } /** @@ -333,7 +339,8 @@ class Comms { high: candlestick.h, low: candlestick.l, close: candlestick.c, - vol: candlestick.v + vol: candlestick.v, + symbol: tradingPair // Include trading pair for trade matching }; this.candleUpdate(newCandle); diff --git a/src/static/trade.js b/src/static/trade.js index 194b034..acc993c 100644 --- a/src/static/trade.js +++ b/src/static/trade.js @@ -1,195 +1,869 @@ -class Trade { +/** + * TradeUIManager - Handles DOM updates and trade card rendering + */ +class TradeUIManager { constructor() { - // A list of all the open trades. - this.trades = []; - /* HTML elements that interact with this class. */ - // The html element that displays the quote value to execute the trade at. - this.priceInput_el = null; - // The html element that displays the actual quote value of the base asset. - this.currentPrice_el = null; - // The html element that displays the quantity to executed the trade at. - this.qtyInput_el = null; - // The html element that displays the value of the trade. (quantity * price) - this.tradeValue_el = null; - // The html element that displays the orderType of the trade. (Market|Limit) - this.orderType_el = null; - // The html element that displays the target for the trade ['binance'|'test_exchange']. - this.target_el = null; - // The html element that displays the side to trade. [buy|sell] - this.side_el = null; - // The html element that displays the form dialog for a new trade. - this.formDialog_el = null; - // The html element that displays the active trades. - this.activeTLst_el = null; + this.targetEl = null; + this.formElement = null; + this.priceInput = null; + this.currentPriceDisplay = null; + this.qtyInput = null; + this.tradeValueDisplay = null; + this.orderTypeSelect = null; + this.targetSelect = null; + this.sideSelect = null; + this.onCloseTrade = null; } - connectElements({price='price', currentPrice='currentPrice', quantity='quantity', - tradeValue='tradeValue', orderType='orderType', target='tradeTarget', - side='side', ntf='new_trade_form', atl='activeTradesLst'}={}){ - /* - Sets a reference to the dom elements that interact with this class. - Overwriting the default id's allows for instance specific binding. - */ - this.priceInput_el = document.getElementById(price); - this.currentPrice_el = document.getElementById(currentPrice); - this.qtyInput_el = document.getElementById(quantity); - this.tradeValue_el = document.getElementById(tradeValue); - this.orderType_el = document.getElementById(orderType); - this.target_el = document.getElementById(target); - this.side_el = document.getElementById(side); - this.formDialog_el = document.getElementById(ntf); - this.activeTLst_el = document.getElementById(atl); - } - initialize(){ - /* Set the values and behavior of elements this class interacts with. */ - // Bind this instance with the html element ment to interact with it. - this.connectElements(); - // Store this object pointer for referencing inside callbacks and event handlers. - var that = this; - // Assign the quote value of the asset to the current price display element. - if (window.UI.data.price_history && window.UI.data.price_history.length > 0) { - let ph = window.UI.data.price_history; - // Assign the last closing price in the price history to the price input element. - that.priceInput_el.value = ph[ph.length - 1].close; - // Set the current price display to the same value. - that.currentPrice_el.value = that.priceInput_el.value; + /** + * Initializes the UI elements with provided IDs. + * @param {Object} config - Configuration object with element IDs. + */ + initUI(config = {}) { + const { + targetId = 'tradesContainer', + formElId = 'new_trade_form', + priceId = 'price', + currentPriceId = 'currentPrice', + qtyId = 'quantity', + tradeValueId = 'tradeValue', + orderTypeId = 'orderType', + tradeTargetId = 'tradeTarget', + sideId = 'side' + } = config; + + this.targetEl = document.getElementById(targetId); + if (!this.targetEl) { + console.warn(`Element for displaying trades "${targetId}" not found.`); + } + + this.formElement = document.getElementById(formElId); + if (!this.formElement) { + console.warn(`Trade form element "${formElId}" not found.`); + } + + this.priceInput = document.getElementById(priceId); + this.currentPriceDisplay = document.getElementById(currentPriceId); + this.qtyInput = document.getElementById(qtyId); + this.tradeValueDisplay = document.getElementById(tradeValueId); + this.orderTypeSelect = document.getElementById(orderTypeId); + this.targetSelect = document.getElementById(tradeTargetId); + this.sideSelect = document.getElementById(sideId); + + // Set up event listeners + this._setupFormListeners(); + } + + /** + * Set up form event listeners for price calculation. + */ + _setupFormListeners() { + const updateTradeValue = () => { + if (!this.qtyInput || !this.tradeValueDisplay) return; + + let price; + if (this.orderTypeSelect?.value === 'MARKET') { + price = parseFloat(this.currentPriceDisplay?.value || 0); + } else { + price = parseFloat(this.priceInput?.value || 0); + } + const qty = parseFloat(this.qtyInput.value || 0); + this.tradeValueDisplay.value = (qty * price).toFixed(2); + }; + + // Order type changes affect which price to use + if (this.orderTypeSelect) { + this.orderTypeSelect.addEventListener('change', () => { + if (this.orderTypeSelect.value === 'MARKET') { + if (this.currentPriceDisplay) this.currentPriceDisplay.style.display = 'inline-block'; + if (this.priceInput) this.priceInput.style.display = 'none'; + } else { + if (this.currentPriceDisplay) this.currentPriceDisplay.style.display = 'none'; + if (this.priceInput) this.priceInput.style.display = 'inline-block'; + } + updateTradeValue(); + }); + } + + // Update trade value on price/qty changes + if (this.priceInput) { + this.priceInput.addEventListener('change', updateTradeValue); + this.priceInput.addEventListener('input', updateTradeValue); + } + if (this.qtyInput) { + this.qtyInput.addEventListener('change', updateTradeValue); + this.qtyInput.addEventListener('input', updateTradeValue); + } + } + + /** + * Displays the trade creation form. + * @param {number} currentPrice - Optional current price to prefill. + */ + displayForm(currentPrice = null) { + if (!this.formElement) { + console.error("Form element not initialized."); + return; + } + + // Reset form values + if (this.qtyInput) this.qtyInput.value = ''; + if (this.tradeValueDisplay) this.tradeValueDisplay.value = '0'; + + // Set current price if available + if (currentPrice !== null) { + if (this.priceInput) this.priceInput.value = currentPrice; + if (this.currentPriceDisplay) this.currentPriceDisplay.value = currentPrice; + } + + this.formElement.style.display = 'grid'; + } + + /** + * Hides the trade form. + */ + hideForm() { + if (this.formElement) { + this.formElement.style.display = 'none'; + } + } + + /** + * Updates the current price display (for market orders). + * @param {number} price - The current price. + */ + updateCurrentPrice(price) { + if (this.currentPriceDisplay) { + this.currentPriceDisplay.value = price; + } + } + + /** + * Updates the HTML representation of the trades as cards. + * @param {Object[]} trades - The list of trades to display. + */ + updateTradesHtml(trades) { + if (!this.targetEl) { + console.error("Target element for displaying trades is not set."); + return; + } + + // Clear existing content + while (this.targetEl.firstChild) { + this.targetEl.removeChild(this.targetEl.firstChild); + } + + if (!trades || trades.length === 0) { + const emptyMsg = document.createElement('p'); + emptyMsg.className = 'no-data-msg'; + emptyMsg.textContent = 'No active trades'; + this.targetEl.appendChild(emptyMsg); + return; + } + + // Create and append cards for all trades + for (const trade of trades) { + try { + const tradeCard = this._createTradeCard(trade); + this.targetEl.appendChild(tradeCard); + } catch (error) { + console.error(`Error processing trade:`, error, trade); + } + } + } + + /** + * Creates a trade card HTML element. + * @param {Object} trade - The trade data. + * @returns {HTMLElement} - The card element. + */ + _createTradeCard(trade) { + const tradeItem = document.createElement('div'); + tradeItem.className = 'trade-card'; + tradeItem.setAttribute('data-trade-id', trade.unique_id || trade.tbl_key); + + // Add paper/live class + if (trade.is_paper) { + tradeItem.classList.add('trade-paper'); } else { - console.error('Price history data is not available or empty.'); + tradeItem.classList.add('trade-live'); } - // Set the trade value to zero. This will update when price and quantity inputs are received. - this.tradeValue_el.value = 0; - // Toggle current price or input-field for value updates depending on orderType. - function update_tradeValue(){ - if ( that.orderType_el.value == 'MARKET') - {that.tradeValue_el.value = that.qtyInput_el.value * that.currentPrice_el.value;} - else - {that.tradeValue_el.value = that.qtyInput_el.value * that.priceInput_el.value;} - } - // Market orders are executed at the current price. To avoid confusion. - // Grey out this price input when orderType is set to Market. - this.orderType_el.addEventListener('change', function(){ - if(this.value == 'MARKET') - { - that.currentPrice_el.style.display = "inline-block"; - that.priceInput_el.style.display = "none"; + + // Add side class + const side = (trade.side || 'BUY').toUpperCase(); + tradeItem.classList.add(side === 'BUY' ? 'trade-buy' : 'trade-sell'); + + // Close/Delete button + const closeButton = document.createElement('button'); + closeButton.className = 'trade-close-btn'; + closeButton.innerHTML = '✘'; + closeButton.title = 'Close Trade'; + closeButton.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.onCloseTrade) { + this.onCloseTrade(trade.unique_id || trade.tbl_key); } - else if(this.value == 'LIMIT') - { - that.currentPrice_el.style.display = "none"; - that.priceInput_el.style.display = "inline-block"; - } - update_tradeValue(); }); - // Update the trade value display everytime the price input changes. - this.priceInput_el.addEventListener('change', update_tradeValue); - // 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. - this.fetchTrades(); + tradeItem.appendChild(closeButton); + + // Paper badge + if (trade.is_paper) { + const paperBadge = document.createElement('span'); + paperBadge.className = 'trade-paper-badge'; + paperBadge.textContent = 'PAPER'; + tradeItem.appendChild(paperBadge); + } + + // Trade info container + const tradeInfo = document.createElement('div'); + tradeInfo.className = 'trade-info'; + + // Symbol and side + const symbolRow = document.createElement('div'); + symbolRow.className = 'trade-symbol-row'; + + const sideSpan = document.createElement('span'); + sideSpan.className = `trade-side ${side.toLowerCase()}`; + sideSpan.textContent = side; + symbolRow.appendChild(sideSpan); + + const symbolSpan = document.createElement('span'); + symbolSpan.className = 'trade-symbol'; + symbolSpan.textContent = trade.symbol || 'N/A'; + symbolRow.appendChild(symbolSpan); + + tradeInfo.appendChild(symbolRow); + + // Quantity + const qtyRow = document.createElement('div'); + qtyRow.className = 'trade-row'; + qtyRow.innerHTML = `Qty:${this._formatNumber(trade.base_order_qty)}`; + tradeInfo.appendChild(qtyRow); + + // Entry price + const entryRow = document.createElement('div'); + entryRow.className = 'trade-row'; + const stats = trade.stats || {}; + const entryPrice = stats.opening_price || trade.order_price || 0; + entryRow.innerHTML = `Entry:${this._formatPrice(entryPrice)}`; + tradeInfo.appendChild(entryRow); + + // P/L + const pl = stats.profit || 0; + const plPct = stats.profit_pct || 0; + const plRow = document.createElement('div'); + plRow.className = 'trade-row trade-pl-row'; + plRow.id = `${trade.unique_id}_pl`; + + const plClass = pl >= 0 ? 'positive' : 'negative'; + plRow.innerHTML = `P/L:${this._formatPL(pl)} (${this._formatPct(plPct)})`; + tradeInfo.appendChild(plRow); + + tradeItem.appendChild(tradeInfo); + + // Hover details panel + const hoverPanel = document.createElement('div'); + hoverPanel.className = 'trade-hover'; + + let hoverHtml = `${trade.symbol}`; + hoverHtml += `