1998 lines
71 KiB
JavaScript
1998 lines
71 KiB
JavaScript
/**
|
|
* 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 = `<span class="trade-label">Qty:</span><span class="trade-value">${this._formatNumber(trade.base_order_qty)}</span>`;
|
|
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 = `<span class="trade-label">Entry:</span><span class="trade-value">${this._formatPrice(entryPrice)}</span>`;
|
|
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 = `<span class="trade-label">P/L:</span><span class="trade-value trade-pl ${plClass}" id="${trade.unique_id}_pl_value">${this._formatPL(pl)} (${this._formatPct(plPct)})</span>`;
|
|
tradeInfo.appendChild(plRow);
|
|
|
|
tradeItem.appendChild(tradeInfo);
|
|
|
|
// Hover details panel
|
|
const hoverPanel = document.createElement('div');
|
|
hoverPanel.className = 'trade-hover';
|
|
|
|
let hoverHtml = `<strong>${trade.symbol}</strong>`;
|
|
hoverHtml += `<div class="trade-details">`;
|
|
hoverHtml += `<span>Side: ${side}</span>`;
|
|
hoverHtml += `<span>Type: ${trade.order_type || 'MARKET'}</span>`;
|
|
hoverHtml += `<span>Quantity: ${this._formatNumber(trade.base_order_qty)}</span>`;
|
|
hoverHtml += `<span>Entry Price: ${this._formatPrice(entryPrice)}</span>`;
|
|
hoverHtml += `<span>Current Value: ${this._formatPrice(stats.current_value || 0)}</span>`;
|
|
hoverHtml += `<span class="${plClass}">P/L: ${this._formatPL(pl)} (${this._formatPct(plPct)})</span>`;
|
|
hoverHtml += `<span>Status: ${trade.status || 'unknown'}</span>`;
|
|
// 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 += `<span>Exchange: ${displayExchange}</span>`;
|
|
if (trade.is_paper) {
|
|
hoverHtml += `<span class="trade-paper-indicator">Paper Trade</span>`;
|
|
} else if (trade.testnet) {
|
|
hoverHtml += `<span class="trade-testnet-indicator" style="color: #28a745;">Testnet</span>`;
|
|
} else {
|
|
hoverHtml += `<span class="trade-prod-indicator" style="color: #dc3545;">Production (Live)</span>`;
|
|
}
|
|
hoverHtml += `</div>`;
|
|
|
|
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 = '<p class="no-data-msg">No open orders</p>';
|
|
return;
|
|
}
|
|
|
|
const table = document.createElement('table');
|
|
table.className = 'orders-table';
|
|
table.innerHTML = `
|
|
<tr>
|
|
<th>Symbol</th>
|
|
<th>Side</th>
|
|
<th>Size</th>
|
|
<th>Price</th>
|
|
<th>Broker</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
`;
|
|
|
|
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 = `
|
|
<td>${order.symbol || 'N/A'}</td>
|
|
<td>${(order.side || '').toUpperCase()}</td>
|
|
<td>${this._formatNumber(order.size)}</td>
|
|
<td>${order.price ? this._formatPrice(order.price) : 'MARKET'}</td>
|
|
<td>${order.broker_key || 'paper'}</td>
|
|
<td>
|
|
<button class="btn-cancel" onclick="UI.trade.cancelOrder('${order.order_id}', '${order.broker_key || 'paper'}')">
|
|
Cancel
|
|
</button>
|
|
</td>
|
|
`;
|
|
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 = '<p class="no-data-msg">No open positions</p>';
|
|
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 = `
|
|
<div class="position-header">
|
|
<span class="position-symbol">${position.symbol || 'N/A'}</span>
|
|
<span class="position-broker">${position.broker_key || 'paper'}</span>
|
|
</div>
|
|
<div class="position-details">
|
|
<div class="position-row">
|
|
<span>Size:</span>
|
|
<span>${this._formatNumber(position.size)}</span>
|
|
</div>
|
|
<div class="position-row">
|
|
<span>Entry:</span>
|
|
<span>${position.entry_price ? this._formatPrice(position.entry_price) : '-'}</span>
|
|
</div>
|
|
<div class="position-row">
|
|
<span>P/L:</span>
|
|
<span class="position-pl ${plClass}">${plSign}${pl.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="position-actions">
|
|
<button class="btn-close-position" onclick="UI.trade.closePosition('${position.symbol}', '${position.broker_key || 'paper'}')">
|
|
Close Position
|
|
</button>
|
|
<button class="btn-cancel-orders" onclick="UI.trade.cancelOrdersForSymbol('${position.symbol}', '${position.broker_key || 'paper'}')" title="Cancel resting orders">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
`;
|
|
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 = '<p class="no-data-msg">No trade history</p>';
|
|
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 = `
|
|
<span class="trade-side ${side.toLowerCase()}">${side}</span>
|
|
<span class="trade-symbol">${trade.symbol || 'N/A'}</span>
|
|
`;
|
|
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 += `
|
|
<div class="trade-row">
|
|
<span class="trade-label">Qty:</span>
|
|
<span class="trade-value">${this._formatNumber(qty)}</span>
|
|
</div>
|
|
<div class="trade-row">
|
|
<span class="trade-label">Price:</span>
|
|
<span class="trade-value">${this._formatPrice(settledPrice)}</span>
|
|
</div>
|
|
<div class="trade-row">
|
|
<span class="trade-label">P/L:</span>
|
|
<span class="trade-pl ${profitClass}">${profitSign}${profit.toFixed(2)}</span>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
}
|
|
}
|