/** * TradeUIManager - Handles DOM updates and trade card rendering */ class TradeUIManager { constructor() { 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.symbolInput = null; this.exchangeSelect = null; this.testnetCheckbox = null; this.testnetRow = null; this.stopLossInput = null; this.takeProfitInput = null; this.timeInForceSelect = null; this.exchangeRow = null; this.sltpRow = null; this.onCloseTrade = null; // Exchanges known to support testnet/sandbox mode in ccxt // IMPORTANT: Only include exchanges with verified working sandbox URLs // KuCoin does NOT have sandbox support - removed to prevent real trades! this.testnetSupportedExchanges = [ 'binance', 'binanceus', 'binanceusdm', 'binancecoinm', 'bybit', 'okx', 'okex', 'bitget', 'bitmex', 'deribit', 'phemex' // Removed: 'kucoin', 'kucoinfutures', 'mexc' - no sandbox support ]; } /** * 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', symbolId = 'tradeSymbol', exchangeId = 'tradeExchange', testnetId = 'tradeTestnet', testnetRowId = 'testnet-row', stopLossId = 'stopLoss', takeProfitId = 'takeProfit', timeInForceId = 'timeInForce', exchangeRowId = 'exchange-row', sltpRowId = 'sltp-row' } = 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); this.symbolInput = document.getElementById(symbolId); this.exchangeSelect = document.getElementById(exchangeId); this.testnetCheckbox = document.getElementById(testnetId); this.testnetRow = document.getElementById(testnetRowId); this.stopLossInput = document.getElementById(stopLossId); this.takeProfitInput = document.getElementById(takeProfitId); this.timeInForceSelect = document.getElementById(timeInForceId); this.exchangeRow = document.getElementById(exchangeRowId); this.sltpRow = document.getElementById(sltpRowId); // 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); } // Trade target (exchange) changes affect testnet visibility, exchange row, and SELL availability if (this.targetSelect) { this.targetSelect.addEventListener('change', () => { this._updateTestnetVisibility(); this._updateExchangeRowVisibility(); this._updateSellAvailability(); this._updateSltpVisibility(); }); } // Exchange dropdown changes update symbol list if (this.exchangeSelect) { this.exchangeSelect.addEventListener('change', async () => { const selectedExchange = this.exchangeSelect.value; await this._populateSymbolDropdown(selectedExchange, null); }); } // Symbol changes affect SELL availability if (this.symbolInput) { this.symbolInput.addEventListener('change', () => { this._updateSellAvailability(); }); } // Testnet checkbox changes affect broker key, thus SELL availability if (this.testnetCheckbox) { this.testnetCheckbox.addEventListener('change', () => { this._updateSellAvailability(); }); } // Side changes affect SL/TP visibility (not applicable for SELL/close) if (this.sideSelect) { this.sideSelect.addEventListener('change', () => { this._updateSltpVisibility(); }); } } /** * Populates the symbol dropdown with trading pairs from EDM. * @param {string} exchange - The exchange to fetch symbols for. * @param {string|null} selectedSymbol - Symbol to select (or null to use first). */ async _populateSymbolDropdown(exchange, selectedSymbol) { if (!this.symbolInput) return; // Popular base currencies to prioritize (most traded first) const popularBases = ['BTC', 'ETH', 'SOL', 'XRP', 'ADA', 'DOGE', 'AVAX', 'DOT', 'MATIC', 'LINK', 'LTC', 'UNI', 'ATOM', 'XLM', 'ALGO', 'FIL', 'NEAR', 'APT', 'ARB', 'OP']; // Common symbols as fallback const commonSymbols = ['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/USD', 'SOL/USDT', 'XRP/USDT', 'ADA/USDT', 'DOGE/USDT']; let symbols = []; let allExchangeSymbols = []; // Try to fetch symbols from EDM try { const edm_url = window.bt_data?.edm_url || 'http://localhost:8080'; const response = await fetch(`${edm_url}/exchanges/${exchange.toLowerCase()}/symbols`); if (response.ok) { const data = await response.json(); if (data.symbols && data.symbols.length > 0) { allExchangeSymbols = data.symbols; console.log(`Fetched ${allExchangeSymbols.length} symbols for ${exchange}`); // Prioritize: First add popular USDT pairs in order of popularity for (const base of popularBases) { const usdtPair = `${base}/USDT`; if (allExchangeSymbols.includes(usdtPair) && !symbols.includes(usdtPair)) { symbols.push(usdtPair); } } // Then add popular USD pairs for (const base of popularBases) { const usdPair = `${base}/USD`; if (allExchangeSymbols.includes(usdPair) && !symbols.includes(usdPair)) { symbols.push(usdPair); } } // Then add remaining USDT pairs (sorted) const remainingUsdtPairs = allExchangeSymbols .filter(s => s.endsWith('/USDT') && !symbols.includes(s)) .sort(); symbols.push(...remainingUsdtPairs); // Then add remaining USD pairs const remainingUsdPairs = allExchangeSymbols .filter(s => s.endsWith('/USD') && !symbols.includes(s)) .sort(); symbols.push(...remainingUsdPairs); // Finally add other pairs (BTC pairs, etc.) const otherPairs = allExchangeSymbols .filter(s => !symbols.includes(s)) .sort(); symbols.push(...otherPairs); } } } catch (error) { console.warn(`Failed to fetch symbols for ${exchange}, using defaults:`, error); } // Fall back to common symbols if nothing fetched if (symbols.length === 0) { symbols = [...commonSymbols]; } // Ensure selected symbol is at the top of the list if (selectedSymbol) { // Remove it if it exists elsewhere in the list symbols = symbols.filter(s => s !== selectedSymbol); // Add it to the front symbols.unshift(selectedSymbol); } // Populate the dropdown this.symbolInput.innerHTML = ''; for (const symbol of symbols) { const option = document.createElement('option'); option.value = symbol; option.textContent = symbol; this.symbolInput.appendChild(option); } // Set the selected value if (selectedSymbol) { this.symbolInput.value = selectedSymbol; } } /** * Updates testnet checkbox visibility based on selected exchange. */ _updateTestnetVisibility() { if (!this.testnetRow || !this.targetSelect) return; const selectedTarget = this.targetSelect.value; const isPaperTrade = selectedTarget === 'test_exchange'; if (isPaperTrade) { // Hide testnet row for paper trading this.testnetRow.style.display = 'none'; } else { // Show testnet row for live exchanges this.testnetRow.style.display = 'block'; // Check if this exchange supports testnet const exchangeId = selectedTarget.toLowerCase(); const supportsTestnet = this.testnetSupportedExchanges.includes(exchangeId); const warningEl = document.getElementById('testnet-warning'); const unavailableEl = document.getElementById('testnet-unavailable'); if (supportsTestnet) { // Enable testnet checkbox if (this.testnetCheckbox) { this.testnetCheckbox.disabled = false; this.testnetCheckbox.checked = true; } if (warningEl) warningEl.style.display = 'block'; if (unavailableEl) unavailableEl.style.display = 'none'; } else { // Disable testnet checkbox - this exchange doesn't support it if (this.testnetCheckbox) { this.testnetCheckbox.disabled = true; this.testnetCheckbox.checked = false; } if (warningEl) warningEl.style.display = 'none'; if (unavailableEl) unavailableEl.style.display = 'block'; } } } /** * Updates exchange row visibility based on trade mode. * Paper trades use a single synthetic market, so exchange selection is irrelevant. */ _updateExchangeRowVisibility() { if (!this.exchangeRow || !this.targetSelect) return; const selectedTarget = this.targetSelect.value; const isPaperTrade = selectedTarget === 'test_exchange'; if (isPaperTrade) { // Hide exchange row for paper trading (uses single synthetic market) this.exchangeRow.style.display = 'none'; } else { // Show exchange row for live exchanges this.exchangeRow.style.display = 'contents'; } } /** * Updates SL/TP row visibility based on supported mode and side. * Manual SL/TP is currently supported for paper BUY orders only. */ _updateSltpVisibility() { if (!this.sltpRow || !this.sideSelect || !this.targetSelect) return; const side = this.sideSelect.value.toLowerCase(); const isPaperTrade = this.targetSelect.value === 'test_exchange'; if (!isPaperTrade || side === 'sell') { // Hide SL/TP when unsupported or not applicable. this.sltpRow.style.display = 'none'; // Clear values to avoid submitting stale unsupported inputs. if (this.stopLossInput) this.stopLossInput.value = ''; if (this.takeProfitInput) this.takeProfitInput.value = ''; } else { // Show SL/TP for paper BUY orders. this.sltpRow.style.display = 'contents'; } } /** * Populates the exchange selector with connected exchanges. * @param {string[]} connectedExchanges - List of connected exchange names. */ populateExchangeSelector(connectedExchanges) { if (!this.targetSelect) return; // Clear existing options except Paper Trade while (this.targetSelect.options.length > 1) { this.targetSelect.remove(1); } // Add connected exchanges if (connectedExchanges && connectedExchanges.length > 0) { for (const exchange of connectedExchanges) { // Skip 'default' exchange used internally if (exchange.toLowerCase() === 'default') continue; const option = document.createElement('option'); option.value = exchange.toLowerCase(); option.textContent = `${exchange} (Live)`; this.targetSelect.appendChild(option); } } } /** * Populates the exchange dropdown for price data source. * @param {string[]} availableExchanges - List of available exchange names. * @param {string} defaultExchange - Default exchange to select (chart view). */ populateExchangeDropdown(availableExchanges, defaultExchange = null) { if (!this.exchangeSelect) return; // Clear existing options this.exchangeSelect.innerHTML = ''; // Common exchanges list as fallback const commonExchanges = ['binance', 'kucoin', 'coinbase', 'kraken', 'bybit', 'okx', 'gemini', 'bitstamp']; let exchanges = (availableExchanges && availableExchanges.length > 0) ? [...availableExchanges] : [...commonExchanges]; // Ensure chart view exchange is in the list and at the top if (defaultExchange) { const normalizedDefault = defaultExchange.toLowerCase(); // Remove if exists elsewhere in the list exchanges = exchanges.filter(e => e.toLowerCase() !== normalizedDefault); // Add at the beginning exchanges.unshift(normalizedDefault); } // Add exchanges for (const exchange of exchanges) { if (exchange.toLowerCase() === 'default') continue; const option = document.createElement('option'); option.value = exchange.toLowerCase(); option.textContent = exchange.charAt(0).toUpperCase() + exchange.slice(1).toLowerCase(); this.exchangeSelect.appendChild(option); } // Set default to chart view exchange (first option after our reordering) if (defaultExchange) { this.exchangeSelect.value = defaultExchange.toLowerCase(); } } /** * Displays the trade creation form. * @param {number} currentPrice - Optional current price to prefill. * @param {string} symbol - Optional trading pair to prefill. * @param {string[]} connectedExchanges - Optional list of connected exchanges. * @param {string} chartExchange - Optional current chart view exchange. */ async displayForm(currentPrice = null, symbol = null, connectedExchanges = null, chartExchange = 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'; if (this.stopLossInput) this.stopLossInput.value = ''; if (this.takeProfitInput) this.takeProfitInput.value = ''; if (this.timeInForceSelect) this.timeInForceSelect.value = 'GTC'; // Set current price if available if (currentPrice !== null) { if (this.priceInput) this.priceInput.value = currentPrice; if (this.currentPriceDisplay) this.currentPriceDisplay.value = currentPrice; } // Populate target selector (Paper vs Live exchanges) if (connectedExchanges) { this.populateExchangeSelector(connectedExchanges); } // Populate exchange dropdown for price data source this.populateExchangeDropdown(connectedExchanges, chartExchange); // Populate symbol dropdown with the chart exchange and symbol const exchangeToUse = chartExchange || 'binance'; await this._populateSymbolDropdown(exchangeToUse, symbol); // Reset to paper trade and hide testnet/exchange rows if (this.targetSelect) { this.targetSelect.value = 'test_exchange'; } if (this.testnetRow) { this.testnetRow.style.display = 'none'; } if (this.exchangeRow) { // Hide exchange row for paper trading (uses single synthetic market) this.exchangeRow.style.display = 'none'; } if (this.testnetCheckbox) { this.testnetCheckbox.checked = true; } // Reset side to BUY if (this.sideSelect) { this.sideSelect.value = 'buy'; } this.formElement.style.display = 'grid'; // Update SELL availability based on current broker/symbol await this._updateSellAvailability(); this._updateSltpVisibility(); } /** * 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 { tradeItem.classList.add('trade-live'); } // 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); } }); 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); } else if (trade.testnet) { // Testnet badge for live trades const testnetBadge = document.createElement('span'); testnetBadge.className = 'trade-testnet-badge'; testnetBadge.textContent = 'TESTNET'; testnetBadge.style.cssText = 'background: #28a745; color: white; padding: 1px 4px; border-radius: 3px; font-size: 9px; position: absolute; top: 4px; left: 4px;'; tradeItem.appendChild(testnetBadge); } else if (!trade.is_paper) { // Production badge for live trades (not paper, not testnet) const prodBadge = document.createElement('span'); prodBadge.className = 'trade-prod-badge'; prodBadge.textContent = 'LIVE'; prodBadge.style.cssText = 'background: #dc3545; color: white; padding: 1px 4px; border-radius: 3px; font-size: 9px; position: absolute; top: 4px; left: 4px;'; tradeItem.appendChild(prodBadge); } // 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 += `
`; hoverHtml += `Side: ${side}`; hoverHtml += `Type: ${trade.order_type || 'MARKET'}`; hoverHtml += `Quantity: ${this._formatNumber(trade.base_order_qty)}`; hoverHtml += `Entry Price: ${this._formatPrice(entryPrice)}`; hoverHtml += `Current Value: ${this._formatPrice(stats.current_value || 0)}`; hoverHtml += `P/L: ${this._formatPL(pl)} (${this._formatPct(plPct)})`; hoverHtml += `Status: ${trade.status || 'unknown'}`; // Show actual exchange name (use 'exchange' field for paper trades, otherwise 'target') const displayExchange = trade.is_paper ? (trade.exchange || trade.target || 'N/A') : (trade.target || 'N/A'); hoverHtml += `Exchange: ${displayExchange}`; if (trade.is_paper) { hoverHtml += `Paper Trade`; } else if (trade.testnet) { hoverHtml += `Testnet`; } else { hoverHtml += `Production (Live)`; } hoverHtml += `
`; hoverPanel.innerHTML = hoverHtml; tradeItem.appendChild(hoverPanel); return tradeItem; } /** * Updates a single trade's P/L display. * @param {string} tradeId - The trade ID. * @param {number} pl - The P/L value. * @param {number} plPct - The P/L percentage. */ updateTradePL(tradeId, pl, plPct) { const plEl = document.getElementById(`${tradeId}_pl_value`); if (plEl) { const plClass = pl >= 0 ? 'positive' : 'negative'; plEl.className = `trade-value trade-pl ${plClass}`; plEl.textContent = `${this._formatPL(pl)} (${this._formatPct(plPct)})`; // Add flash animation plEl.classList.add('trade-pl-flash'); setTimeout(() => plEl.classList.remove('trade-pl-flash'), 300); } } /** * Removes a trade card from the display. * @param {string} tradeId - The trade ID. */ removeTradeCard(tradeId) { const card = document.querySelector(`[data-trade-id="${tradeId}"]`); if (card) { card.classList.add('trade-card-removing'); setTimeout(() => card.remove(), 300); } } // ============ Formatting helpers ============ _formatNumber(num) { if (num === null || num === undefined) return 'N/A'; return parseFloat(num).toLocaleString(undefined, { maximumFractionDigits: 6 }); } _formatPrice(price) { if (price === null || price === undefined) return 'N/A'; return parseFloat(price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 }); } _formatPL(pl) { if (pl === null || pl === undefined) return '$0.00'; const formatted = Math.abs(pl).toFixed(2); return pl >= 0 ? `+$${formatted}` : `-$${formatted}`; } _formatPct(pct) { if (pct === null || pct === undefined) return '0.00%'; const formatted = Math.abs(pct).toFixed(2); return pct >= 0 ? `+${formatted}%` : `-${formatted}%`; } /** * Sets the callback function for closing a trade. * @param {Function} callback - The callback function. */ registerCloseTradeCallback(callback) { this.onCloseTrade = callback; } /** * Sets the callback function for full refresh (trades + statistics). * @param {Function} callback - The callback function. */ registerRefreshCallback(callback) { this.onRefresh = callback; } /** * Sets the callback function for position-close updates. * @param {Function} callback - The callback function. */ registerPositionClosedCallback(callback) { this.onPositionClosed = callback; } // ============ Broker Event Listeners ============ /** * Initialize broker event listeners through Comms. * @param {Comms} comms - The communications instance. */ initBrokerListeners(comms) { if (!comms) return; // Listen for order fill events via existing message/reply pattern comms.on('order_filled', (data) => { console.log('Order filled:', data); this.refreshAll(); }); comms.on('order_cancelled', (data) => { console.log('Order cancelled:', data); this.refreshAll(); }); comms.on('position_closed', (data) => { console.log('Position closed:', data); if (this.onPositionClosed) { this.onPositionClosed(data); } else { this.refreshAll(); } }); comms.on('sltp_triggered', (data) => { console.log('SL/TP triggered:', data); const triggerName = data.trigger === 'stop_loss' ? 'Stop Loss' : 'Take Profit'; const pnl = data.pnl != null ? data.pnl.toFixed(2) : 'N/A'; alert(`${triggerName} triggered for ${data.symbol}\nPrice: ${data.trigger_price}\nP/L: ${pnl}`); this.refreshAll(); }); } // ============ Open Orders Section ============ /** * Render open orders section. * @param {Object[]} orders - List of open order dicts. */ renderOrders(orders) { const container = document.getElementById('openOrdersContainer'); if (!container) return; container.innerHTML = ''; if (!orders || orders.length === 0) { container.innerHTML = '

No open orders

'; return; } const table = document.createElement('table'); table.className = 'orders-table'; table.innerHTML = ` Symbol Side Size Price Broker Action `; for (const order of orders) { const row = document.createElement('tr'); const sideClass = (order.side || '').toLowerCase() === 'buy' ? 'order-buy' : 'order-sell'; row.className = `order-row ${sideClass}`; row.innerHTML = ` ${order.symbol || 'N/A'} ${(order.side || '').toUpperCase()} ${this._formatNumber(order.size)} ${order.price ? this._formatPrice(order.price) : 'MARKET'} ${order.broker_key || 'paper'} `; table.appendChild(row); } container.appendChild(table); } // ============ Positions Section ============ /** * Render positions section. * @param {Object[]} positions - List of position dicts. */ renderPositions(positions) { const container = document.getElementById('positionsContainer'); if (!container) return; container.innerHTML = ''; // Filter out closed positions (size <= 0) const openPositions = (positions || []).filter(pos => pos.size && Math.abs(pos.size) > 0); if (openPositions.length === 0) { container.innerHTML = '

No open positions

'; return; } for (const pos of openPositions) { const card = this._createPositionCard(pos); container.appendChild(card); } } _createPositionCard(position) { const card = document.createElement('div'); card.className = 'position-card'; const pl = position.unrealized_pnl || 0; const plClass = pl >= 0 ? 'positive' : 'negative'; const plSign = pl >= 0 ? '+' : ''; // Price source for tooltip (shows which exchange's prices are used for P&L) const priceSource = position.price_source || 'default'; card.title = `P&L uses ${priceSource} prices`; card.innerHTML = `
${position.symbol || 'N/A'} ${position.broker_key || 'paper'}
Size: ${this._formatNumber(position.size)}
Entry: ${position.entry_price ? this._formatPrice(position.entry_price) : '-'}
P/L: ${plSign}${pl.toFixed(2)}
`; return card; } // ============ History Section ============ /** * Render trade history section. * @param {Object[]} history - List of trade history dicts. */ renderHistory(history) { const container = document.getElementById('historyContainer'); if (!container) return; container.innerHTML = ''; if (!history || history.length === 0) { container.innerHTML = '

No trade history

'; return; } // Use history-specific card (no close button) for (const trade of history) { try { const card = this._createHistoryCard(trade); container.appendChild(card); } catch (error) { console.error('Error rendering history trade:', error, trade); } } } /** * Create a history card for settled/cancelled trades. * Unlike active trade cards, history cards have no close button. * @param {Object} trade - The trade data. * @returns {HTMLElement} - The history card element. */ _createHistoryCard(trade) { const card = document.createElement('div'); card.className = 'trade-card trade-history'; card.setAttribute('data-trade-id', trade.unique_id || trade.tbl_key); // Add paper/live class if (trade.is_paper) { card.classList.add('trade-paper'); } // Add side class const side = (trade.side || 'BUY').toUpperCase(); card.classList.add(side === 'BUY' ? 'trade-buy' : 'trade-sell'); // Status badge (closed/cancelled) const statusBadge = document.createElement('span'); statusBadge.className = 'trade-status-badge'; statusBadge.textContent = (trade.status || 'closed').toUpperCase(); statusBadge.style.cssText = ` position: absolute; top: 4px; right: 4px; background: ${trade.status === 'cancelled' ? '#ff9800' : '#9e9e9e'}; color: white; padding: 1px 4px; border-radius: 3px; font-size: 9px; `; card.appendChild(statusBadge); // Paper badge if (trade.is_paper) { const paperBadge = document.createElement('span'); paperBadge.className = 'trade-paper-badge'; paperBadge.textContent = 'PAPER'; card.appendChild(paperBadge); } // Trade info const info = document.createElement('div'); info.className = 'trade-info'; const symbolRow = document.createElement('div'); symbolRow.className = 'trade-symbol-row'; symbolRow.innerHTML = ` ${side} ${trade.symbol || 'N/A'} `; info.appendChild(symbolRow); // Stats const stats = trade.stats || {}; const qty = stats.qty_filled || trade.base_order_qty || 0; const settledPrice = stats.settled_price || stats.opening_price || trade.order_price || 0; const profit = stats.profit || 0; const profitClass = profit >= 0 ? 'positive' : 'negative'; const profitSign = profit >= 0 ? '+' : ''; info.innerHTML += `
Qty: ${this._formatNumber(qty)}
Price: ${this._formatPrice(settledPrice)}
P/L: ${profitSign}${profit.toFixed(2)}
`; card.appendChild(info); return card; } // ============ Refresh Methods ============ async refreshOrders() { try { const response = await fetch('/api/manual/orders'); const data = await response.json(); if (data.success) { this.renderOrders(data.orders); } } catch (e) { console.error('Failed to refresh orders:', e); } } async refreshPositions() { try { const response = await fetch('/api/manual/positions'); const data = await response.json(); if (data.success) { this.renderPositions(data.positions); } } catch (e) { console.error('Failed to refresh positions:', e); } } async refreshHistory() { try { const response = await fetch('/api/manual/history?limit=20'); const data = await response.json(); if (data.success) { this.renderHistory(data.history); } } catch (e) { console.error('Failed to refresh history:', e); } } refreshAll() { this.refreshOrders(); this.refreshPositions(); this.refreshHistory(); this.updateBrokerStatus(); // Call refresh callback to update trades and statistics if (this.onRefresh) { this.onRefresh(); } } // ============ Broker Actions ============ /** * Cancel a specific open order via REST API. */ async cancelOrder(orderId, brokerKey) { try { const response = await fetch(`/api/manual/orders/${encodeURIComponent(orderId)}/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ broker_key: brokerKey }) }); const result = await response.json(); if (result.success) { this.refreshAll(); } else { console.error('Cancel failed:', result.message); alert('Failed to cancel order: ' + result.message); } } catch (error) { console.error('Cancel order error:', error); } } /** * Close a position (filled exposure only) via REST API. */ async closePosition(symbol, brokerKey) { try { const response = await fetch(`/api/manual/positions/${encodeURIComponent(symbol)}/close`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ broker_key: brokerKey }) }); const result = await response.json(); if (result.success) { this.refreshAll(); } else { console.error('Close position failed:', result.message); alert('Failed to close position: ' + result.message); } } catch (error) { console.error('Close position error:', error); } } /** * Cancel all resting orders for a symbol (explicit user action). */ async cancelOrdersForSymbol(symbol, brokerKey) { try { const response = await fetch(`/api/manual/orders/symbol/${encodeURIComponent(symbol)}/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ broker_key: brokerKey }) }); const result = await response.json(); if (result.success) { this.refreshAll(); } else { console.error('Cancel orders failed:', result.message); } } catch (error) { console.error('Cancel orders error:', error); } } // ============ Broker Status ============ /** * Get current broker key based on form selection. */ _getCurrentBrokerKey() { if (!this.targetSelect) return 'paper'; const target = this.targetSelect.value; if (target === 'test_exchange' || target === 'paper') return 'paper'; const testnet = this.testnetCheckbox?.checked ?? true; return `${target}_${testnet ? 'testnet' : 'production'}`; } /** * Update broker status bar display. */ async updateBrokerStatus() { const brokerKey = this._getCurrentBrokerKey(); const chartExchange = this.data?.exchange || ''; try { const params = new URLSearchParams({ broker_key: brokerKey, exchange: chartExchange }); const response = await fetch(`/api/manual/balance?${params.toString()}`); const data = await response.json(); if (data.success) { const balanceEl = document.getElementById('brokerBalance'); const modeEl = document.getElementById('brokerModeIndicator'); if (balanceEl) { const balance = data.available ?? data.total ?? 0; // Get currency preference from localStorage (default: USD for paper, USDT for live) const defaultCurrency = brokerKey === 'paper' ? 'USD' : 'USDT'; const currency = localStorage.getItem('balanceCurrency') || defaultCurrency; balanceEl.textContent = `Available: $${balance.toFixed(2)} ${currency}`; balanceEl.style.cursor = 'pointer'; balanceEl.title = data.source === 'exchange' ? `Using ${chartExchange || 'exchange'} chart balance. Click to toggle USD/USDT` : 'Click to toggle USD/USDT'; // Add click handler if not already added if (!balanceEl.dataset.clickHandler) { balanceEl.dataset.clickHandler = 'true'; balanceEl.addEventListener('click', () => { const current = localStorage.getItem('balanceCurrency') || defaultCurrency; const newCurrency = current === 'USD' ? 'USDT' : 'USD'; localStorage.setItem('balanceCurrency', newCurrency); this.updateBrokerStatus(); }); } } if (modeEl) { if (brokerKey === 'paper') { modeEl.textContent = 'PAPER'; modeEl.className = 'mode-badge mode-paper'; } else if (brokerKey.includes('testnet')) { modeEl.textContent = 'TESTNET'; modeEl.className = 'mode-badge mode-testnet'; } else { modeEl.textContent = 'LIVE'; modeEl.className = 'mode-badge mode-live'; } } } } catch (e) { console.warn('Could not fetch balance:', e); } // Update status bar class for reset button visibility const statusBar = document.getElementById('brokerStatusBar'); if (statusBar) { statusBar.className = `broker-status-bar mode-${brokerKey === 'paper' ? 'paper' : brokerKey.includes('testnet') ? 'testnet' : 'live'}`; } } /** * Reset paper trading balance to initial state ($10,000). */ async resetPaperBalance() { if (!confirm('Reset paper trading? This will clear all positions, orders, and restore your balance to $10,000 USD.')) { return; } try { const response = await fetch('/api/manual/paper/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { console.log('Paper balance reset:', data); alert(`Paper trading reset! New balance: $${data.balance.toFixed(2)} USD`); this.refreshAll(); } else { alert(`Reset failed: ${data.message}`); } } catch (e) { console.error('Error resetting paper balance:', e); alert('Failed to reset paper balance'); } } // ============ Broker-Aware SELL Disable ============ /** * Check if position exists for symbol and broker. */ async _checkPositionExists(symbol, brokerKey) { try { const response = await fetch('/api/manual/positions'); const data = await response.json(); if (data.success && data.positions) { return data.positions.some(p => p.symbol === symbol && p.broker_key === brokerKey && (p.size || 0) > 0 ); } } catch (e) { console.warn('Could not check position:', e); } return false; } /** * Update SELL option availability based on position. * If SELL is currently selected but becomes invalid, reset to BUY. */ async _updateSellAvailability() { if (!this.sideSelect || !this.symbolInput) return; const symbol = this.symbolInput.value; const brokerKey = this._getCurrentBrokerKey(); const hasPosition = await this._checkPositionExists(symbol, brokerKey); const sellOption = this.sideSelect.querySelector('option[value="SELL"]'); if (sellOption) { sellOption.disabled = !hasPosition; sellOption.title = hasPosition ? '' : 'No position to sell. Buy first.'; // If SELL is currently selected but no longer valid, reset to BUY if (!hasPosition && this.sideSelect.value === 'SELL') { this.sideSelect.value = 'BUY'; this._updateSltpVisibility(); } } } } /** * TradeDataManager - Manages in-memory trade data store */ class TradeDataManager { constructor() { this.trades = []; } /** * Fetches trades from the server. * @param {Object} comms - The communications instance. * @param {Object} data - An object containing user data. */ fetchTrades(comms, data) { if (comms) { try { const requestData = { request: 'trades', user_name: data?.user_name }; comms.sendToApp('request', requestData); } catch (error) { console.error("Error fetching trades:", error.message); } } else { throw new Error('Communications instance not available.'); } } /** * Adds a new trade to the local store. * @param {Object} data - The trade data. */ addTrade(data) { const tradeData = data.trade || data; console.log("Adding new trade:", tradeData); if (!tradeData.unique_id && !tradeData.tbl_key) { console.error("Trade data missing identifier:", tradeData); return; } // Check for duplicates const id = tradeData.unique_id || tradeData.tbl_key; const exists = this.trades.find(t => t.unique_id === id || t.tbl_key === id); if (!exists) { this.trades.push(tradeData); } } /** * Retrieves a trade by its ID. * @param {string} tradeId - The trade ID. * @returns {Object|null} - The trade object or null. */ getTradeById(tradeId) { return this.trades.find(t => t.unique_id === tradeId || t.tbl_key === tradeId) || null; } /** * Updates trade data (including P/L updates). * @param {Object} updateData - The updated trade data. */ updateTrade(updateData) { const tradeId = updateData.id || updateData.unique_id || updateData.tbl_key; if (!tradeId) return; const trade = this.getTradeById(tradeId); if (trade) { // Update stats if (updateData.pl !== undefined) { trade.stats = trade.stats || {}; trade.stats.profit = updateData.pl; } if (updateData.pl_pct !== undefined) { trade.stats = trade.stats || {}; trade.stats.profit_pct = updateData.pl_pct; } if (updateData.status) { trade.status = updateData.status; } } } /** * Removes a trade from the store. * @param {string} tradeId - The trade ID. */ removeTrade(tradeId) { console.log(`Removing trade: ${tradeId}`); this.trades = this.trades.filter( t => t.unique_id !== tradeId && t.tbl_key !== tradeId ); } /** * Returns all trades. * @returns {Object[]} - The list of trades. */ getAllTrades() { return this.trades; } /** * Sets all trades (used when loading from server). * @param {Object[]} trades - The list of trades. */ setTrades(trades) { this.trades = Array.isArray(trades) ? trades : []; } /** * Applies batch P/L updates from server. * @param {Object[]} updates - Array of trade update objects. */ applyUpdates(updates) { if (!Array.isArray(updates)) return; for (const update of updates) { this.updateTrade(update); } } /** * Gets trade statistics. * @returns {Object} - Statistics object. */ getStatistics() { const totalTrades = this.trades.length; const paperTrades = this.trades.filter(t => t.is_paper).length; const liveTrades = totalTrades - paperTrades; let totalPL = 0; for (const trade of this.trades) { totalPL += (trade.stats?.profit || 0); } return { totalTrades, paperTrades, liveTrades, totalPL }; } } /** * Trade - Main coordinator class that manages TradeUIManager, TradeDataManager, and SocketIO communication */ class Trade { constructor(ui = null) { this.ui = ui; this.comms = null; this.data = null; this.dataManager = new TradeDataManager(); this.uiManager = new TradeUIManager(); // Set up close callback this.uiManager.registerCloseTradeCallback(this.closeTrade.bind(this)); // Set up refresh callback for trades and statistics this.uiManager.registerRefreshCallback(() => { this.fetchTrades(); this._updateStatistics(); }); this.uiManager.registerPositionClosedCallback(this.handlePositionClosed.bind(this)); // Bind methods this.submitNewTrade = this.submitNewTrade.bind(this); this._initialized = false; } /** * Initializes the Trade instance. * Called after page load when UI.data is available. * @param {Object} config - Configuration object. */ initialize(config = {}) { try { // Get references from global UI if not provided in constructor if (!this.ui && window.UI) { this.ui = window.UI; } this.data = this.ui?.data || window.UI?.data; this.comms = this.data?.comms; this.uiManager.initUI(config); if (!this.comms) { console.error("Communications instance not available."); return; } // Register handlers with Comms this.comms.on('trades', this.handleTradesResponse.bind(this)); this.comms.on('trade_created', this.handleTradeCreated.bind(this)); this.comms.on('trade_closed', this.handleTradeClosed.bind(this)); this.comms.on('trade_error', this.handleTradeError.bind(this)); this.comms.on('updates', this.handleUpdates.bind(this)); this.comms.on('trade_update', this.handleTradeUpdate.bind(this)); // Set initial price if available if (this.data?.price_history && this.data.price_history.length > 0) { const lastPrice = this.data.price_history[this.data.price_history.length - 1].close; this.uiManager.updateCurrentPrice(lastPrice); } // Update trading pair display this._updateTradingPairDisplay(); // Fetch existing trades (legacy path) this.dataManager.fetchTrades(this.comms, this.data); // Initialize broker event listeners and refresh broker UI this.uiManager.initBrokerListeners(this.comms); this.refreshAll(); this._initialized = true; console.log("Trade module initialized successfully"); } catch (error) { console.error("Error initializing Trade:", error); } } /** * Refresh all broker-backed panels through the UI manager. */ refreshAll() { this.uiManager.refreshAll(); // Also refresh trades and statistics this.fetchTrades(); this._updateStatistics(); } /** * Delegate broker balance refresh to the UI manager. */ updateBrokerStatus() { return this.uiManager.updateBrokerStatus(); } /** * Delegate order cancellation to the UI manager. * Kept here because DOM actions call UI.trade.* methods. */ cancelOrder(orderId, brokerKey) { return this.uiManager.cancelOrder(orderId, brokerKey); } /** * Delegate position close to the UI manager. * Kept here because DOM actions call UI.trade.* methods. */ closePosition(symbol, brokerKey) { return this.uiManager.closePosition(symbol, brokerKey); } /** * Delegate symbol-wide order cancellation to the UI manager. * Kept here because DOM actions call UI.trade.* methods. */ cancelOrdersForSymbol(symbol, brokerKey) { return this.uiManager.cancelOrdersForSymbol(symbol, brokerKey); } /** * Delegate paper balance reset to the UI manager. * Kept here because DOM actions call UI.trade.* methods. */ resetPaperBalance() { return this.uiManager.resetPaperBalance(); } /** * Updates the trading pair display in the form. * @private */ _updateTradingPairDisplay() { const symbolDisplay = document.getElementById('trade_symbol_display'); if (symbolDisplay && this.data?.trading_pair) { symbolDisplay.textContent = this.data.trading_pair; } } /** * Handle initial trades list response from server. * @param {Array} data - List of trade objects. */ handleTradesResponse(data) { console.log("Received trades list:", data); if (Array.isArray(data)) { this.dataManager.setTrades(data); this.uiManager.updateTradesHtml(this.dataManager.getAllTrades()); this._updateStatistics(); } } /** * Handle new trade created event. * @param {Object} data - Server response with trade data. */ handleTradeCreated(data) { console.log("Trade created:", data); if (data.success) { this.dataManager.addTrade(data); this.uiManager.updateTradesHtml(this.dataManager.getAllTrades()); this._updateStatistics(); // Also refresh the new broker UI panels (Orders, Positions, History) this.refreshAll(); } else { alert(`Failed to create trade: ${data.message}`); } } /** * Handle trade closed event. * @param {Object} data - Server response with closed trade info. */ handleTradeClosed(data) { console.log("Trade closed:", data); if (data.success) { const tradeId = data.trade_id; this.dataManager.removeTrade(tradeId); this.uiManager.removeTradeCard(tradeId); this._updateStatistics(); // Also refresh the new broker UI panels this.refreshAll(); // Show P/L notification if (data.final_pl !== undefined) { const plText = data.final_pl >= 0 ? `+$${data.final_pl.toFixed(2)}` : `-$${Math.abs(data.final_pl).toFixed(2)}`; console.log(`Trade closed with P/L: ${plText}`); } } } /** * Handle position-closed event from the REST close-position flow. * @param {Object} data - Position close payload with affected trade IDs. */ handlePositionClosed(data) { console.log("Position closed event received:", data); const closedTrades = Array.isArray(data?.closed_trades) ? data.closed_trades : []; for (const tradeId of closedTrades) { this.dataManager.removeTrade(tradeId); } this.uiManager.updateTradesHtml(this.dataManager.getAllTrades()); this._updateStatistics(); this.uiManager.refreshAll(); } /** * Handle trade error. * @param {Object} data - Error data. */ handleTradeError(data) { console.error("Trade error:", data.message); alert(`Trade error: ${data.message}`); } /** * Handle updates (including trade P/L updates). * @param {Object} data - Update data from server. */ handleUpdates(data) { const { trade_updts } = data; if (trade_updts && Array.isArray(trade_updts)) { this.dataManager.applyUpdates(trade_updts); // Update UI for each trade for (const update of trade_updts) { const tradeId = update.id; if (update.pl !== undefined && update.pl_pct !== undefined) { this.uiManager.updateTradePL(tradeId, update.pl, update.pl_pct); } } this._updateStatistics(); } } /** * Handle individual trade update from execution loop. * @param {Object} data - Single trade update data from server. */ handleTradeUpdate(data) { console.log('Trade update event received:', data); if (data && data.id) { // Update the trade in data manager this.dataManager.applyUpdates([data]); // Update legacy trade card UI if (data.pl !== undefined && data.pl_pct !== undefined) { this.uiManager.updateTradePL(data.id, data.pl, data.pl_pct); } // Also update position cards (for filled positions showing unrealized P/L) // Match by symbol + broker_key to avoid cross-broker contamination if (data.symbol && (data.pl !== undefined || data.current_price !== undefined)) { this._updatePositionPL(data.symbol, data.broker_key, data.pl, data.current_price); } this._updateStatistics(); } } /** * Update P/L display in position cards for a given symbol + broker. * @param {string} symbol - The trading symbol. * @param {string} brokerKey - The broker key ('paper' or 'exchange_mode'). * @param {number} pl - The unrealized P/L. * @param {number} currentPrice - The current price. */ _updatePositionPL(symbol, brokerKey, pl, currentPrice) { const container = document.getElementById('positionsContainer'); if (!container) return; // Find position card matching this symbol AND broker_key const cards = container.querySelectorAll('.position-card'); for (const card of cards) { const symbolEl = card.querySelector('.position-symbol'); const brokerEl = card.querySelector('.position-broker'); // Match both symbol and broker_key to avoid cross-broker contamination const symbolMatch = symbolEl && symbolEl.textContent === symbol; const brokerMatch = !brokerKey || (brokerEl && brokerEl.textContent === brokerKey); if (symbolMatch && brokerMatch) { const plEl = card.querySelector('.position-pl'); if (plEl && pl !== undefined) { const plClass = pl >= 0 ? 'positive' : 'negative'; const plSign = pl >= 0 ? '+' : ''; plEl.textContent = `${plSign}${pl.toFixed(2)}`; plEl.className = `position-pl ${plClass}`; // Flash animation plEl.classList.add('trade-pl-flash'); setTimeout(() => plEl.classList.remove('trade-pl-flash'), 300); } } } } // ================ Form Methods ================ /** * Opens the trade creation form. */ async open_tradeForm() { // Get current price if available let currentPrice = null; if (this.data?.price_history && this.data.price_history.length > 0) { currentPrice = this.data.price_history[this.data.price_history.length - 1].close; } // Get symbol from chart const symbol = this.data?.trading_pair || ''; // Get current chart view exchange const chartExchange = this.data?.exchange || 'binance'; // Get connected exchanges from UI.exchanges const connectedExchanges = window.UI?.exchanges?.connected_exchanges || []; await this.uiManager.displayForm(currentPrice, symbol, connectedExchanges, chartExchange); } /** * Sets the symbol dropdown to the current chart symbol. */ async useChartSymbol() { if (this.uiManager.symbolInput && this.data?.trading_pair) { const chartExchange = this.data?.exchange || 'binance'; // Re-populate dropdown with chart symbol at top await this.uiManager._populateSymbolDropdown(chartExchange, this.data.trading_pair); } } /** * Closes the trade form. */ close_tradeForm() { this.uiManager.hideForm(); } /** * Submits a new trade. */ submitNewTrade() { if (!this.comms) { console.error("Comms instance not available."); return; } const formElement = this.uiManager.formElement; if (!formElement) { console.error("Form element not available."); return; } const target = this.uiManager.targetSelect?.value || 'test_exchange'; const symbol = this.uiManager.symbolInput?.value || this.data?.trading_pair || ''; const orderType = this.uiManager.orderTypeSelect?.value || 'MARKET'; const side = this.uiManager.sideSelect?.value || 'buy'; const exchange = this.uiManager.exchangeSelect?.value || this.data?.exchange || 'binance'; // Get testnet setting (only relevant for live exchanges) const isPaperTrade = target === 'test_exchange'; const testnet = isPaperTrade ? false : (this.uiManager.testnetCheckbox?.checked ?? true); let price; if (orderType === 'MARKET') { price = parseFloat(this.uiManager.currentPriceDisplay?.value || 0); } else { price = parseFloat(this.uiManager.priceInput?.value || 0); } const quantity = parseFloat(this.uiManager.qtyInput?.value || 0); // Get SL/TP and TIF const stopLossVal = this.uiManager.stopLossInput?.value; const takeProfitVal = this.uiManager.takeProfitInput?.value; const stopLoss = stopLossVal ? parseFloat(stopLossVal) : null; const takeProfit = takeProfitVal ? parseFloat(takeProfitVal) : null; const timeInForce = this.uiManager.timeInForceSelect?.value || 'GTC'; // Validation if (!symbol) { alert('Please enter a trading pair.'); return; } if (quantity <= 0) { alert('Please enter a valid quantity.'); return; } if (orderType === 'LIMIT' && price <= 0) { alert('Please enter a valid price for limit orders.'); return; } // SL/TP validation (paper BUY only) if (isPaperTrade && side.toUpperCase() === 'BUY') { if (stopLoss && stopLoss >= price) { alert('Stop Loss must be below entry price for BUY orders.'); return; } if (takeProfit && takeProfit <= price) { alert('Take Profit must be above entry price for BUY orders.'); return; } } else if (!isPaperTrade && (stopLoss || takeProfit)) { alert('Manual live Stop Loss / Take Profit is not supported yet.'); return; } // Show confirmation for production live trades if (!isPaperTrade && !testnet) { const proceed = confirm( "WARNING: PRODUCTION MODE\n\n" + "You are about to execute a LIVE trade with REAL MONEY.\n\n" + `Exchange: ${target}\n` + `Symbol: ${symbol}\n` + `Side: ${side.toUpperCase()}\n` + `Quantity: ${quantity}\n\n` + "Are you sure you want to proceed?" ); if (!proceed) { return; } } const tradeData = { target, exchange, symbol, price, side, orderType, quantity, testnet, stopLoss, takeProfit, timeInForce, user_name: this.data?.user_name }; console.log("Submitting trade:", tradeData); this.comms.sendToApp('new_trade', tradeData); this.close_tradeForm(); } /** * Closes a trade. * @param {string} tradeId - The trade ID. */ closeTrade(tradeId) { if (!this.comms) { console.error("Comms instance not available."); return; } console.log(`Closing trade: ${tradeId}`); this.comms.sendToApp('close_trade', { trade_id: tradeId }); } /** * Fetches trades from server. */ fetchTrades() { this.dataManager.fetchTrades(this.comms, this.data); } /** * Updates the statistics panel with trade data. * @private */ _updateStatistics() { const stats = this.dataManager.getStatistics(); // Update statistics panel if available const activeTradesEl = document.getElementById('stat_active_trades'); const totalPLEl = document.getElementById('stat_trades_pl'); const paperCountEl = document.getElementById('stat_paper_trades'); const liveCountEl = document.getElementById('stat_live_trades'); if (activeTradesEl) { activeTradesEl.textContent = stats.totalTrades; } if (totalPLEl) { const plClass = stats.totalPL >= 0 ? 'positive' : 'negative'; totalPLEl.className = `stat-value ${plClass}`; totalPLEl.textContent = stats.totalPL >= 0 ? `+$${stats.totalPL.toFixed(2)}` : `-$${Math.abs(stats.totalPL).toFixed(2)}`; } if (paperCountEl) { paperCountEl.textContent = stats.paperTrades; } if (liveCountEl) { liveCountEl.textContent = stats.liveTrades; } } // ================ Legacy Methods (for backwards compatibility) ================ /** * Legacy method: Connect elements (calls initUI). */ connectElements(config = {}) { this.uiManager.initUI({ priceId: config.price || 'price', currentPriceId: config.currentPrice || 'currentPrice', qtyId: config.quantity || 'quantity', tradeValueId: config.tradeValue || 'tradeValue', orderTypeId: config.orderType || 'orderType', tradeTargetId: config.target || 'tradeTarget', sideId: config.side || 'side', formElId: config.ntf || 'new_trade_form', targetId: config.atl || 'tradesContainer' }); } /** * Legacy method: Set data (handles trades response). * @param {Array} trades - List of trades. */ set_data(trades) { this.handleTradesResponse(trades); } /** * Legacy method: Update received (handles updates). * @param {Array} trade_updts - Trade updates. */ update_received(trade_updts) { this.handleUpdates({ trade_updts }); } /** * Legacy: Open trade details form. */ open_tradeDetails_Form() { const el = document.getElementById("trade_details_form"); if (el) el.style.display = "grid"; } /** * Legacy: Close trade details form. */ close_tradeDetails_Form() { const el = document.getElementById("trade_details_form"); if (el) el.style.display = "none"; } /** * Gets all trades. * @returns {Object[]} - The list of trades. */ get trades() { return this.dataManager.getAllTrades(); } }