/**
 * WebTrader JavaScript Application
 * Maneja gráficas, órdenes y actualizaciones en tiempo real
 */

class WebTraderApp {
    constructor(cfg = {}) {
        this.charts = {};
        this.positions = [];
        this.orders = [];
        this.instruments = [];
        this.accountData = {};
        this.websocket = null;
        this.wsClient = null;
        this.updateInterval = null;
        this.demoMode = Boolean((cfg && cfg.demo_mode) || (typeof window !== 'undefined' && window.webtraderConfig && window.webtraderConfig.demo_mode));
        this.activeAccountNumber = null; // Cuenta activa del CRM/WebTrader
        this.favorites = [];
        this.favoritesFilterOn = false;
        this.currentSymbol = 'EURUSD';
        this.currentTimeframe = 1; // minutos
        this.baseMinuteCandles = [];
        this.marketWatchRowsBySymbol = {};
        this.pendingTicks = new Map();
        this.tickRaf = null;
        this.lastUpdateTimestamp = null;
        // Estado del feed de datos (Finage WS)
        this.feedLive = false;
        this.feedStatus = 'disconnected';
        // Intervalo de actualización de cuenta (balance/equity/margen)
        this.accountUpdateInterval = null;
        // Estado UX avanzado para Market Watch
        this.mwSortKey = (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwSortKey')) || 'symbol';
        this.mwSortDir = (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwSortDir')) || 'asc';
        this.mwSearch = (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwSearch')) || '';
        this.mwFilters = {
            moving: (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwFiltMoving') === '1'),
            lowSpread: (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwFiltLowSpread') === '1')
        };
        this.mwDensity = (typeof localStorage !== 'undefined' && localStorage.getItem('wtMwDensity')) || 'comfortable';
        this.lastTickBySymbol = {};
        this.lastPriceBySymbol = {};
        this.lastPnlByPositionId = {};
        this.lowSpreadThresholdPips = 1.5;
        this.searchAliases = {
            'oro': ['XAUUSD','GOLD'],
            'gold': ['XAUUSD','GOLD'],
            'petroleo': ['WTI','BRENT','OIL'],
            'oil': ['WTI','BRENT','OIL'],
            'euro': ['EURUSD','EUR'],
            'yen': ['USDJPY','JPY']
        };
        this.defaultFavorites = ['EURUSD','GBPUSD','USDJPY','XAUUSD','NDX','AMZN'];
        // Cachear handler para cerrar modales con ESC y poder removerlo correctamente
        this.boundHandleModalKeydown = this.handleModalKeydown.bind(this);
        // Estado avanzado de interacción del gráfico
        this.currentMidPrice = null; // usado por la línea de precio
        this.viewRange = { fullMin: undefined, fullMax: undefined, min: undefined, max: undefined };
        // Seguir y centrar en la última vela por defecto
        this.followLast = true;
        try {
            const savedBars = (typeof localStorage !== 'undefined') ? parseInt(localStorage.getItem('wtVisibleBars') || '0', 10) : 0;
            this.defaultVisibleBars = Number.isFinite(savedBars) && savedBars > 0 ? savedBars : 120;
        } catch (e) { this.defaultVisibleBars = 120; }
        this.isPanning = false;
        this.panStartX = 0;
        this.panStartRange = null;
        this.candleCountdownTimer = null;
        this.chartHeartbeatTimer = null;
        this.heartbeatIntervalMs = 1000;
        
        this.init();
    }

    // Normalizar símbolos a una forma abreviada coherente (e.g., BTCUSD, EURUSD)
    normalizeSymbol(symbol) {
        const raw = String(symbol || '').toUpperCase();
        // quitar separadores comunes
        const s = raw.replace(/[^A-Z0-9]/g, '');
        if (!s) return '';
        // alias comunes
        const base = s.slice(0, 3);
        const quote = s.slice(3);
        const aliasBase = { 'XBT': 'BTC' };
        const b = aliasBase[base] || base;
        let out = b + quote;
        if (out === 'XBTUSD') out = 'BTCUSD';
        return out;
    }

    async init() {
        console.log('🚀 Inicializando WebTrader...');
        try {
            // Inicializar cuenta activa desde configuración o localStorage
            const cfgAccounts = (window.webtraderConfig && Array.isArray(window.webtraderConfig.accounts)) ? window.webtraderConfig.accounts : [];
            const savedAcc = (typeof localStorage !== 'undefined') ? localStorage.getItem('webtrader-active-account') : null;
            this.activeAccountNumber = savedAcc || (cfgAccounts[0] && cfgAccounts[0].account_number) || null;
            // Sincronizar selector si existe en DOM
            setTimeout(() => {
                const sel = document.getElementById('accountSelector');
                const selHeader = document.getElementById('accountSelectorHeader');
                if (sel && this.activeAccountNumber) {
                    sel.value = this.activeAccountNumber;
                }
                if (selHeader && this.activeAccountNumber) {
                    selHeader.value = this.activeAccountNumber;
                }
                if (sel) {
                    sel.addEventListener('change', (e) => {
                        const accNum = e.target.value || null;
                        this.setActiveAccount(accNum);
                    });
                }
                if (selHeader) {
                    selHeader.addEventListener('change', (e) => {
                        const accNum = e.target.value || null;
                        this.setActiveAccount(accNum);
                        // Mantener sincronizado el selector del modal si existe
                        if (sel) sel.value = accNum;
                    });
                }
            }, 0);
        } catch (e) {
            console.warn('No se pudo leer configuración inicial');
        }
        try { this.setDefaultFavoritesIfEmpty(); } catch (_) {}
        try {
            if (Array.isArray(this.favorites) && this.favorites.length) {
                const f0 = this.normalizeSymbol(this.favorites[0]);
                if (f0 && typeof f0 === 'string') this.currentSymbol = f0;
            }
        } catch (_) {}
        
        // Inicializar gráfico lo antes posible y cargar datos en paralelo
        const pInitChart = this.initializeCharts();
        const pLoad = this.loadInitialData();
        await pLoad;
        await pInitChart;
        try { this.preCachePaymentMethods((this.accountData?.account?.currency || 'USD')); } catch(_) {}
        this.initializeMarketWatch();
        // Fail-safe: si Market Watch quedó vacío, renderizar con lista base
        try {
            const listEl = document.getElementById('mwList');
            const catCount = listEl ? listEl.querySelectorAll('.mw-category').length : 0;
            if (listEl && catCount === 0) {
                if (!Array.isArray(this.instruments) || this.instruments.length === 0) {
                    const favs = Array.isArray(this.favorites) && this.favorites.length ? this.favorites.map(s=>this.normalizeSymbol(s)) : this.defaultFavorites;
                    this.instruments = favs.map(s => ({ symbol:s, name:s, category:this.getCategoryForSymbol(s) }));
                }
                this.updateMarketWatchTable(this.instruments);
            }
        } catch (e) {}
        this.initializeOrderPanel();
        this.initializePositionsPanel();
        this.initializeEventListeners();
        this.initializeUserMenu(); // Nueva función para el menú de usuario
        this.initializeDepositWithdraw(); // Nueva función para depósito/retiro
        this.initializeHeaderSearch(); // Mini buscador en el header
        // Estado de datos y accesibilidad
        this.updateDataSourceLabel();
        this.updateStatusBar();
        this.setupKeyboardShortcuts();
        this.setupCopyable();
        
        // Iniciar actualizaciones automáticas
        this.startAutoUpdates();
        
        console.log('✅ WebTrader inicializado correctamente');
    }

    /**
     * Establecer cuenta activa y recargar datos
     */
    setActiveAccount(accountNumber) {
        this.activeAccountNumber = accountNumber || null;
        try {
            if (typeof localStorage !== 'undefined') {
                if (this.activeAccountNumber) {
                    localStorage.setItem('webtrader-active-account', this.activeAccountNumber);
                } else {
                    localStorage.removeItem('webtrader-active-account');
                }
            }
        } catch (e) {}
        this.showNotification('Cuenta activa actualizada', 'info');
        this.loadInitialData();
    }

    /**
     * Configurar cliente WebSocket
     */
    setWebSocketClient(wsClient) {
        this.wsClient = wsClient;
        // Inicializar registro de suscripciones de precios
        if (!this._priceSubsMap) this._priceSubsMap = new Map();
        
        // Configurar eventos WebSocket
        this.wsClient.on('price_update', (data) => {
            this.handlePriceUpdate(data);
        });
        
        this.wsClient.on('account_update', (data) => {
            this.handleAccountUpdate(data);
        });
        
        this.wsClient.on('order_update', (data) => {
            this.handleOrderUpdate(data);
        });
        
        this.wsClient.on('position_update', (data) => {
            this.handlePositionUpdate(data);
        });

        // Velas: snapshot inicial y actualizaciones en tiempo real desde el servidor
        this.wsClient.on('candle_snapshot', (data) => {
            this.applyCandleSnapshot(data);
        });
        this.wsClient.on('candle_update', (data) => {
            this.handleCandleUpdate(data);
        });

        // Estado del feed Finage: conectar/desconectar/errores
        this.wsClient.on('feed_status', (evt) => {
            try {
                const status = (evt && evt.status) || 'unknown';
                this.feedStatus = status;
                this.feedLive = status === 'connected';
                this.updateDataSourceLabel();
                const msg = this.feedLive ? 'Conectado a datos LIVE (Finage WS)' : `Estado del feed: ${status}`;
                this.showNotification(msg, this.feedLive ? 'success' : 'warning');
                // Reconfigurar actualizaciones según estado del feed
                this.startAutoUpdates();
            } catch (e) { console.warn('Error manejando feed_status:', e); }
        });

        // Alternar auto-actualizaciones según estado del socket
        this.wsClient.on('connected', () => {
            try {
                this.startAutoUpdates();
                this.updatePriceSubscriptions();
                if (this.currentSymbol && this.currentTimeframe) {
                    this.subscribeCandles(this.currentSymbol, this.currentTimeframe);
                }
            } catch (e) {}
        });
        this.wsClient.on('disconnected', () => {
            try { this.startAutoUpdates(); } catch (e) {}
        });

        // Suscribir tras autenticación: velas, cuenta y precios
        this.wsClient.on('authenticated', () => {
            try {
                // Velas del símbolo/timeframe actuales
                this.subscribeCandles(this.currentSymbol, this.currentTimeframe);
                // Datos de cuenta
                if (this.activeAccountNumber) {
                    this.wsClient.subscribe('account_update', this.activeAccountNumber, null);
                } else {
                    this.wsClient.subscribe('account_update', null, null);
                }
                // Precios: suscribirse según favoritos + símbolo actual
                this.updatePriceSubscriptions();
            } catch (e) { console.warn('No se pudo suscribir tras autenticación:', e); }
        });

        // En modo invitado, iniciar suscripciones básicas al conectar
        this.wsClient.on('guest', () => {
            try {
                // Suscribir sólo a favoritos (si hay) y garantizar símbolo activo
                this.updatePriceSubscriptions();
                if (this.currentSymbol && this.currentTimeframe) {
                    this.subscribeCandles(this.currentSymbol, this.currentTimeframe);
                }
            } catch (e) { console.warn('No se pudo inicializar suscripciones de invitado:', e); }
        });
        
        console.log('🔌 WebSocket client configurado');
    }

    setWebSocketClients(wsClients) {
        this.wsClients = Array.isArray(wsClients) ? wsClients : [];
        if (this.wsClients.length) {
            this.wsClient = this.wsClients[0];
        }
        if (!this._priceSubsMap) this._priceSubsMap = new Map();
        const attach = (client) => {
            client.on('price_update', (data) => { this.handlePriceUpdate(data); });
            client.on('account_update', (data) => { this.handleAccountUpdate(data); });
            client.on('order_update', (data) => { this.handleOrderUpdate(data); });
            client.on('position_update', (data) => { this.handlePositionUpdate(data); });
            client.on('candle_snapshot', (data) => { this.applyCandleSnapshot(data); });
            client.on('candle_update', (data) => { this.handleCandleUpdate(data); });
            client.on('feed_status', (evt) => {
                try {
                    const status = (evt && evt.status) || 'unknown';
                    this.feedStatus = status;
                    this.feedLive = status === 'connected';
                    this.updateDataSourceLabel();
                    const msg = this.feedLive ? 'Conectado a datos LIVE (Finage WS)' : `Estado del feed: ${status}`;
                    this.showNotification(msg, this.feedLive ? 'success' : 'warning');
                    this.startAutoUpdates();
                } catch (e) {}
            });
            client.on('connected', () => {
                try {
                    this.startAutoUpdates();
                    // Auto-suscribir lista propia del cliente si existe
                    const syms = Array.isArray(client.__symbols) ? client.__symbols : [];
                    syms.forEach(s => { try { client.subscribe('prices', s); } catch (e) {} });
                    this.updatePriceSubscriptions();
                    if (this.currentSymbol && this.currentTimeframe) this.subscribeCandles(this.currentSymbol, this.currentTimeframe);
                } catch (e) {}
            });
            client.on('disconnected', () => { try { this.startAutoUpdates(); } catch (e) {} });
            client.on('authenticated', () => {
                try {
                    if (this.currentSymbol && this.currentTimeframe) this.subscribeCandles(this.currentSymbol, this.currentTimeframe);
                    if (this.activeAccountNumber) client.subscribe('account_update', this.activeAccountNumber, null);
                    this.updatePriceSubscriptions();
                } catch (e) {}
            });
            client.on('guest', () => { try { this.updatePriceSubscriptions(); if (this.currentSymbol && this.currentTimeframe) this.subscribeCandles(this.currentSymbol, this.currentTimeframe); } catch (e) {} });
        };
        this.wsClients.forEach(attach);
        console.log('🔌 WebSocket multi-client configurado:', this.wsClients.length);
    }

    // Suscribir a canal de velas para símbolo/timeframe actuales
    subscribeCandles(symbol, timeframe) {
        if (!this.wsClient) return;
        // Cancelar suscripción anterior si existe
        if (this._candleSub && (this._candleSub.symbol || this._candleSub.timeframe)) {
            try { this.wsClient.unsubscribe('candle_update', this._candleSub.symbol, this._candleSub.timeframe); } catch (e) {}
        }
        this._candleSub = { symbol, timeframe };
        this.wsClient.subscribe('candle_update', symbol, timeframe);
    }

    // Aplicar snapshot inicial de velas desde servidor
    applyCandleSnapshot(data) {
        const { symbol, timeframe, candles } = data || {};
        if (!symbol || !timeframe || !Array.isArray(candles)) return;
        if (symbol !== this.currentSymbol || timeframe !== this.currentTimeframe) return;
        if (!this.charts.main) return;
        const chart = this.charts.main;
        const mapped = candles.map(c => ({ x: c.x, o: c.o, h: c.h, l: c.l, c: c.c }));
        if (chart.config.type === 'candlestick') {
            chart.data.datasets[0].label = symbol;
            chart.data.datasets[0].data = mapped;
            chart.update('none');
        } else {
            chart.data.datasets[0].label = symbol;
            chart.data.labels = mapped.map(c => new Date(c.x));
            chart.data.datasets[0].data = mapped.map(c => c.c);
            chart.update('none');
        }
        // Centrar vista en la última vela del snapshot
        this.followLast = true;
        this.setViewRangeCenteredOnLast(mapped);
        // Sincronizar buffer base de minutos con snapshot si timeframe=1
        if (timeframe === 1) {
            this.baseMinuteCandles = mapped.map(c => ({ x: c.x, o: c.o, h: c.h, l: c.l, c: c.c }));
        }
        this.lastUpdateTimestamp = Date.now();
        this.updateStatusBar();
    }

    // Manejar actualización de vela en tiempo real
    handleCandleUpdate(data) {
        const { symbol, timeframe, candle } = data || {};
        if (!symbol || !timeframe || !candle) return;
        if (symbol !== this.currentSymbol || timeframe !== this.currentTimeframe) return;
        if (!this.charts.main) return;
        const chart = this.charts.main;
        const bar = { x: candle.time, o: candle.open, h: candle.high, l: candle.low, c: candle.close };
        let arr;
        if (chart.config.type === 'candlestick') {
            arr = chart.data.datasets[0].data;
        } else {
            arr = chart.data.datasets[0].data.map((v, i) => ({ x: chart.data.labels[i].getTime(), o: v, h: v, l: v, c: v }));
        }
        const last = arr[arr.length - 1];
        if (last && last.x === bar.x) {
            arr[arr.length - 1] = bar;
        } else {
            arr.push(bar);
            // Límite de barras visibles
            if (arr.length > 200) arr.shift();
        }
        if (chart.config.type === 'candlestick') {
            chart.data.datasets[0].data = arr;
        } else {
            chart.data.labels = arr.map(c => new Date(c.x));
            chart.data.datasets[0].data = arr.map(c => c.c);
        }
        // Si estamos siguiendo la última vela, mantenerla centrada
        if (this.followLast) {
            this.setViewRangeCenteredOnLast(arr);
        } else {
            chart.update('none');
        }
        this.lastUpdateTimestamp = Date.now();
        this.updateStatusBar();
    }

    /**
     * Manejar actualizaciones de precios en tiempo real
     */
    handlePriceUpdate(data) {
        const { symbol, bid, ask, change, change_percent, source, timestamp } = data;
        const norm = (typeof symbol === 'string') ? symbol.replace('/', '') : symbol;
        this.enqueueTick({ symbol: norm, bid, ask, change, changePercent: change_percent, source, timestamp });
        console.log(`📈 Tick recibido: ${symbol} - Bid: ${bid}, Ask: ${ask} (${source})`);
    }

    /**
     * Manejar actualizaciones de cuenta
     */
    handleAccountUpdate(data) {
        this.accountData = { ...this.accountData, ...data.data };
        this.updateAccountInfo();
        console.log('💰 Datos de cuenta actualizados:', data.data);
    }

    /**
     * Manejar actualizaciones de órdenes
     */
    handleOrderUpdate(data) {
        // Recargar órdenes
        this.loadOrders();
        console.log('📋 Órdenes actualizadas:', data);
    }

    /**
     * Manejar actualizaciones de posiciones
     */
    handlePositionUpdate(data) {
        // Recargar posiciones
        this.loadPositions();
        console.log('📊 Posiciones actualizadas:', data);
    }

    async loadInitialData() {
        try {
            const params = new URLSearchParams();
            if (this.activeAccountNumber) params.set('account_number', this.activeAccountNumber);
            const qp = params.toString() ? `?${params.toString()}` : '';
            // Restaurar símbolo/timeframe guardados
            try {
                const savedSymbol = (typeof localStorage !== 'undefined') ? localStorage.getItem('wtSymbol') : null;
                if (savedSymbol && typeof savedSymbol === 'string' && savedSymbol.length >= 6) {
                    this.currentSymbol = this.normalizeSymbol(savedSymbol);
                    const symEl = document.getElementById('current-symbol');
                    if (symEl) symEl.textContent = this.currentSymbol;
                    this.renderSymbolFlags(this.currentSymbol);
                }
            } catch (_) {}
            
            this.toggleMarketWatchLoader(true);
            this.togglePositionsLoader(true);
            this.toggleOrdersLoader(true);
            // Render inmediato con placeholders completos si no hay instrumentos
            if (!Array.isArray(this.instruments) || this.instruments.length === 0) {
                this.instruments = [
                    // Forex solicitados
                    'EURUSD','GBPUSD','USDJPY','USDCHF','AUDUSD','USDCAD',
                    'NZDUSD','EURGBP','EURJPY','GBPJPY','USDTRY','USDZAR','USDMXN','USDINR','USDBRL','EURTRY','GBPZAR','AUDMXN','USDPLN','USDRUB'
                ].map(s => ({ symbol:s, name:s, category:'forex' }));
                // Acciones solicitadas
                this.instruments.push(
                    { symbol:'AMD', name:'AMD', category:'stocks' },
                    { symbol:'AMZN', name:'Amazon', category:'stocks' },
                    { symbol:'META', name:'Meta', category:'stocks' },
                    { symbol:'GOOGL', name:'Alphabet', category:'stocks' },
                    { symbol:'JPM', name:'JPMorgan', category:'stocks' },
                    { symbol:'BAC', name:'Bank of America', category:'stocks' },
                    { symbol:'WFC', name:'Wells Fargo', category:'stocks' },
                    { symbol:'C', name:'Citigroup', category:'stocks' },
                    { symbol:'GS', name:'Goldman Sachs', category:'stocks' },
                    { symbol:'MS', name:'Morgan Stanley', category:'stocks' },
                    { symbol:'BLK', name:'BlackRock', category:'stocks' }
                );
                // Índices
                this.instruments.push(
                    { symbol:'SPX', name:'S&P 500', category:'indices' },
                    { symbol:'NDX', name:'Nasdaq 100', category:'indices' },
                    { symbol:'DJI', name:'Dow Jones', category:'indices' },
                    { symbol:'FTSE', name:'FTSE 100', category:'indices' },
                    { symbol:'GDAXI', name:'DAX', category:'indices' },
                    { symbol:'N225', name:'Nikkei 225', category:'indices' },
                    { symbol:'HSI', name:'Hang Seng', category:'indices' },
                    { symbol:'FCHI', name:'CAC 40', category:'indices' }
                );
                // Materias Primas
                this.instruments.push(
                    { symbol:'XAUUSD', name:'Oro', category:'commodities' },
                    { symbol:'XAGUSD', name:'Plata', category:'commodities' },
                    { symbol:'XPDUSD', name:'Paladio', category:'commodities' },
                    { symbol:'USOIL', name:'WTI (USOIL)', category:'commodities' },
                    { symbol:'WTIUSD', name:'WTIUSD', category:'commodities' },
                    { symbol:'XBRUSD', name:'Brent', category:'commodities' },
                    { symbol:'UKOILUSD', name:'UKOILUSD', category:'commodities' },
                    { symbol:'COPPERUSD', name:'Cobre', category:'commodities' },
                    { symbol:'CORNUSD', name:'Maíz', category:'commodities' },
                    { symbol:'SUGARUSD', name:'Azúcar', category:'commodities' }
                );
                try { this.updateMarketWatchTable(this.instruments); } catch (_) {}
                try { this.updatePriceSubscriptions(); } catch (_) {}
            }
            // Cargar datos de la cuenta
            const accountResponse = await fetch(`api/account.php${qp}`);
            const accountJson = await accountResponse.json().catch(() => ({}));
            this.accountData = accountJson?.data || accountJson || {};
            // Sin datos de backend, mantener estructura vacía sin simulación
            if (!this.accountData || (!this.accountData.account && !('balance' in this.accountData))) {
                this.accountData = { account: {} };
            }
            const accRaw = this.accountData?.account || this.accountData || {};
            const accNum = accRaw.account_number || accRaw.accountNumber || null;
            if (!this.activeAccountNumber && accNum) {
                this.activeAccountNumber = accNum;
                try {
                    if (typeof localStorage !== 'undefined') localStorage.setItem('webtrader-active-account', this.activeAccountNumber);
                } catch (e) {}
                // sincronizar selectores si existen
                const sel = document.getElementById('accountSelector');
                const selHeader = document.getElementById('accountSelectorHeader');
                if (sel) sel.value = this.activeAccountNumber;
                if (selHeader) selHeader.value = this.activeAccountNumber;
            }

            // Estados premium de gráfica
            try {
                this.autoScaleOn = (typeof localStorage !== 'undefined') ? (localStorage.getItem('wtAutoScaleOn') !== '0') : true; // por defecto activado
            } catch (e) { this.autoScaleOn = true; }
            this.fixedYRange = null; // { min, max } cuando autoScale está apagado
            try {
                this.showCrosshair = (typeof localStorage !== 'undefined') ? (localStorage.getItem('wtShowCrosshair') === '1') : false;
            } catch (e) { this.showCrosshair = false; }
            this.crosshairX = null; // píxel X relativo al canvas para dibujar overlay
    
            
            // Cargar instrumentos (fast)
            const fastQp = qp ? `${qp}&fast=1` : '?fast=1';
            const instrumentsResponse = await fetch(`api/instruments.php${fastQp}`);
            const ict = (instrumentsResponse.headers && instrumentsResponse.headers.get('Content-Type')) || '';
            let instrumentsJson = {};
            if (ict.indexOf('application/json') !== -1) {
                try { instrumentsJson = await instrumentsResponse.json(); } catch (_) { instrumentsJson = {}; }
            } else {
                try {
                    const txt = await instrumentsResponse.text();
                    try { instrumentsJson = JSON.parse(txt); } catch (_) { instrumentsJson = {}; }
                } catch (_) { instrumentsJson = {}; }
            }
            // Aceptar array o objeto con propiedad instruments
            const apiList = Array.isArray(instrumentsJson) ? instrumentsJson : (instrumentsJson.instruments || []);
            // Unir con la lista previa (placeholders) para no perder símbolos visibles
            const mergeBySymbol = (a, b) => {
                const map = new Map();
                const put = (it) => { if (!it || !it.symbol) return; const k = this.normalizeSymbol(it.symbol); map.set(k, { ...map.get(k), ...it }); };
                (Array.isArray(a)?a:[]).forEach(put);
                (Array.isArray(b)?b:[]).forEach(put);
                return Array.from(map.values());
            };
            this.instruments = mergeBySymbol(this.instruments, apiList);
            // Asegurar que no haya filtros persistentes que oculten instrumentos al inicio
            this.resetMwFilters();
            
            // Cargar posiciones
            const positionsResponse = await fetch(`api/positions.php${qp}`);
            const pct = (positionsResponse.headers && positionsResponse.headers.get('Content-Type')) || '';
            let positionsJson = [];
            if (pct.indexOf('application/json') !== -1) {
                try { positionsJson = await positionsResponse.json(); } catch (_) { positionsJson = []; }
            } else {
                try {
                    const txt = await positionsResponse.text();
                    try { positionsJson = JSON.parse(txt); } catch (_) { positionsJson = []; }
                } catch (_) { positionsJson = []; }
            }
            this.positions = Array.isArray(positionsJson) ? positionsJson : (positionsJson.positions || []);
            const stats = Array.isArray(positionsJson) ? null : (positionsJson.statistics || null);
            
            // Cargar órdenes pendientes
            const ordersResponse = await fetch(`api/orders.php${qp}`);
            const ordersJson = await ordersResponse.json().catch(() => []);
            this.orders = Array.isArray(ordersJson) ? ordersJson : (ordersJson.orders || []);
            
            // Sin datos simulados: si falla, mantener listas vacías
            if (!Array.isArray(this.instruments)) this.instruments = [];
            if (!Array.isArray(this.positions)) this.positions = [];
            if (!Array.isArray(this.orders)) this.orders = [];
            // Fallback: si no hay instrumentos tras llamar API, usar lista completa
            if (this.instruments.length === 0) {
                this.instruments = [
                    'EURUSD','GBPUSD','USDJPY','USDCHF','AUDUSD','USDCAD',
                    'NZDUSD','EURGBP','EURJPY','GBPJPY','USDTRY','USDZAR','USDMXN','USDINR','USDBRL','EURTRY','GBPZAR','AUDMXN','USDPLN','USDRUB'
                ].map(s => ({ symbol:s, name:s, category:'forex' }));
                this.instruments.push(
                    { symbol:'AMD', name:'AMD', category:'stocks' },
                    { symbol:'AMZN', name:'Amazon', category:'stocks' },
                    { symbol:'META', name:'Meta', category:'stocks' },
                    { symbol:'GOOGL', name:'Alphabet', category:'stocks' },
                    { symbol:'JPM', name:'JPMorgan', category:'stocks' },
                    { symbol:'BAC', name:'Bank of America', category:'stocks' },
                    { symbol:'WFC', name:'Wells Fargo', category:'stocks' },
                    { symbol:'C', name:'Citigroup', category:'stocks' },
                    { symbol:'GS', name:'Goldman Sachs', category:'stocks' },
                    { symbol:'MS', name:'Morgan Stanley', category:'stocks' },
                    { symbol:'BLK', name:'BlackRock', category:'stocks' }
                );
                this.instruments.push(
                    { symbol:'SPX', name:'S&P 500', category:'indices' },
                    { symbol:'NDX', name:'Nasdaq 100', category:'indices' },
                    { symbol:'DJI', name:'Dow Jones', category:'indices' },
                    { symbol:'FTSE', name:'FTSE 100', category:'indices' },
                    { symbol:'GDAXI', name:'DAX', category:'indices' },
                    { symbol:'N225', name:'Nikkei 225', category:'indices' },
                    { symbol:'HSI', name:'Hang Seng', category:'indices' },
                    { symbol:'FCHI', name:'CAC 40', category:'indices' }
                );
                this.instruments.push(
                    { symbol:'XAUUSD', name:'Oro', category:'commodities' },
                    { symbol:'XAGUSD', name:'Plata', category:'commodities' },
                    { symbol:'XPDUSD', name:'Paladio', category:'commodities' },
                    { symbol:'USOIL', name:'WTI (USOIL)', category:'commodities' },
                    { symbol:'WTIUSD', name:'WTIUSD', category:'commodities' },
                    { symbol:'XBRUSD', name:'Brent', category:'commodities' },
                    { symbol:'UKOILUSD', name:'UKOILUSD', category:'commodities' },
                    { symbol:'COPPERUSD', name:'Cobre', category:'commodities' },
                    { symbol:'CORNUSD', name:'Maíz', category:'commodities' },
                    { symbol:'SUGARUSD', name:'Azúcar', category:'commodities' }
                );
            }
            
            // Actualizar UI con datos iniciales
            this.startKpiHeartbeat && this.startKpiHeartbeat();
            this.startPositionsHeartbeat && this.startPositionsHeartbeat();
            this.updateMarketWatch();
            this.updatePositionsTable(this.positions);
            this.updateOrdersTable(this.orders);
            this.toggleMarketWatchLoader(false);
            this.togglePositionsLoader(false);
            this.toggleOrdersLoader(false);
            
        } catch (error) {
            console.error('❌ Error cargando datos iniciales:', error);
            this.showNotification('Error cargando datos iniciales', 'error');
            this.toggleMarketWatchLoader(false);
            this.togglePositionsLoader(false);
            this.toggleOrdersLoader(false);
        }
    }

    resetMwFilters() {
        this.favoritesFilterOn = false;
        this.mwSearch = '';
        this.mwFilters = { moving: false, lowSpread: false };
        try {
            localStorage.removeItem('wtFavFilterOn');
            localStorage.removeItem('wtMwSearch');
            localStorage.removeItem('wtMwFiltMoving');
            localStorage.removeItem('wtMwFiltLowSpread');
        } catch (e) {}
    }

    // Actualizar el Market Watch usando la lista de instrumentos en memoria
    updateMarketWatch() {
        const list = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        if (!Array.isArray(list) || !list.length) {
            // Reset filtros/búsqueda si vacían el listado
            this.favoritesFilterOn = false;
            this.mwSearch = '';
            this.mwFilters = { moving: false, lowSpread: false };
            if (Array.isArray(this.instruments) && this.instruments.length) {
                this.updateMarketWatchTable(this.instruments);
            }
            return;
        }
        this.updateMarketWatchTable(list);
        // Lanzar suscripciones de precio basadas en instrumentos visibles
        try { this.updatePriceSubscriptions(); } catch (e) {}
        // Si mercado de acciones está cerrado, aplicar precios estáticos inmediatamente
        try { this.applyStaticStockPricesIfClosed(); } catch (e) {}
    }

    applyStaticStockPricesIfClosed() {
        const open = this.isMarketOpen('stocks');
        if (open) return;
        const list = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        for (const inst of list) {
            const cat = String(inst?.category||'').toLowerCase();
            if (cat !== 'stocks') continue;
            const sym = this.normalizeSymbol(inst.symbol);
            const px = Number(inst.price ?? ((Number(inst.bid)+Number(inst.ask))/2));
            if (!Number.isFinite(px)) continue;
            const prev = Number(inst.prevClose ?? inst.open);
            const change = Number.isFinite(prev) ? (px - prev) : 0;
            const changePct = Number.isFinite(prev) && prev>0 ? (change/prev)*100 : 0;
            this.updateMarketWatchPrice(sym, px, px, change, changePct, 'static');
        }
    }

    async initializeCharts() {
        // Intentar inicializar gráfico de velas en el canvas candlestick-chart
        const ctx = document.getElementById('candlestick-chart');
        if (!ctx) return;

        const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
        const isDark = currentTheme === 'dark';
        const gridColor = isDark ? '#2b3139' : '#e1e5e9';
        const textColor = isDark ? '#c7ccd1' : '#4a5568';

        // Registrar plugin de línea de precio (una sola vez)
        try {
            if (typeof Chart !== 'undefined' && !window.__wtCurrentPricePluginRegistered) {
                const CurrentPricePlugin = {
                    id: 'currentPriceLine',
                    afterDatasetsDraw: (chart, args, opts) => {
                        const app = window.__webTraderAppInstance;
                        if (!app || app.currentMidPrice == null) return;
                        const yScale = chart.scales && chart.scales.y;
                        const xScale = chart.scales && chart.scales.x;
                        if (!yScale || !xScale) return;
                        const y = yScale.getPixelForValue(app.currentMidPrice);
                        const ctx2 = chart.ctx;
                        const color = (opts && opts.color) || '#00d4aa';
                        ctx2.save();
                        ctx2.strokeStyle = color;
                        ctx2.lineWidth = 1;
                        ctx2.setLineDash([4,3]);
                        ctx2.beginPath();
                        ctx2.moveTo(xScale.left, y);
                        ctx2.lineTo(xScale.right, y);
                        ctx2.stroke();
                        // Etiqueta de precio en el eje derecho
                        const label = app.formatPrice(app.currentSymbol, app.currentMidPrice);
                        const padding = 6;
                        const fontSize = 11;
                        ctx2.font = `${fontSize}px Inter, system-ui`;
                        const textW = ctx2.measureText(label).width;
                        const boxW = textW + padding*2;
                        const boxH = fontSize + padding*2 - 2;
                        const bx = xScale.right - boxW;
                        const by = Math.max(y - boxH/2, yScale.top);
                        ctx2.fillStyle = '#0b1220';
                        ctx2.globalAlpha = 0.85;
                        ctx2.fillRect(bx, by, boxW, boxH);
                        ctx2.globalAlpha = 1;
                        ctx2.strokeStyle = color;
                        ctx2.lineWidth = 1;
                        ctx2.strokeRect(bx, by, boxW, boxH);
                        ctx2.fillStyle = color;
                        ctx2.fillText(label, bx + padding, by + boxH - padding - 2);
                        ctx2.restore();
                    }
                };
                Chart.register(CurrentPricePlugin);
                window.__wtCurrentPricePluginRegistered = true;
            }
            // Registrar plugin de crosshair vertical (una sola vez)
            if (typeof Chart !== 'undefined' && !window.__wtCrosshairPluginRegistered) {
                const CrosshairPlugin = {
                    id: 'crosshairOverlay',
                    afterDatasetsDraw: (chart, args, opts) => {
                        const app = window.__webTraderAppInstance;
                        if (!app || !app.showCrosshair || app.crosshairX == null) return;
                        const xScale = chart.scales && chart.scales.x;
                        const yScale = chart.scales && chart.scales.y;
                        if (!xScale || !yScale) return;
                        const ctx2 = chart.ctx;
                        const color = (opts && opts.color) || '#8aa7ff';
                        const x = Math.max(xScale.left, Math.min(xScale.right, app.crosshairX));
                        ctx2.save();
                        ctx2.strokeStyle = color;
                        ctx2.lineWidth = 1;
                        ctx2.setLineDash([3,3]);
                        ctx2.beginPath();
                        ctx2.moveTo(x, yScale.top);
                        ctx2.lineTo(x, yScale.bottom);
                        ctx2.stroke();
                        ctx2.restore();
                    }
                };
                Chart.register(CrosshairPlugin);
                window.__wtCrosshairPluginRegistered = true;
            }
            // Exponer instancia para el plugin
            window.__webTraderAppInstance = this;
        } catch (e) {
            console.warn('No se pudo registrar plugin de línea de precio:', e);
        }

        try {
            const axisDigits = this.digitsForSymbol(this.currentSymbol);
            // Crear gráfico inmediatamente con dataset vacío para evitar espera del spinner
            this.charts.main = new Chart(ctx, {
                type: 'candlestick',
                data: {
                    datasets: [{
                        label: this.currentSymbol,
                        data: [],
                        color: {
                            up: '#00d4aa',
                            down: '#ff4757',
                            unchanged: '#8b93a6'
                        }
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    animation: false,
                    interaction: { mode: 'nearest', intersect: false },
                    plugins: {
                        legend: { display: false },
                        currentPriceLine: { color: '#00d4aa' },
                        crosshairOverlay: { color: '#8aa7ff' },
                        tooltip: {
                            enabled: true,
                            backgroundColor: 'rgba(20,22,30,0.92)',
                            borderColor: gridColor,
                            borderWidth: 1,
                            titleColor: textColor,
                            bodyColor: textColor,
                            callbacks: {
                                title: (items) => {
                                    try { return new Date(items[0].raw?.x || items[0].parsed?.x).toLocaleString(); } catch (_) { return ''; }
                                },
                                label: (ctx) => {
                                    const r = ctx.raw || {};
                                    if (r && typeof r === 'object' && 'o' in r) {
                                        return `O ${r.o}  H ${r.h}  L ${r.l}  C ${r.c}`;
                                    }
                                    return `C ${ctx.parsed?.y ?? ''}`;
                                }
                            }
                        }
                    },
                    layout: { padding: { left: 8, right: 16, top: 8, bottom: 8 } },
                    scales: {
                        x: {
                            type: 'timeseries',
                            time: { unit: 'minute', displayFormats: { minute: 'HH:mm', hour: 'MMM d HH:mm', day: 'MMM d' } },
                            grid: { color: gridColor },
                            ticks: { color: textColor, maxTicksLimit: 8 }
                        },
                        y: {
                            position: 'right',
                            grid: { color: gridColor },
                            ticks: {
                                color: textColor,
                                maxTicksLimit: 8,
                                callback: (v) => (Number.isFinite(v) ? Number(v).toFixed(axisDigits) : v)
                            }
                        }
                    }
                }
            });
            // Ocultar skeleton inmediatamente ya que el lienzo está listo
            this.toggleChartSkeleton(false);
            // Banderas junto al símbolo en la barra superior
            try { this.renderSymbolFlags(this.currentSymbol); } catch (_) {}
            // Inicializar rango de vista
            // Cargar velas rápidas para primer pintado y luego ampliar historial
            (async () => {
                const fast = await this.fetchCandlesFast(this.currentSymbol, this.currentTimeframe);
                const chart = this.charts.main;
                if (Array.isArray(fast) && fast.length && chart) {
                    chart.data.datasets[0].data = fast;
                    chart.update('none');
                    this.setViewRangeCenteredOnLast(fast);
                    const last = fast[fast.length - 1];
                    if (last && Number.isFinite(last.c)) this.currentMidPrice = last.c;
                }
                // Cargar base 1M para agregación y ampliar datos
                try {
                    if (this.currentTimeframe === 1) {
                        this.baseMinuteCandles = fast.slice();
                    } else {
                        const m1 = await this.fetchCandles(this.currentSymbol, 1);
                        if (Array.isArray(m1) && m1.length) this.baseMinuteCandles = m1.slice();
                    }
                } catch (_) {}
                // Ampliar dataset con historial completo en background
                try {
                    const full = await this.fetchCandles(this.currentSymbol, this.currentTimeframe);
                    if (Array.isArray(full) && full.length && chart) {
                        chart.data.datasets[0].data = full;
                        chart.update('none');
                        this.setViewRangeCenteredOnLast(full);
                        const last2 = full[full.length - 1];
                        if (last2 && Number.isFinite(last2.c)) this.currentMidPrice = last2.c;
                    }
                } catch (_) {}
            })();
            // Interacciones de zoom/pan y countdown
            // Guardar precio actual inicial para mostrar la línea inmediatamente
            this.setupChartInteractions();
            this.startCandleCountdown();
            this.setupToolbarButtons();
        } catch (e) {
            console.warn('Candlestick plugin no disponible. Fallback a línea.', e);
            // Fallback: gráfico de línea simple usando el mismo canvas
            const axisDigits2 = this.digitsForSymbol(this.currentSymbol);
            this.charts.main = new Chart(ctx, {
                type: 'line',
                data: { labels: [], datasets: [{ label: this.currentSymbol, data: [], borderColor: '#00d4aa', backgroundColor: 'rgba(0, 212, 170, 0.1)', borderWidth: 2, fill: true, tension: 0.1 }] },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    animation: false,
                    interaction: { mode: 'nearest', intersect: false },
                    plugins: {
                        legend: { display: false },
                        currentPriceLine: { color: '#00d4aa' },
                        crosshairOverlay: { color: '#8aa7ff' },
                        tooltip: {
                            enabled: true,
                            backgroundColor: 'rgba(20,22,30,0.92)',
                            borderColor: gridColor,
                            borderWidth: 1,
                            titleColor: textColor,
                            bodyColor: textColor,
                            callbacks: {
                                title: (items) => {
                                    try { return new Date(items[0].label).toLocaleString(); } catch (_) { return ''; }
                                },
                                label: (ctx) => `C ${ctx.parsed?.y ?? ''}`
                            }
                        }
                    },
                    layout: { padding: { left: 8, right: 16, top: 8, bottom: 8 } },
                    scales: {
                        x: { type: 'time', time: { unit: 'minute', displayFormats: { minute: 'HH:mm', hour: 'MMM d HH:mm', day: 'MMM d' } }, grid: { color: gridColor }, ticks: { color: textColor, maxTicksLimit: 8 } },
                        y: { position: 'right', grid: { color: gridColor }, ticks: { color: textColor, maxTicksLimit: 8, callback: (v) => (Number.isFinite(v) ? Number(v).toFixed(axisDigits2) : v) } }
                    }
                }
            });
            this.toggleChartSkeleton(false);
            // Pintado rápido
            (async () => {
                const fast = await this.fetchCandlesFast(this.currentSymbol, this.currentTimeframe);
                const chart = this.charts.main;
                if (Array.isArray(fast) && fast.length && chart) {
                    chart.data.labels = fast.map(c => new Date(c.x));
                    chart.data.datasets[0].data = fast.map(c => c.c);
                    chart.update('none');
                    this.setViewRangeCenteredOnLast(fast);
                    const last = fast[fast.length - 1];
                    if (last && Number.isFinite(last.c)) this.currentMidPrice = last.c;
                }
            })();
            this.setupChartInteractions();
            this.startCandleCountdown();
            this.stopChartHeartbeat();
            this.startChartHeartbeat();
            this.setupToolbarButtons();
        }
    }

    generateInitialCandles(symbol) {
        // Demo desactivado: no generar velas simuladas
        return [];
    }

    // Ingresar un tick al buffer base de minutos y actualizar vela en formación
    onTick(price, timestamp = Date.now(), symbol = this.currentSymbol) {
        if (symbol !== this.currentSymbol) return;
        const minuteStart = Math.floor(timestamp / 60000) * 60000;
        const last = this.baseMinuteCandles[this.baseMinuteCandles.length - 1];
        if (!last || last.x !== minuteStart) {
            // Finalizar vela anterior y comenzar nueva
            this.baseMinuteCandles.push({ x: minuteStart, o: price, h: price, l: price, c: price });
            // Limitar tamaño del buffer (p.ej., 720 min ~12h)
            if (this.baseMinuteCandles.length > 720) this.baseMinuteCandles.shift();
        } else {
            // Actualizar vela en formación
            if (price > last.h) last.h = price;
            if (price < last.l) last.l = price;
            last.c = price;
        }
    }

    async fetchCandles(symbol, timeframe) {
        try {
            try { this._candlesController && this._candlesController.abort(); } catch(_){}
            this._candlesController = (typeof AbortController !== 'undefined') ? new AbortController() : null;
            const tf = Number.isFinite(timeframe) ? timeframe : 1;
            const limit = (tf === 1) ? 2880 : 500;
            const params = new URLSearchParams({ symbol, timeframe: String(tf), limit: String(limit) });
            const resp = await fetch(`api/candles.php?${params.toString()}`, { signal: this._candlesController?.signal });
            const json = await resp.json().catch(() => ({ data: [] }));
            const arr = (json && Array.isArray(json.data)) ? json.data : [];
            const mapped = arr.map(c => ({ x: c.x, o: c.o, h: c.h, l: c.l, c: c.c }));
            const out = (tf === 1) ? this.normalizeMinuteCandles(mapped) : mapped;
            try {
                const key = `wt-candles-${symbol}-${tf}`;
                localStorage.setItem(key, JSON.stringify(out.slice(-300)));
            } catch (_) {}
            return out;
        } catch (e) {
            return [];
        }
    }

    async fetchCandlesFast(symbol, timeframe) {
        // Versión rápida con límite pequeño para primer pintado
        try {
            // Intentar cache primero
            try {
                const key = `wt-candles-${symbol}-${timeframe}`;
                const raw = localStorage.getItem(key);
                if (raw) {
                    const cached = JSON.parse(raw);
                    if (Array.isArray(cached) && cached.length) return cached.slice(-120);
                }
            } catch (_) {}
            const tf = Number.isFinite(timeframe) ? timeframe : 1;
            const limit = Math.min(tf === 1 ? 600 : 150, 600);
            const params = new URLSearchParams({ symbol, timeframe: String(tf), limit: String(limit) });
            try { this._candlesFastController && this._candlesFastController.abort(); } catch(_){}
            this._candlesFastController = (typeof AbortController !== 'undefined') ? new AbortController() : null;
            const resp = await fetch(`api/candles.php?${params.toString()}`, { signal: this._candlesFastController?.signal });
            const json = await resp.json().catch(() => ({ data: [] }));
            const arr = (json && Array.isArray(json.data)) ? json.data : [];
            const mapped = arr.map(c => ({ x: c.x, o: c.o, h: c.h, l: c.l, c: c.c }));
            return (tf === 1) ? this.normalizeMinuteCandles(mapped).slice(-300) : mapped.slice(-150);
        } catch (_) { return []; }
    }

    setDefaultFavoritesIfEmpty() {
        try {
            const raw = (typeof localStorage !== 'undefined') ? localStorage.getItem('wtFavorites') : null;
            const arr = raw ? JSON.parse(raw) : [];
            if (!Array.isArray(arr) || arr.length === 0) {
                this.favorites = this.defaultFavorites.slice();
                if (typeof localStorage !== 'undefined') localStorage.setItem('wtFavorites', JSON.stringify(this.favorites));
            } else {
                this.favorites = arr;
            }
        } catch (_) {
            this.favorites = this.defaultFavorites.slice();
        }
    }

    getCategoryForSymbol(s) {
        const sym = this.normalizeSymbol(s);
        if (/^[A-Z]{6}$/.test(sym)) return 'forex';
        if (/^(XAUUSD|XAGUSD|XPDUSD|USOIL|WTIUSD|XBRUSD|UKOILUSD|COPPERUSD|CORNUSD|SUGARUSD)$/.test(sym)) return 'commodities';
        if (/^(SPX|NDX|DJI|FTSE|GDAXI|N225|HSI|FCHI)$/.test(sym)) return 'indices';
        return 'stocks';
    }

    async prefetchFavoritesData() {
        const favs = Array.isArray(this.favorites) ? this.favorites.map(s=>this.normalizeSymbol(s)) : [];
        for (const s of favs.slice(0,6)) {
            try { await this.fetchCandlesFast(s, this.currentTimeframe); } catch (_) {}
        }
    }

    // Normalizar velas de 1 minuto para respetar apertura/cierre y alineación temporal
    normalizeMinuteCandles(candles) {
        if (!Array.isArray(candles) || candles.length === 0) return [];
        const map = new Map();
        for (const c of candles) {
            const t = +new Date(c.x);
            const start = Math.floor(t / 60000) * 60000;
            const prev = map.get(start);
            if (!prev) {
                map.set(start, { x: start, o: c.o, h: c.h, l: c.l, c: c.c });
            } else {
                map.set(start, {
                    x: start,
                    o: prev.o,
                    h: Math.max(prev.h, c.h),
                    l: Math.min(prev.l, c.l),
                    c: c.c
                });
            }
        }
        return Array.from(map.entries()).sort((a,b)=>a[0]-b[0]).map(([,v])=>v);
    }

    // Agregar velas por timeframe agrupando por múltiplos de tiempo (alineado a UTC)
    aggregateCandlesFromBase(tfMinutes = this.currentTimeframe) {
        const N = Math.max(1, parseInt(tfMinutes, 10));
        const base = this.baseMinuteCandles || [];
        if (!Array.isArray(base) || base.length === 0) return [];
        const bucketMs = N * 60 * 1000;
        const out = [];
        let curStart = null;
        let o = null, h = -Infinity, l = Infinity, c = null;
        for (let i = 0; i < base.length; i++) {
            const m = base[i];
            const t = +new Date(m.x);
            const start = Math.floor(t / bucketMs) * bucketMs;
            if (curStart === null) {
                curStart = start; o = m.o; c = m.c; h = m.h; l = m.l;
            } else if (start === curStart) {
                // mismo bucket
                if (o === null) o = m.o;
                c = m.c;
                if (m.h > h) h = m.h; if (m.l < l) l = m.l;
            } else {
                // cerrar bucket anterior y empezar nuevo
                out.push({ x: curStart, o, h, l, c });
                curStart = start; o = m.o; c = m.c; h = m.h; l = m.l;
            }
        }
        if (curStart !== null) out.push({ x: curStart, o, h, l, c });
        const maxBars = 150;
        return out.slice(Math.max(0, out.length - maxBars));
    }

    // Número de barras visibles sugeridas según timeframe
    getDefaultBarsToShow() {
        const tf = this.currentTimeframe || 1;
        if (tf >= 1440) return Math.min(this.defaultVisibleBars, 80); // 1D+
        if (tf >= 240) return Math.min(this.defaultVisibleBars, 100); // 4H
        if (tf >= 60) return Math.min(this.defaultVisibleBars, 120);  // 1H
        return Math.min(this.defaultVisibleBars, 120);                 // <=30M
    }

    // Centrar el rango visible en la última vela
    setViewRangeCenteredOnLast(candlesOrTimes, barsTarget = this.getDefaultBarsToShow()) {
        const chart = this.charts.main;
        if (!chart || !chart.options?.scales?.x) return;
        const xs = Array.isArray(candlesOrTimes) ? (
            typeof candlesOrTimes[0] === 'object' && candlesOrTimes[0] && 'x' in candlesOrTimes[0]
                ? candlesOrTimes.map(c => +new Date(c.x))
                : candlesOrTimes.map(t => +new Date(t))
        ) : [];
        if (!xs.length) return;
        const fullMin = xs[0];
        const fullMax = xs[xs.length - 1];
        const count = Math.max(1, Math.min(barsTarget, xs.length));
        const startIdx = Math.max(0, xs.length - count);
        const windowMin = xs[startIdx];
        const spanMs = Math.max(1, fullMax - windowMin);
        const half = Math.floor(spanMs / 2);
        let min = fullMax - half;
        let max = fullMax + half;
        // En 1M, limitar a máximo 2 días hacia atrás
        if (this.currentTimeframe === 1) {
            const twoDaysMs = 2 * 24 * 60 * 60 * 1000;
            min = Math.max(fullMin, fullMax - twoDaysMs);
            max = fullMax;
        }
        // Garantizar que no se vaya totalmente fuera del rango completo
        if (min < fullMin) { min = fullMin; max = fullMin + spanMs; }
        this.viewRange = { fullMin, fullMax, min, max };
        chart.options.scales.x.min = min;
        chart.options.scales.x.max = max;
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        chart.update('none');
    }

    // Refrescar dataset principal a partir del buffer base y timeframe actual
    refreshChartFromBaseBuffer() {
        if (!this.charts.main) return;
        const chart = this.charts.main;
        const aggregated = this.aggregateCandlesFromBase(this.currentTimeframe);
        if (!Array.isArray(aggregated) || aggregated.length === 0) return;
        if (chart.config.type === 'candlestick') {
            chart.data.datasets[0].label = this.currentSymbol;
            const current = Array.isArray(chart.data.datasets[0].data) ? chart.data.datasets[0].data.slice() : [];
            const merged = current.slice();
            const last = merged[merged.length - 1];
            const next = aggregated[aggregated.length - 1];
            if (last && next && last.x === next.x) {
                merged[merged.length - 1] = next;
            } else if (next && (!last || next.x > last.x)) {
                merged.push(next);
            }
            // Limitar barras visibles si seguimos la última vela para evitar crecimiento descontrolado
            if (this.followLast) {
                const bars = this.getDefaultBarsToShow();
                if (merged.length > bars) merged.splice(0, merged.length - bars);
            }
            chart.data.datasets[0].data = merged;
            const xs = chart.data.datasets[0].data.map(c => +new Date(c.x));
            const fullMin = xs.length ? xs[0] : undefined;
            const fullMax = xs.length ? xs[xs.length - 1] : undefined;
            this.viewRange.fullMin = fullMin;
            this.viewRange.fullMax = fullMax;
            if (chart.options?.scales?.x) {
                if (this.followLast) {
                    this.setViewRangeCenteredOnLast(chart.data.datasets[0].data);
                } else {
                    const { min, max } = this.viewRange;
                    chart.options.scales.x.min = min ?? fullMin;
                    chart.options.scales.x.max = max ?? fullMax;
                }
            }
            if (this.autoScaleOn) {
                this.updateYAxisForVisibleRange();
            } else if (this.fixedYRange && chart.options?.scales?.y) {
                chart.options.scales.y.min = this.fixedYRange.min;
                chart.options.scales.y.max = this.fixedYRange.max;
            }
            chart.update('none');
        } else {
            chart.data.datasets[0].label = this.currentSymbol;
            const currentLabels = Array.isArray(chart.data.labels) ? chart.data.labels.slice() : [];
            const currentData = Array.isArray(chart.data.datasets[0].data) ? chart.data.datasets[0].data.slice() : [];
            const next = aggregated[aggregated.length - 1];
            const lastLabel = currentLabels.length ? +new Date(currentLabels[currentLabels.length - 1]) : null;
            if (next) {
                const nx = +new Date(next.x);
                if (lastLabel === nx) {
                    currentData[currentData.length - 1] = next.c;
                } else if (!lastLabel || nx > lastLabel) {
                    currentLabels.push(new Date(next.x));
                    currentData.push(next.c);
                }
            }
            if (this.followLast) {
                const bars = this.getDefaultBarsToShow();
                if (currentLabels.length > bars) {
                    currentLabels.splice(0, currentLabels.length - bars);
                    currentData.splice(0, currentData.length - bars);
                }
            }
            chart.data.labels = currentLabels;
            chart.data.datasets[0].data = currentData;
            const xs = currentLabels.map(d => +new Date(d));
            const fullMin = xs.length ? xs[0] : undefined;
            const fullMax = xs.length ? xs[xs.length - 1] : undefined;
            this.viewRange.fullMin = fullMin;
            this.viewRange.fullMax = fullMax;
            if (chart.options?.scales?.x) {
                if (this.followLast) {
                    this.setViewRangeCenteredOnLast(currentLabels.map((d,i)=>({x:+new Date(d), c: currentData[i]})));
                } else {
                    const { min, max } = this.viewRange;
                    chart.options.scales.x.min = min ?? fullMin;
                    chart.options.scales.x.max = max ?? fullMax;
                }
            }
            if (this.autoScaleOn) {
                this.updateYAxisForVisibleRange();
            } else if (this.fixedYRange && chart.options?.scales?.y) {
                chart.options.scales.y.min = this.fixedYRange.min;
                chart.options.scales.y.max = this.fixedYRange.max;
            }
            chart.update('none');
        }
    }

    // Botones de toolbar para zoom/pan
    setupToolbarButtons() {
        const zi = document.getElementById('btnZoomIn');
        const zo = document.getElementById('btnZoomOut');
        const zr = document.getElementById('btnResetZoom');
        const pl = document.getElementById('btnPanLeft');
        const pr = document.getElementById('btnPanRight');
        zi && zi.addEventListener('click', () => this.zoomInStep());
        zo && zo.addEventListener('click', () => this.zoomOutStep());
        zr && zr.addEventListener('click', () => this.resetZoom());
        pl && pl.addEventListener('click', () => this.panByPercent(-0.2));
        pr && pr.addEventListener('click', () => this.panByPercent(0.2));

        // Selector de timeframe eliminado: gráfico fijo en 1M

        // Cargar historial de 3 meses bajo demanda
        // Botón de 3M eliminado, la carga extendida ya no se usa

        // Toggle Auto-scale
        const autoBtn = document.getElementById('btnToggleAutoscale');
        if (autoBtn) {
            autoBtn.classList.toggle('active', !!this.autoScaleOn);
            autoBtn.addEventListener('click', () => {
                this.toggleAutoScale();
                autoBtn.classList.toggle('active', !!this.autoScaleOn);
                // Centrar en la última vela y armonizar la vista
                try {
                    const chart = this.charts.main;
                    if (chart) {
                        const ds = chart.data?.datasets?.[0]?.data || [];
                        if (Array.isArray(ds) && ds.length) {
                            this.followLast = true;
                            this.setViewRangeCenteredOnLast(ds);
                            if (this.autoScaleOn) this.updateYAxisForVisibleRange();
                            chart.update('none');
                        }
                    }
                } catch(_) {}
            });
        }

        // Toggle Crosshair
        const crossBtn = document.getElementById('btnToggleCrosshair');
        if (crossBtn) {
            crossBtn.classList.toggle('active', !!this.showCrosshair);
            crossBtn.addEventListener('click', () => {
                this.toggleCrosshair();
                crossBtn.classList.toggle('active', !!this.showCrosshair);
            });
        }
    }

    // Interacciones de rueda y arrastre en el canvas
    setupChartInteractions() {
        const canvas = document.getElementById('candlestick-chart');
        if (!canvas || !this.charts.main) return;
        const chart = this.charts.main;
        // Zoom con rueda del mouse
        canvas.addEventListener('wheel', (e) => {
            e.preventDefault();
            // Interacción del usuario: desactiva seguimiento de última vela
            this.followLast = false;
            const xScale = chart.scales && chart.scales.x;
            if (!xScale) return;
            const pivotValue = xScale.getValueForPixel(e.offsetX);
            const rng = this.viewRange;
            const min = rng.min ?? rng.fullMin;
            const max = rng.max ?? rng.fullMax;
            if (min == null || max == null || pivotValue == null) return;
            const factor = e.deltaY > 0 ? 1.15 : 0.85; // out / in
            const newMin = pivotValue - (pivotValue - min) * factor;
            const newMax = pivotValue + (max - pivotValue) * factor;
            this.viewRange.min = Math.max(this.viewRange.fullMin ?? newMin, newMin);
            this.viewRange.max = Math.min(this.viewRange.fullMax ?? newMax, newMax);
            chart.options.scales.x.min = this.viewRange.min;
            chart.options.scales.x.max = this.viewRange.max;
            if (this.autoScaleOn) this.updateYAxisForVisibleRange();
            chart.update('none');
        }, { passive: false });

        // Pan con arrastre
        canvas.addEventListener('mousedown', (e) => {
            // Al comenzar a arrastrar, desactivar seguimiento automático
            this.followLast = false;
            this.isPanning = true;
            this.panStartX = e.clientX;
            const rng = this.viewRange;
            this.panStartRange = { min: rng.min ?? rng.fullMin, max: rng.max ?? rng.fullMax };
        });
        window.addEventListener('mouseup', () => { this.isPanning = false; });
        canvas.addEventListener('mousemove', (e) => {
            if (!this.isPanning || !this.panStartRange) return;
            const xScale = chart.scales && chart.scales.x;
            if (!xScale) return;
            const dx = e.clientX - this.panStartX;
            // Convertir píxeles a tiempo usando escala
            const v1 = xScale.getValueForPixel(xScale.left);
            const v2 = xScale.getValueForPixel(xScale.left + 100);
            const msPer100px = (v2 - v1) || 1;
            const deltaMs = (dx / 100) * msPer100px;
            let newMin = this.panStartRange.min - deltaMs;
            let newMax = this.panStartRange.max - deltaMs;
            // Limitar al rango completo
            const fullMin = this.viewRange.fullMin;
            const fullMax = this.viewRange.fullMax;
            const span = newMax - newMin;
            if (newMin < fullMin) { newMin = fullMin; newMax = fullMin + span; }
            if (newMax > fullMax) { newMax = fullMax; newMin = fullMax - span; }
            this.viewRange.min = newMin;
            this.viewRange.max = newMax;
            chart.options.scales.x.min = newMin;
            chart.options.scales.x.max = newMax;
            if (this.autoScaleOn) this.updateYAxisForVisibleRange();
            chart.update('none');
        });

        // Crosshair overlay tracking
        canvas.addEventListener('mousemove', (e) => {
            if (this.showCrosshair) {
                this.crosshairX = e.offsetX;
                chart.update('none');
            }
        });
        canvas.addEventListener('mouseleave', () => {
            if (this.showCrosshair) {
                this.crosshairX = null;
                chart.update('none');
            }
        });
    }

    zoomInStep() {
        if (!this.charts.main) return;
        // Desactivar seguimiento por interacción manual
        this.followLast = false;
        const rng = this.viewRange;
        const min = rng.min ?? rng.fullMin;
        const max = rng.max ?? rng.fullMax;
        if (min == null || max == null) return;
        const pivot = (min + max) / 2;
        const factor = 0.85;
        const newMin = pivot - (pivot - min) * factor;
        const newMax = pivot + (max - pivot) * factor;
        this.viewRange.min = Math.max(this.viewRange.fullMin ?? newMin, newMin);
        this.viewRange.max = Math.min(this.viewRange.fullMax ?? newMax, newMax);
        this.charts.main.options.scales.x.min = this.viewRange.min;
        this.charts.main.options.scales.x.max = this.viewRange.max;
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        this.charts.main.update('none');
    }

    zoomOutStep() {
        if (!this.charts.main) return;
        // Desactivar seguimiento por interacción manual
        this.followLast = false;
        const rng = this.viewRange;
        const min = rng.min ?? rng.fullMin;
        const max = rng.max ?? rng.fullMax;
        if (min == null || max == null) return;
        const pivot = (min + max) / 2;
        const factor = 1.15;
        const newMin = pivot - (pivot - min) * factor;
        const newMax = pivot + (max - pivot) * factor;
        this.viewRange.min = Math.max(this.viewRange.fullMin ?? newMin, newMin);
        this.viewRange.max = Math.min(this.viewRange.fullMax ?? newMax, newMax);
        this.charts.main.options.scales.x.min = this.viewRange.min;
        this.charts.main.options.scales.x.max = this.viewRange.max;
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        this.charts.main.update('none');
    }

    resetZoom() {
        if (!this.charts.main) return;
        // Reactivar seguimiento y centrar en la última vela
        this.followLast = true;
        const data = (this.charts.main.config.type === 'candlestick')
            ? (this.charts.main.data.datasets[0]?.data || [])
            : this.aggregateCandlesFromBase(this.baseMinuteCandles || [], this.currentTimeframe);
        this.setViewRangeCenteredOnLast(data);
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        this.charts.main.update('none');
    }

    panByPercent(percent = 0.2) {
        if (!this.charts.main) return;
        // Desactivar seguimiento por interacción manual
        this.followLast = false;
        const rng = this.viewRange;
        const min = rng.min ?? rng.fullMin;
        const max = rng.max ?? rng.fullMax;
        if (min == null || max == null) return;
        const span = max - min;
        const delta = span * percent;
        let newMin = min + delta;
        let newMax = max + delta;
        // Limitar al rango completo
        const fullMin = rng.fullMin;
        const fullMax = rng.fullMax;
        if (newMin < fullMin) { newMin = fullMin; newMax = fullMin + span; }
        if (newMax > fullMax) { newMax = fullMax; newMin = fullMax - span; }
        this.viewRange.min = newMin;
        this.viewRange.max = newMax;
        this.charts.main.options.scales.x.min = newMin;
        this.charts.main.options.scales.x.max = newMax;
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        this.charts.main.update('none');
    }

    // Countdown de cierre de vela
    // -------- Premium toolbar helpers --------
    updateTimeframeUIActive() {
        // Segmented buttons
        document.querySelectorAll('.tf-seg-btn').forEach(b => {
            const tf = parseInt(b.getAttribute('data-tf') || '1', 10);
            const active = tf === this.currentTimeframe;
            b.classList.toggle('active', active);
            b.setAttribute('aria-pressed', active ? 'true' : 'false');
        });
        // Dropdown labels and options
        const container = document.querySelector('.timeframe-selector');
        if (container) {
            const currentLabel = container.querySelector('#tfCurrentLabel');
            if (currentLabel) currentLabel.textContent = this.timeframeLabelShort(this.currentTimeframe);
            const options = container.querySelectorAll('.tf-option');
            options.forEach(opt => {
                const minutes = parseInt(opt.getAttribute('data-tf') || '1', 10);
                const isActive = minutes === this.currentTimeframe;
                opt.classList.toggle('active', isActive);
                opt.setAttribute('aria-selected', isActive ? 'true' : 'false');
            });
        }
    }

    toggleAutoScale() {
        this.autoScaleOn = !this.autoScaleOn;
        try { if (typeof localStorage !== 'undefined') localStorage.setItem('wtAutoScaleOn', this.autoScaleOn ? '1' : '0'); } catch (e) {}
        const chart = this.charts.main;
        if (!chart) return;
        if (this.autoScaleOn) {
            this.fixedYRange = null;
            this.updateYAxisForVisibleRange();
            chart.update('none');
        } else {
            // Congelar rango Y actual
            const yScale = chart.scales && chart.scales.y;
            const currentMin = (yScale && typeof yScale.min === 'number') ? yScale.min : (chart.options?.scales?.y?.min);
            const currentMax = (yScale && typeof yScale.max === 'number') ? yScale.max : (chart.options?.scales?.y?.max);
            this.fixedYRange = { min: currentMin, max: currentMax };
            if (chart.options?.scales?.y) {
                chart.options.scales.y.min = this.fixedYRange.min;
                chart.options.scales.y.max = this.fixedYRange.max;
            }
            chart.update('none');
        }
    }

    toggleCrosshair() {
        this.showCrosshair = !this.showCrosshair;
        try { if (typeof localStorage !== 'undefined') localStorage.setItem('wtShowCrosshair', this.showCrosshair ? '1' : '0'); } catch (e) {}
        const canvas = document.getElementById('candlestick-chart');
        if (canvas) {
            canvas.style.cursor = this.showCrosshair ? 'crosshair' : '';
        }
        if (!this.showCrosshair) {
            this.crosshairX = null;
        }
        if (this.charts.main) this.charts.main.update('none');
    }

    updateYAxisForVisibleRange() {
        const chart = this.charts.main;
        if (!chart) return;
        if (!this.autoScaleOn) {
            if (this.fixedYRange && chart.options?.scales?.y) {
                chart.options.scales.y.min = this.fixedYRange.min;
                chart.options.scales.y.max = this.fixedYRange.max;
            }
            return;
        }
        const bounds = this.computeYAxisBoundsForVisibleRange();
        if (!bounds) return;
        const pad = (bounds.max - bounds.min) * 0.06;
        const yMin = bounds.min - (Number.isFinite(pad) ? pad : 0);
        const yMax = bounds.max + (Number.isFinite(pad) ? pad : 0);
        if (chart.options?.scales?.y) {
            chart.options.scales.y.min = yMin;
            chart.options.scales.y.max = yMax;
        }
    }

    computeYAxisBoundsForVisibleRange() {
        const chart = this.charts.main;
        if (!chart) return null;
        const xMin = this.viewRange.min ?? this.viewRange.fullMin;
        const xMax = this.viewRange.max ?? this.viewRange.fullMax;
        if (xMin == null || xMax == null) return null;
        let minY = Infinity;
        let maxY = -Infinity;
        if (chart.config.type === 'candlestick') {
            const data = chart.data.datasets[0]?.data || [];
            for (const c of data) {
                const t = +new Date(c.x);
                if (t >= xMin && t <= xMax) {
                    if (Number.isFinite(c.l)) minY = Math.min(minY, c.l);
                    if (Number.isFinite(c.h)) maxY = Math.max(maxY, c.h);
                }
            }
        } else {
            const labels = chart.data.labels || [];
            const values = chart.data.datasets[0]?.data || [];
            for (let i = 0; i < labels.length; i++) {
                const t = +new Date(labels[i]);
                if (t >= xMin && t <= xMax) {
                    const v = values[i];
                    if (Number.isFinite(v)) {
                        minY = Math.min(minY, v);
                        maxY = Math.max(maxY, v);
                    }
                }
            }
        }
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) return null;
        if (minY === maxY) {
            const epsilon = Math.abs(minY) * 0.001 || 0.0001;
            minY -= epsilon;
            maxY += epsilon;
        }
        return { min: minY, max: maxY };
    }

    startCandleCountdown() {
        const el = document.getElementById('candleCountdown');
        const bar = document.getElementById('candleProgress');
        if (!el) return;
        this.stopCandleCountdown();
        const tfMin = this.currentTimeframe;
        let nextClose = this.computeNextCandleCloseTime(tfMin);
        const tick = () => {
            const now = Date.now();
            // Recalcular para evitar deriva y alinear al borde exacto
            if (now >= nextClose) nextClose = this.computeNextCandleCloseTime(tfMin);
            const remaining = Math.max(0, nextClose - now);
            const m = Math.floor(remaining / 60000);
            const s = Math.floor((remaining % 60000) / 1000);
            el.textContent = `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
            if (bar && bar.querySelector) {
                const tfMs = Math.max(1, parseInt(tfMin,10)) * 60000;
                const done = tfMs - remaining;
                const pct = Math.max(0, Math.min(100, (done / tfMs) * 100));
                const fill = bar.querySelector('.fill');
                if (fill) fill.style.width = pct.toFixed(1) + '%';
            }
        };
        tick();
        this.candleCountdownTimer = setInterval(tick, 1000);
    }

    stopCandleCountdown() {
        if (this.candleCountdownTimer) {
            clearInterval(this.candleCountdownTimer);
            this.candleCountdownTimer = null;
        }
    }

    // Calcular el cierre de la próxima vela alineado al timeframe
    computeNextCandleCloseTime(tfMin = this.currentTimeframe) {
        const tfMs = Math.max(1, parseInt(tfMin, 10)) * 60000;
        const now = Date.now();
        const bucketStart = Math.floor(now / tfMs) * tfMs;
        return bucketStart + tfMs;
    }

    // Heartbeat para suavizar actualización de vela en formación cuando no llegan ticks
    startChartHeartbeat() {
        this.stopChartHeartbeat();
        const run = () => {
            if (!this.baseMinuteCandles || !this.baseMinuteCandles.length) return;
            const last = this.baseMinuteCandles[this.baseMinuteCandles.length - 1];
            const now = Date.now();
            const minuteStart = Math.floor(now / 60000) * 60000;
            // Solo actualizar si estamos dentro de la vela en formación y tenemos precio actual
            if (last && last.x === minuteStart && Number.isFinite(this.currentMidPrice)) {
                if (this.currentMidPrice > last.h) last.h = this.currentMidPrice;
                if (this.currentMidPrice < last.l) last.l = this.currentMidPrice;
                last.c = this.currentMidPrice;
                this.refreshChartFromBaseBuffer();
                this.updateStatusBar();
            }
        };
        this.chartHeartbeatTimer = setInterval(run, this.heartbeatIntervalMs);
    }

    stopChartHeartbeat() {
        if (this.chartHeartbeatTimer) {
            clearInterval(this.chartHeartbeatTimer);
            this.chartHeartbeatTimer = null;
        }
    }

    // Encolar y procesar ticks en lote para rendimiento
    enqueueTick(update) {
        const { symbol } = update;
        this.pendingTicks.set(symbol, update);
        if (!this.tickRaf) {
            this.tickRaf = (typeof window !== 'undefined' && window.requestAnimationFrame)
                ? window.requestAnimationFrame(() => this.processTickQueue())
                : setTimeout(() => this.processTickQueue(), 50);
        }
    }

    processTickQueue() {
        const updates = Array.from(this.pendingTicks.values());
        this.pendingTicks.clear();
        this.tickRaf = null;

        let shouldRefreshChart = false;
        for (const u of updates) {
            const { symbol, bid, ask, change, changePercent, source, timestamp } = u;
            this._wsLastTickAt = Date.now();
            if (!this.latestTicks) this.latestTicks = new Map();
            this.latestTicks.set(symbol, { bid, ask, change, changePercent, source, timestamp });
            this.updatePositionsPnL(symbol, bid, ask);
            // Actualizar fila del Market Watch para cada símbolo
            const ch = Number(change);
            const chPct = Number(changePercent);
            this.updateMarketWatchPrice(symbol, bid, ask, Number.isFinite(ch) ? ch : 0, Number.isFinite(chPct) ? chPct : 0, source || 'ws');
            const b = parseFloat(bid);
            const a = parseFloat(ask);
            const midAny = (Number.isFinite(b) && Number.isFinite(a)) ? ((b + a) / 2) : (Number.isFinite(b) ? b : (Number.isFinite(a) ? a : NaN));
            if (Number.isFinite(midAny)) this.lastPriceBySymbol[symbol] = midAny;
            if (symbol === this.currentSymbol && Number.isFinite(midAny)) {
                this.currentMidPrice = midAny;
                this.onTick(midAny, timestamp || Date.now(), symbol);
                shouldRefreshChart = true;
                this.updateToolbarPnlForCurrentSymbol();
            }
        }

        if (shouldRefreshChart) {
            this.refreshChartFromBaseBuffer();
            this.lastUpdateTimestamp = Date.now();
            this.updateStatusBar();
            this.updateAccountKPIsRealtime();
        }
    }

    isWsHealthy() {
        try {
            const connected = !!(this.wsClient && this.wsClient.connected);
            const last = Number(this._wsLastTickAt || 0);
            return connected && (Date.now() - last) < 2000;
        } catch (_) { return false; }
    }

    initializeMarketWatch() {
        this.loadFavorites();
        // Leer preferencia de filtro de favoritos
        try { this.favoritesFilterOn = localStorage.getItem('wtFavFilterOn') === '1'; } catch (e) {}
        // Actualizar etiqueta de fuente según estado del feed
        const ds = document.querySelector('.market-watch-header .data-source');
        if (ds) ds.textContent = (this.feedLive ? 'Finage WS LIVE' : 'Finage API');
        const favToggle = document.getElementById('favFilterToggle');
        if (favToggle) {
            favToggle.classList.toggle('active', this.favoritesFilterOn);
            favToggle.addEventListener('click', () => {
                this.favoritesFilterOn = !this.favoritesFilterOn;
                favToggle.classList.toggle('active', this.favoritesFilterOn);
                favToggle.innerHTML = this.favoritesFilterOn ? '<i class="fas fa-star"></i> Solo favoritos' : '<i class="fas fa-star"></i> Solo favoritos';
                try { localStorage.setItem('wtFavFilterOn', this.favoritesFilterOn ? '1' : '0'); } catch (e) {}
                this.updateMarketWatchTable(this.instruments);
                try { this.updatePriceSubscriptions(); } catch (e) {}
            });
        }
        // Búsqueda
        const searchInput = document.getElementById('mwSearch');
        if (searchInput) {
            searchInput.value = this.mwSearch || '';
            let debounce;
            searchInput.addEventListener('input', () => {
                this.mwSearch = searchInput.value.trim();
                try { localStorage.setItem('wtMwSearch', this.mwSearch); } catch (e) {}
                clearTimeout(debounce);
                debounce = setTimeout(() => { 
                    this.updateMarketWatchTable(this.instruments);
                    try { this.updatePriceSubscriptions(); } catch (e) {}
                }, 120);
            });
        }
        // Filtros
        const movingBtn = document.getElementById('mwMovingFilter');
        const lowSpreadBtn = document.getElementById('mwLowSpreadFilter');
        if (movingBtn) {
            movingBtn.classList.toggle('active', !!this.mwFilters.moving);
            movingBtn.setAttribute('aria-pressed', this.mwFilters.moving ? 'true' : 'false');
            movingBtn.addEventListener('click', () => {
                this.mwFilters.moving = !this.mwFilters.moving;
                movingBtn.classList.toggle('active', this.mwFilters.moving);
                movingBtn.setAttribute('aria-pressed', this.mwFilters.moving ? 'true' : 'false');
                try { localStorage.setItem('wtMwFiltMoving', this.mwFilters.moving ? '1' : '0'); } catch (e) {}
                // Cuando se activa Top Movers, ordenar por variación porcentual descendente
                if (this.mwFilters.moving) {
                    this.mwSortKey = 'changePercent';
                    this.mwSortDir = 'desc';
                    this.updateMarketWatchSortIcons();
                }
                this.updateMarketWatchTable(this.instruments);
                try { this.updatePriceSubscriptions(); } catch (e) {}
            });
        }
        if (lowSpreadBtn) {
            lowSpreadBtn.classList.toggle('active', !!this.mwFilters.lowSpread);
            lowSpreadBtn.setAttribute('aria-pressed', this.mwFilters.lowSpread ? 'true' : 'false');
            lowSpreadBtn.addEventListener('click', () => {
                this.mwFilters.lowSpread = !this.mwFilters.lowSpread;
                lowSpreadBtn.classList.toggle('active', this.mwFilters.lowSpread);
                lowSpreadBtn.setAttribute('aria-pressed', this.mwFilters.lowSpread ? 'true' : 'false');
                try { localStorage.setItem('wtMwFiltLowSpread', this.mwFilters.lowSpread ? '1' : '0'); } catch (e) {}
                this.updateMarketWatchTable(this.instruments);
                try { this.updatePriceSubscriptions(); } catch (e) {}
            });
        }
        // Densidad
        const densityToggle = document.getElementById('mwDensityToggle');
        const mwContainer = document.getElementById('market-watch');
        if (densityToggle && mwContainer) {
            const isDense = this.mwDensity === 'dense';
            mwContainer.classList.toggle('dense', isDense);
            densityToggle.setAttribute('aria-pressed', isDense ? 'true' : 'false');
            densityToggle.textContent = isDense ? 'Vista cómoda' : 'Vista compacta';
            densityToggle.addEventListener('click', () => {
                const nextDense = !mwContainer.classList.contains('dense');
                mwContainer.classList.toggle('dense', nextDense);
                this.mwDensity = nextDense ? 'dense' : 'comfortable';
                densityToggle.setAttribute('aria-pressed', nextDense ? 'true' : 'false');
                densityToggle.textContent = nextDense ? 'Vista cómoda' : 'Vista compacta';
                try { localStorage.setItem('wtMwDensity', this.mwDensity); } catch (e) {}
            });
        }
        // Ordenación por cabecera
        document.querySelectorAll('#marketWatchTable thead th.sortable').forEach(th => {
            th.addEventListener('click', () => {
                const key = th.dataset.sortKey;
                this.setMarketWatchSort(key);
                this.updateMarketWatchSortIcons();
                this.updateMarketWatchTable(this.instruments);
            });
        });
        this.updateMarketWatchSortIcons();
        // Render inicial y activar suscripciones de precio
        this.updateMarketWatch();
    }

    updateMarketWatchTable(instruments) {
        const listEl = document.getElementById('mwList');
        if (!listEl) return;

        listEl.innerHTML = '';
        const frag = document.createDocumentFragment();
        this.marketWatchRowsBySymbol = {};
        const isFavSym = (s) => {
            const n = this.normalizeSymbol(s);
            return Array.isArray(this.favorites) && this.favorites.includes(n);
        };
        let list = instruments.slice();
        // Favoritos
        if (this.favoritesFilterOn) {
            list = list.filter(i => isFavSym(i.symbol));
        }
        // Búsqueda
        const q = (this.mwSearch || '').toLowerCase();
        if (q) list = list.filter(i => this.searchMatchesInstrument(i, q));
        // Filtros
        if (this.mwFilters.moving) {
            const cutoff = Date.now() - 60000;
            list = list.filter(i => (this.lastTickBySymbol[this.normalizeSymbol(i.symbol)] || 0) >= cutoff);
        }
        if (this.mwFilters.lowSpread) {
            list = list.filter(i => {
                const bid = parseFloat(i.bid), ask = parseFloat(i.ask);
                const pips = this.calculateSpreadPips(this.normalizeSymbol(i.symbol), bid, ask);
                return Number.isFinite(pips) && pips <= this.lowSpreadThresholdPips;
            });
        }
        // Ordenación
        const dir = (this.mwSortDir || 'asc').toLowerCase() === 'desc' ? -1 : 1;
        const key = this.mwSortKey || 'symbol';
        list.sort((a,b) => {
            const favDelta = (!this.favoritesFilterOn) ? (Number(isFavSym(b.symbol)) - Number(isFavSym(a.symbol))) : 0;
            if (favDelta !== 0) return favDelta;
            let av, bv;
            switch (key) {
                case 'price': {
                    const amid = (parseFloat(a.bid) + parseFloat(a.ask)) / 2;
                    const bmid = (parseFloat(b.bid) + parseFloat(b.ask)) / 2;
                    av = Number(amid); bv = Number(bmid);
                    break;
                }
                case 'change': {
                    av = Number(a.change); bv = Number(b.change);
                    break;
                }
                case 'changePercent': {
                    const ap = parseFloat(String(a.changePercent).replace('%',''));
                    const bp = parseFloat(String(b.changePercent).replace('%',''));
                    av = Number(ap); bv = Number(bp);
                    break;
                }
                case 'spread': {
                    av = this.calculateSpreadPips(this.normalizeSymbol(a.symbol), parseFloat(a.bid), parseFloat(a.ask));
                    bv = this.calculateSpreadPips(this.normalizeSymbol(b.symbol), parseFloat(b.bid), parseFloat(b.ask));
                    break;
                }
                default:
                    av = (this.normalizeSymbol(a.symbol) || '').toLowerCase();
                    bv = (this.normalizeSymbol(b.symbol) || '').toLowerCase();
            }
            if (Number.isFinite(av) && Number.isFinite(bv)) return dir * (av - bv);
            return dir * String(av).localeCompare(String(bv));
        });
        const favItems = list.filter(i => isFavSym(i.symbol));
        const groups = {};
        for (const inst of list) {
            const cat = (inst.category || 'fx').toString().toLowerCase();
            if (!groups[cat]) groups[cat] = [];
            groups[cat].push(inst);
        }
        const order = ['forex','fx','indices','commodities','crypto','stocks','etf','popular','otros'];
        let cats = Object.keys(groups).sort((a,b) => {
            const ia = order.indexOf(a), ib = order.indexOf(b);
            if (ia === -1 && ib === -1) return a.localeCompare(b);
            if (ia === -1) return 1;
            if (ib === -1) return -1;
            return ia - ib;
        });
        cats = ['favoritos', ...cats];

        const renderItem = (instrument) => {
            const item = document.createElement('div');
            item.className = 'mw-item';
            item.setAttribute('role','listitem');
            const symNorm = this.normalizeSymbol(instrument.symbol);
            item.dataset.symbol = symNorm;

            const bidVal = parseFloat(instrument.bid);
            const askVal = parseFloat(instrument.ask);
            const midVal = (isFinite(bidVal) && isFinite(askVal)) ? ((bidVal + askVal) / 2) : (isFinite(bidVal) ? bidVal : (isFinite(askVal) ? askVal : NaN));
            const singlePrice = isFinite(midVal) ? this.formatPrice(symNorm, midVal) : (instrument.price ?? '-');
            const ql = (this.mwSearch || '').toLowerCase();
            const displaySymbolRaw = instrument.name ? String(instrument.name) : String(instrument.symbol);
            const symbolText = ql ? this.highlightSearch(displaySymbolRaw, ql) : displaySymbolRaw;
            let changePercentVal = parseFloat(String(instrument.changePercent).replace('%',''));
            if (!Number.isFinite(changePercentVal)) {
                const prevClose = parseFloat(instrument.prevClose);
                const open = parseFloat(instrument.open);
                const ref = Number.isFinite(prevClose) ? prevClose : (Number.isFinite(open) ? open : NaN);
                if (Number.isFinite(ref) && Number.isFinite(midVal)) {
                    changePercentVal = ((midVal - ref) / ref) * 100;
                }
            }
            const changePercentTxt = Number.isFinite(changePercentVal) ? ((changePercentVal >= 0 ? '+' : '') + changePercentVal.toFixed(2) + '%') : '-';
            const trendIcon = Number.isFinite(changePercentVal)
                ? `<span class="trend-icon" aria-hidden="true">${changePercentVal >= 0 ? '▲' : '▼'}</span>`
                : '';

            const header = document.createElement('div');
            header.className = 'mw-item-header';
            header.setAttribute('tabindex','0');
            header.setAttribute('role','button');
            const flagsHtml = this.renderFlagStackHTML(symNorm);
            header.innerHTML = `
                <div class="mw-item-flags" aria-hidden="true">${flagsHtml}</div>
                <div class="mw-item-symbol" title="${symNorm}${instrument.name ? ' – ' + instrument.name : ''}">${symNorm}<span class="mw-updated"></span></div>
                <div class="mw-item-price num">${singlePrice}</div>
                <div class="mw-item-change-percent num ${Number.isFinite(changePercentVal) ? (changePercentVal >= 0 ? 'positive' : 'negative') : ''}" aria-label="${Number.isFinite(changePercentVal) ? ('Diario ' + (changePercentVal >= 0 ? 'positivo ' : 'negativo ') + changePercentVal.toFixed(2) + '%') : ''}">${trendIcon}${changePercentTxt}</div>
            `;

            const isFav = isFavSym(instrument.symbol);
            const actions = document.createElement('div');
            actions.className = 'mw-item-actions';
            actions.innerHTML = `
                <div class="actions-content">
                    <div class="order-card">
                        <div class="order-main">
                            <button class="order-btn sell" title="Venta">VENTA<br><span class="order-price">${instrument.bid ? this.formatPrice(symNorm, parseFloat(instrument.bid)) : '-'}</span></button>
                            <div class="order-size">
                                <div class="size-value">0.01</div>
                                <div class="size-sub">≈ 1 149 USD</div>
                                <div class="size-controls">
                                    <button class="btn size-dec" title="Reducir">−</button>
                                    <button class="btn size-inc" title="Aumentar">+</button>
                                </div>
                            </div>
                            <button class="order-btn buy" title="Compra">COMPRA<br><span class="order-price">${instrument.ask ? this.formatPrice(symNorm, parseFloat(instrument.ask)) : '-'}</span></button>
                        </div>
                        <div class="order-footer">
                            <button class="order-advanced-btn" title="Orden avanzada">
                                <i class="fas fa-sliders-h"></i>
                                <span>Orden avanzada</span>
                            </button>
                            <button class="favorite-toggle${isFav ? ' favorite-active' : ''}" title="Favorito">
                                <i class="fas fa-star"></i>
                            </button>
                        </div>
                        <div class="order-advanced-panel" data-symbol="${symNorm}">
                            <button class="adv-close" title="Cerrar">×</button>
                            <div class="adv-row">
                                <div class="adv-label">Volumen</div>
                            </div>
                            <div class="inline-controls vol-controls active">
                                <button class="btn vol-dec">−</button>
                                <input type="text" class="vol-input calc-input display-main" value="0.01"/>
                                <button class="btn vol-inc">+</button>
                            </div>
                            <div class="unit-under vol-approx">— USD</div>
                            <div class="percent-bar"><div class="percent-fill" style="width:0%"></div><span class="percent-label">0%</span></div>

                            <div class="adv-row" style="margin-top:12px">
                                <div class="adv-label">Tipo de orden</div>
                                <div class="calc-select-wrap">
                                    <select class="calc-select type-mode">
                                        <option value="market" selected>Market</option>
                                        <option value="limit">Limit</option>
                                        <option value="stop">Stop</option>
                                    </select>
                                    <i class="chev fas fa-chevron-down"></i>
                                </div>
                            </div>
                            <div class="inline-controls entry-controls" style="display:none">
                                <button class="btn entry-dec">−</button>
                                <input type="text" class="entry-price calc-input display-main" value="" placeholder="Precio de entrada"/>
                                <button class="btn entry-inc">+</button>
                            </div>
                            <div class="toggle">
                                <label class="switch"><input type="checkbox" class="sl-toggle" id="slToggle_${symNorm}"/><span class="slider"></span></label>
                                <span class="toggle-label">Stop Loss</span>
                                <div class="calc-select-wrap">
                                <select class="calc-select sl-mode">
                                    <option value="precio">Precio</option>
                                    <option value="valor">Valor</option>
                                    <option value="porcentaje">Porcentaje</option>
                                    <option value="puntos">Puntos</option>
                                </select>
                                <i class="chev fas fa-chevron-down"></i>
                                </div>
                            </div>
                            <div class="inline-controls sl-controls" style="display:none">
                                <button class="btn sl-dec">−</button>
                                <input type="text" class="sl-price calc-input" value="" placeholder="Precio"/>
                                <button class="btn sl-inc">+</button>
                                <input type="text" class="sl-value calc-input" value="" placeholder="USD" style="display:none"/>
                                <input type="text" class="sl-percent calc-input" value="" placeholder="%" style="display:none"/>
                                <input type="text" class="sl-points calc-input" value="" placeholder="Puntos" style="display:none"/>
                                
                            </div>
                            <div class="metric-row sl-metrics" style="display:none">
                                <span class="neg">▼</span>
                                <span class="sl-amount">0.00 USD</span>
                                <span class="sl-percent-val">0.00%</span>
                                <span class="sl-pips">0 Puntos</span>
                            </div>
                            <div class="toggle">
                                <label class="switch"><input type="checkbox" class="tp-toggle" id="tpToggle_${symNorm}"/><span class="slider"></span></label>
                                <span class="toggle-label">Take Profit</span>
                                <div class="calc-select-wrap">
                                <select class="calc-select tp-mode">
                                    <option value="precio">Precio</option>
                                    <option value="valor">Valor</option>
                                    <option value="porcentaje">Porcentaje</option>
                                    <option value="puntos">Puntos</option>
                                </select>
                                <i class="chev fas fa-chevron-down"></i>
                                </div>
                            </div>
                            <div class="inline-controls tp-controls" style="display:none">
                                <button class="btn tp-dec">−</button>
                                <input type="text" class="tp-price calc-input" value="" placeholder="Precio"/>
                                <button class="btn tp-inc">+</button>
                                <input type="text" class="tp-value calc-input" value="" placeholder="USD" style="display:none"/>
                                <input type="text" class="tp-percent calc-input" value="" placeholder="%" style="display:none"/>
                                <input type="text" class="tp-points calc-input" value="" placeholder="Puntos" style="display:none"/>
                                
                            </div>
                            <div class="metric-row tp-metrics" style="display:none">
                                <span class="pos">▲</span>
                                <span class="tp-amount">0.00 USD</span>
                                <span class="tp-percent-val">0.00%</span>
                                <span class="tp-pips">0 Puntos</span>
                            </div>
                            <div class="summary">
                                <div class="row"><span>Margen requerido:</span><span class="sum-margin">—</span></div>
                                <div class="row"><span>Fondos libres:</span><span class="sum-funds">—</span></div>
                                <div class="row"><span>Diferencial:</span><span class="sum-spread">—</span></div>
                                <div class="row"><span>Comisión:</span><span class="sum-commission">0.00 USD</span></div>
                            </div>
                            <div class="adv-exec">
                                <button class="exec-btn sell adv-exec-sell" title="Venta">
                                    VENTA<br>
                                    <span class="exec-price">${instrument.bid ? this.formatPrice(symNorm, parseFloat(instrument.bid)) : '-'}</span>
                                </button>
                                <button class="exec-btn buy adv-exec-buy" title="Compra">
                                    COMPRA<br>
                                    <span class="exec-price">${instrument.ask ? this.formatPrice(symNorm, parseFloat(instrument.ask)) : '-'}</span>
                                </button>
                            </div>
                        </div>
                    </div>
                    <div class="mw-quick-actions">
                        <button class="mw-qa-btn quick-buy" title="Comprar">Compra</button>
                        <button class="mw-qa-btn quick-sell" title="Vender">Venta</button>
                    </div>
                </div>
            `;

            header.addEventListener('click', () => {
                // Selección exclusiva: la propia función selectInstrument expandirá el elemento y contraerá los demás
                this.selectInstrument(symNorm);
            });
            header.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    this.selectInstrument(symNorm);
                }
            });

            const quickBuy = actions.querySelector('.quick-buy');
            const quickSell = actions.querySelector('.quick-sell');
            const favToggle = actions.querySelector('.favorite-toggle');
            const catLower = String(instrument.category||'').toLowerCase();
            const guardMarket = (ev)=>{
                if (catLower==='stocks' && !this.isMarketOpen('stocks')) {
                    ev.stopPropagation();
                    const msgEl = document.getElementById('marketClosedMessage');
                    if (msgEl) msgEl.textContent = 'Mercado cerrado: Acciones USA se operan de 9:30 a 16:00 (ET), Lun–Vie.';
                    try { this.openModal && this.openModal('marketClosedModal'); } catch(e){}
                    return true;
                }
                if (!this.isMarketOpen(catLower)) {
                    ev.stopPropagation();
                    const msgEl = document.getElementById('marketClosedMessage');
                    if (msgEl) msgEl.textContent = 'Mercado cerrado en este momento.';
                    try { this.openModal && this.openModal('marketClosedModal'); } catch(e){}
                    return true;
                }
                return false;
            };
            if (quickBuy) quickBuy.addEventListener('click', (ev) => { if (guardMarket(ev)) return; ev.stopPropagation(); this.quickOrder(symNorm, 'buy'); });
            if (quickSell) quickSell.addEventListener('click', (ev) => { if (guardMarket(ev)) return; ev.stopPropagation(); this.quickOrder(symNorm, 'sell'); });
            // Botón principal de compra/venta (toda el área)
            const mainSellBtn = actions.querySelector('.order-btn.sell');
            const mainBuyBtn = actions.querySelector('.order-btn.buy');
            if (mainSellBtn) mainSellBtn.addEventListener('click', (ev)=>{ if (guardMarket(ev)) return; ev.stopPropagation(); this.quickOrder(symNorm,'sell'); });
            if (mainBuyBtn) mainBuyBtn.addEventListener('click', (ev)=>{ if (guardMarket(ev)) return; ev.stopPropagation(); this.quickOrder(symNorm,'buy'); });
            if (favToggle) favToggle.addEventListener('click', (ev) => { ev.stopPropagation(); this.toggleFavorite(symNorm, favToggle); });

            const advBtn = actions.querySelector('.order-advanced-btn');
            const advPanel = actions.querySelector('.order-advanced-panel');
            const orderMain = actions.querySelector('.order-main');
            const orderFooter = actions.querySelector('.order-footer');
            const quickActionsEl = actions.querySelector('.mw-quick-actions');
            const orderCard = actions.querySelector('.order-card');
            if (advBtn && advPanel) {
                advBtn.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    if (guardMarket(ev)) return;
                    const willOpen = !advPanel.classList.contains('open');
                    if (willOpen) {
                        if (orderMain) { orderMain.classList.add('hidden'); orderMain.setAttribute('aria-hidden','true'); orderMain.style.display = 'none'; }
                        if (orderFooter) { orderFooter.classList.add('hidden'); orderFooter.setAttribute('aria-hidden','true'); orderFooter.style.display = 'none'; }
                        if (quickActionsEl) { quickActionsEl.classList.add('hidden'); quickActionsEl.setAttribute('aria-hidden','true'); quickActionsEl.style.display = 'none'; }
                        if (orderCard) { orderCard.classList.add('adv-open'); }
                        const overlay = document.getElementById('mwAdvOverlay');
                        const mwContainer = document.querySelector('.market-watch-container');
                        if (overlay && mwContainer) {
                            mwContainer.classList.add('adv-mode');
                            const listHeader = document.querySelector('.mw-list-header');
                            if (listHeader) { listHeader.style.display = 'none'; }
                            document.querySelectorAll('.mw-category, .mw-item, .mw-item-header, .mw-category-header').forEach(el=>{ el.style.display='none'; });
                            const mwList = document.getElementById('mwList');
                            if (mwList) {
                                Array.from(mwList.children).forEach(ch=>{ if (ch.id !== 'mwAdvOverlay') ch.style.display = 'none'; });
                                if (mwContainer.__advObserver) { try { mwContainer.__advObserver.disconnect(); } catch(e){} }
                                const obs = new MutationObserver((mutations)=>{
                                    if (!mwContainer.classList.contains('adv-mode')) return;
                                    mutations.forEach(m=>{
                                        m.addedNodes.forEach(n=>{
                                            if (n.nodeType === 1 && n.id !== 'mwAdvOverlay') { n.style.display = 'none'; }
                                        });
                                    });
                                });
                                try { obs.observe(mwList, {childList:true}); } catch(e){}
                                mwContainer.__advObserver = obs;
                            }
                            overlay.innerHTML = '';
                            overlay.appendChild(advPanel);
                            advPanel.__origParent = orderCard;
                            overlay.style.display = 'block';
                        }
                        advPanel.classList.add('open');
                        this.initializeAdvancedPanel(advPanel, symNorm);
                    } else {
                        if (orderMain) { orderMain.classList.remove('hidden'); orderMain.setAttribute('aria-hidden','false'); orderMain.style.display = ''; }
                        if (orderFooter) { orderFooter.classList.remove('hidden'); orderFooter.setAttribute('aria-hidden','false'); orderFooter.style.display = ''; }
                        if (quickActionsEl) { quickActionsEl.classList.remove('hidden'); quickActionsEl.setAttribute('aria-hidden','false'); quickActionsEl.style.display = ''; }
                        if (orderCard) { orderCard.classList.remove('adv-open'); }
                        const overlay = document.getElementById('mwAdvOverlay');
                        const mwContainer = document.querySelector('.market-watch-container');
                        if (overlay && mwContainer) {
                            if (advPanel.__origParent) {
                                advPanel.__origParent.appendChild(advPanel);
                                advPanel.__origParent = null;
                            }
                            overlay.style.display = 'none';
                            overlay.innerHTML = '';
                            mwContainer.classList.remove('adv-mode');
                            document.querySelectorAll('.mw-category, .mw-item, .mw-item-header, .mw-category-header').forEach(el=>{ el.style.display=''; });
                            const listHeader = document.querySelector('.mw-list-header');
                            if (listHeader) { listHeader.style.display = ''; }
                            const mwList = document.getElementById('mwList');
                            if (mwList) {
                                Array.from(mwList.children).forEach(ch=>{ if (ch.id !== 'mwAdvOverlay') ch.style.display = ''; });
                                if (mwContainer.__advObserver) { try { mwContainer.__advObserver.disconnect(); } catch(e){} mwContainer.__advObserver = null; }
                            }
                        }
                        advPanel.classList.remove('open');
                    }
                });
            }
            const execSell = actions.querySelector('.adv-exec-sell');
            const execBuy = actions.querySelector('.adv-exec-buy');
            if (execSell) execSell.addEventListener('click', (ev) => { ev.stopPropagation(); wtPlaceAdvancedOrder(this, symNorm, 'sell', actions); });
            if (execBuy) execBuy.addEventListener('click', (ev) => { ev.stopPropagation(); wtPlaceAdvancedOrder(this, symNorm, 'buy', actions); });

            const sizeDec = actions.querySelector('.size-dec');
            const sizeInc = actions.querySelector('.size-inc');
            const sizeVal = actions.querySelector('.size-value');
            if (sizeDec && sizeInc && sizeVal) {
                const adjust = (d) => {
                    const v = parseFloat(sizeVal.textContent);
                    const nv = Math.max(0.01, Number((v + d).toFixed(2)));
                    sizeVal.textContent = nv.toFixed(2);
                    try {
                        const bidTxt = actions.querySelector('.order-btn.sell .order-price')?.textContent;
                        const askTxt = actions.querySelector('.order-btn.buy .order-price')?.textContent;
                        const bid = parseFloat(bidTxt);
                        const ask = parseFloat(askTxt);
                        this.updateOrderCardEstimates(symNorm, bid, ask);
                    } catch (e) {}
                };
                sizeDec.addEventListener('click', (e) => { e.stopPropagation(); adjust(-0.01); });
                sizeInc.addEventListener('click', (e) => { e.stopPropagation(); adjust(+0.01); });
            }

            const marketOpen = this.isMarketOpen(catLower);
            if (!marketOpen) { actions.querySelectorAll('.order-btn,.mw-qa-btn').forEach(b=>{ b.classList.add('disabled'); }); }
            item.appendChild(header);
            item.appendChild(actions);
            return item;
        };

        const getCatLabel = (catKey) => {
            const k = (catKey || '').toLowerCase();
            if (k === 'fx' || k === 'forex') return 'FX';
            if (k === 'indices' || k === 'index') return 'Indices';
            if (k === 'commodities' || k === 'commodity') return 'Commodities';
            if (k === 'crypto' || k === 'cryptos') return 'Cripto';
            if (k === 'stocks') return 'Acciones';
            if (k === 'etf') return 'ETF';
            if (k === 'popular') return 'Popular';
            if (k === 'favoritos') return 'Favoritos';
            return catKey;
        };

        cats.forEach(cat => {
            const items = (cat === 'favoritos') ? favItems : (groups[cat] || []);
            if (!items.length && cat !== 'favoritos') return;
            const catWrap = document.createElement('div');
            catWrap.className = 'mw-category';
            catWrap.dataset.category = cat;
            const header = document.createElement('div');
            header.className = 'mw-category-header';
            const left = document.createElement('div'); left.className = 'left';
            const icon = document.createElement('i');
            icon.className = (function(){
                const k = cat.toLowerCase();
                if (k==='fx'||k==='forex') return 'fas fa-coins';
                if (k==='indices') return 'fas fa-chart-line';
                if (k==='commodities') return 'fas fa-oil-can';
                if (k==='crypto') return 'fab fa-bitcoin';
                if (k==='stocks') return 'fas fa-building';
                if (k==='etf') return 'fas fa-layer-group';
                if (k==='popular') return 'fas fa-fire';
                if (k==='favoritos') return 'fas fa-star';
                return 'fas fa-circle';
            })();
            const nameSpan = document.createElement('span'); nameSpan.textContent = getCatLabel(cat);
            const badge = document.createElement('span'); badge.className='badge'; badge.textContent = String(items.length);
            left.appendChild(icon); left.appendChild(nameSpan); left.appendChild(badge);
            const chev = document.createElement('i'); chev.className = 'chev fas fa-chevron-down';
            header.appendChild(left); header.appendChild(chev);
            const body = document.createElement('div'); body.className = 'mw-category-body';
            let collapsed = false;
            try {
                const v = localStorage.getItem('wtMwCat_'+cat);
                if (v === null) {
                    const cLower = String(cat).toLowerCase();
                    collapsed = !(cLower === 'favoritos' || cLower === 'forex' || cLower === 'fx');
                } else {
                    collapsed = (v === '0');
                }
            } catch (e) { collapsed = true; }
            if (collapsed) { catWrap.classList.add('collapsed'); }
            const renderCategoryChunk = () => {
                if (body.dataset.rendered === '1') return;
                const renderedSet = new Set();
                const MAX_ITEMS = 100;
                const toRender = items.slice(0, MAX_ITEMS);
                const innerFrag = document.createDocumentFragment();
                toRender.forEach(inst => {
                    const symNorm = this.normalizeSymbol(inst.symbol);
                    if (renderedSet.has(symNorm)) return;
                    renderedSet.add(symNorm);
                    const node = renderItem(inst);
                    innerFrag.appendChild(node);
                    this.marketWatchRowsBySymbol[symNorm] = node;
                });
                body.appendChild(innerFrag);
                body.dataset.rendered = '1';
            };
            header.addEventListener('click', (ev) => {
                const allCats = Array.from(listEl.querySelectorAll('.mw-category'));
                if (ev.shiftKey) {
                    allCats.forEach(w => { w.classList.add('collapsed'); const k = w.dataset.category || ''; try { localStorage.setItem('wtMwCat_'+k, '0'); } catch(e){} });
                    return;
                }
                const isCollapsed = catWrap.classList.toggle('collapsed');
                // Si esta categoría se acaba de expandir, colapsar todas las demás
                if (!isCollapsed) {
                    allCats.forEach(w => {
                        if (w !== catWrap) {
                            w.classList.add('collapsed');
                            const k = w.dataset.category || '';
                            try { localStorage.setItem('wtMwCat_'+k, '0'); } catch(e){}
                        }
                    });
                    if (body.dataset.rendered !== '1') renderCategoryChunk();
                }
                if (isCollapsed) {
                    body.innerHTML = '';
                    body.dataset.rendered = '0';
                }
                try { localStorage.setItem('wtMwCat_'+cat, isCollapsed ? '0' : '1'); } catch (e) {}
            });
            if (!collapsed) { renderCategoryChunk(); }
            catWrap.appendChild(header);
            catWrap.appendChild(body);
            frag.appendChild(catWrap);
        });
        listEl.appendChild(frag);

        try { this.updatePriceSubscriptions(); } catch (e) {}
        try { this.prefetchMarketWatchPrices(); } catch (e) {}
    }

    isMarketOpen(category) {
        const cat = String(category||'').toLowerCase();
        if (cat === 'stocks') {
            try {
                const now = new Date();
                const fmt = new Intl.DateTimeFormat('en-US',{timeZone:'America/New_York',hour:'2-digit',minute:'2-digit',hour12:false,weekday:'short'});
                const parts = fmt.formatToParts(now);
                const hh = Number(parts.find(p=>p.type==='hour')?.value||'0');
                const mm = Number(parts.find(p=>p.type==='minute')?.value||'0');
                const wd = parts.find(p=>p.type==='weekday')?.value||'Mon';
                const mins = hh*60+mm;
                const open = 9*60+30, close = 16*60; // 9:30–16:00 ET
                const isWeekday = ['Mon','Tue','Wed','Thu','Fri'].includes(wd);
                return isWeekday && mins>=open && mins<close;
            } catch(e) { return true; }
        }
        return true;
    }

    selectInstrument(symbol) {
        const prevSymbol = this.currentSymbol;
        // Colapsar y deseleccionar todos los elementos
        document.querySelectorAll('.mw-item').forEach(item => {
            item.classList.remove('selected');
            item.classList.remove('expanded');
        });

        // Seleccionar y expandir el instrumento actual
        const item = document.querySelector(`.mw-item[data-symbol="${symbol}"]`);
        if (item) {
            item.classList.add('selected');
            item.classList.add('expanded');
        }

        // Actualizar estado interno y gráfico con el nuevo instrumento
        this.currentSymbol = symbol;
        // Actualizar encabezado (símbolo y precio) inmediatamente
        try {
            const symEl = document.getElementById('current-symbol');
            if (symEl) symEl.textContent = this.normalizeSymbol(symbol);
            const priceEl = document.getElementById('current-price');
            const changeEl = document.getElementById('current-change');
            const tick = this.latestTicks && this.latestTicks.get(symbol);
            let mid = null;
            if (tick && Number.isFinite(parseFloat(tick.bid)) && Number.isFinite(parseFloat(tick.ask))) {
                mid = (parseFloat(tick.bid) + parseFloat(tick.ask)) / 2;
            } else {
                const inst = this.getInstrument(symbol) || {};
                const bid = parseFloat(inst.bid);
                const ask = parseFloat(inst.ask);
                mid = (Number.isFinite(bid) && Number.isFinite(ask)) ? ((bid + ask) / 2) : (Number.isFinite(bid) ? bid : (Number.isFinite(ask) ? ask : null));
            }
            if (priceEl && Number.isFinite(mid)) priceEl.textContent = this.formatPrice(symbol, mid);
            if (changeEl) {
                const cp = tick && tick.changePercent;
                const ch = tick && tick.change;
                const percentValue = Number.isFinite(parseFloat(cp)) ? parseFloat(cp) : null;
                const absChange = Number.isFinite(parseFloat(ch)) ? Math.abs(parseFloat(ch)) : null;
                const sign = Number.isFinite(parseFloat(ch)) ? (parseFloat(ch) >= 0 ? '+' : '') : '';
                const percentTxt = (percentValue!=null) ? ((percentValue>=0?'+':'') + percentValue.toFixed(2) + '%') : '-';
                const changeTxt = (absChange!=null) ? `${sign}${this.formatPrice(symbol, absChange)} (${percentTxt})` : '-';
                changeEl.textContent = changeTxt;
                changeEl.classList.remove('positive','negative');
                if (percentValue!=null) changeEl.classList.add(percentValue>=0 ? 'positive' : 'negative');
            }
            // Banderas
            this.renderSymbolFlags(symbol);
        } catch (e) {}
        // Persistir símbolo
        try { if (typeof localStorage !== 'undefined') localStorage.setItem('wtSymbol', this.normalizeSymbol(symbol)); } catch(_) {}
        this.loadChartData(symbol);

        // Re-suscribir velas para el nuevo símbolo
        try { this.subscribeCandles(this.currentSymbol, this.currentTimeframe); } catch (e) {}

        // Actualizar suscripciones de precio (añadir nuevo símbolo y retirar el anterior si no es favorito)
        try { this.updatePriceSubscriptions(); } catch (e) {}

        // Actualizar panel de nueva orden
        const orderSymbolInput = document.getElementById('orderSymbol');
        if (orderSymbolInput) orderSymbolInput.value = symbol;
    }

    async loadChartData(symbol) {
        try {
            console.log(`📊 Cargando datos del gráfico para ${symbol}`);
            this.currentSymbol = symbol;
            this.toggleChartSkeleton(true);
            // Prefetch desde caché local si existe para evitar delay inicial
            try {
                const key = `wtCandles:${this.normalizeSymbol(symbol)}:${this.currentTimeframe}`;
                const cached = (typeof localStorage !== 'undefined') ? localStorage.getItem(key) : null;
                if (cached) {
                    const arr = JSON.parse(cached);
                    if (Array.isArray(arr) && arr.length && this.charts.main) {
                        const chart = this.charts.main;
                        if (chart.config.type === 'candlestick') {
                            chart.data.datasets[0].label = symbol;
                            chart.data.datasets[0].data = arr;
                        } else {
                            chart.data.datasets[0].label = symbol;
                            chart.data.labels = arr.map(c => new Date(c.x));
                            chart.data.datasets[0].data = arr.map(c => c.c);
                        }
                        chart.update('none');
                        this.setViewRangeCenteredOnLast(arr);
                        this.toggleChartSkeleton(false);
                    }
                }
            } catch(_) {}
            // Intentar obtener velas reales
            let candles = await this.fetchCandles(symbol, this.currentTimeframe);
            if (!Array.isArray(candles) || candles.length === 0) {
                candles = [];
            }
            // Si timeframe=1, sembrar buffer base
            if (this.currentTimeframe === 1) {
                this.baseMinuteCandles = this.normalizeMinuteCandles(candles).slice();
            } else {
                // Asegurar buffer base 1M para agregación en otros timeframes
                if (!Array.isArray(this.baseMinuteCandles) || !this.baseMinuteCandles.length) {
                    try {
                        const m1 = await this.fetchCandles(symbol, 1);
                        if (Array.isArray(m1) && m1.length) this.baseMinuteCandles = this.normalizeMinuteCandles(m1).slice();
                    } catch(_) {}
                }
            }
            // Actualizar dataset del gráfico (agregar si timeframe != 1)
            if (this.charts.main) {
                const chart = this.charts.main;
                const useAgg = this.currentTimeframe !== 1 && Array.isArray(this.baseMinuteCandles) && this.baseMinuteCandles.length;
                const datasetCandles = useAgg ? this.aggregateCandlesFromBase(this.currentTimeframe) : candles;
                if (chart.config.type === 'candlestick') {
                    chart.data.datasets[0].label = symbol;
                    chart.data.datasets[0].data = datasetCandles;
                    chart.update('none');
                } else {
                    chart.data.datasets[0].label = symbol;
                    chart.data.labels = datasetCandles.map(c => new Date(c.x));
                    chart.data.datasets[0].data = datasetCandles.map(c => c.c);
                    chart.update('none');
                }
            }
            // Recentrar en la última vela al cambiar símbolo
            this.followLast = true;
            this.setViewRangeCenteredOnLast(candles);
            // Ocultar skeleton tras primer render
            this.toggleChartSkeleton(false);
            this.lastUpdateTimestamp = Date.now();
            this.updateStatusBar();
            // Guardar en caché para futuros arranques sin delay
            try {
                const key = `wtCandles:${this.normalizeSymbol(symbol)}:${this.currentTimeframe}`;
                localStorage.setItem(key, JSON.stringify(candles));
            } catch(_) {}
        } catch (error) {
            console.error('❌ Error cargando datos del gráfico:', error);
        }
    }

    initializeOrderPanel() {
        const newOrderBtn = document.getElementById('newOrderBtn');
        const orderTicket = document.getElementById('orderTicket');
        const orderTicketCloseBtn = document.getElementById('orderTicketCloseBtn');
        const orderCancelBtn = document.getElementById('orderCancelBtn');
        const orderForm = document.getElementById('orderForm');

        // Abrir Order Ticket lateral
        if (newOrderBtn && orderTicket) {
            newOrderBtn.addEventListener('click', () => {
                orderTicket.classList.add('active');
                orderTicket.setAttribute('aria-hidden', 'false');
                this.updateOrderPrice();
                this.updateOrderEstimates();
            });
        }

        // Cerrar Order Ticket
        const closeTicket = () => {
            if (orderTicket) {
                orderTicket.classList.remove('active');
                orderTicket.setAttribute('aria-hidden', 'true');
            }
        };

        if (orderTicketCloseBtn) {
            orderTicketCloseBtn.addEventListener('click', closeTicket);
        }
        if (orderCancelBtn) {
            orderCancelBtn.addEventListener('click', closeTicket);
        }

        // Manejar envío de orden
        if (orderForm) {
            orderForm.addEventListener('submit', (e) => {
                e.preventDefault();
                this.submitOrder();
            });
        }

        // Actualizar precio y estimaciones al cambiar tipo de orden
        const orderType = document.getElementById('orderType');
        if (orderType) {
            orderType.addEventListener('change', () => {
                this.updateOrderPrice();
                this.updateOrderEstimates();
            });
        }

        // Actualizar estimaciones al cambiar símbolo, volumen, lado o precio
        const orderSymbolEl = document.getElementById('orderSymbol');
        if (orderSymbolEl) {
            orderSymbolEl.addEventListener('change', () => this.updateOrderEstimates());
        }
        const orderVolumeEl = document.getElementById('orderVolume');
        if (orderVolumeEl) {
            orderVolumeEl.addEventListener('input', () => this.updateOrderEstimates());
        }
        const sideBuyEl = document.getElementById('sideBuy');
        const sideSellEl = document.getElementById('sideSell');
        [sideBuyEl, sideSellEl].forEach(el => el && el.addEventListener('change', () => this.updateOrderEstimates()));
        const orderPriceEl = document.getElementById('orderPrice');
        if (orderPriceEl) {
            orderPriceEl.addEventListener('input', () => this.updateOrderEstimates());
        }

        // Cerrar con ESC
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') closeTicket();
        });
    }

    async submitOrder() {
        const formEl = document.getElementById('orderForm');
        const formData = new FormData(formEl);
        const orderData = Object.fromEntries(formData);

        // Normalizar datos
        const symbol = orderData.symbol;
        const side = orderData.side || 'BUY';
        const type = orderData.type || 'market';
        const volume = parseFloat(orderData.volume || '0') || 0;

        // En modo demo, simular ejecución inmediata y ajustar margen/KPIs
        if (this.demoMode) {
            const marginRequired = this.computeMarginRequired(symbol, side, volume);
            const acc = this.accountData?.account || this.accountData || {};
            const currentMargin = parseFloat(acc?.margin) || 0;
            const balance = parseFloat(acc?.balance) || 10000;
            const newMargin = currentMargin + (marginRequired || 0);

            // Crear posición ejecutada
            const sideUpper = (side || 'BUY').toUpperCase();
            const sideLower = sideUpper.toLowerCase();
            const execPrice = this.getSidePrice(symbol, sideUpper);
            const posId = Date.now();
            this.positions = Array.isArray(this.positions) ? this.positions : [];
            this.positions.push({
                id: posId,
                symbol,
                side: sideLower,
                volume: parseFloat(volume.toFixed(2)),
                open_price: parseFloat(execPrice),
                stop_loss: null,
                take_profit: null,
                profit: 0
            });

            // Recalcular P&L no realizado y KPIs
            let totalUnrealized = 0;
            this.positions.forEach(p => {
                const sU = ((p.side || p.type) || '').toString().toUpperCase();
                const sL = sU.toLowerCase();
                const cp = this.getSidePrice(p.symbol, sU);
                const pnli = this.calculatePnL({ side: sL, open_price: parseFloat(p.open_price ?? p.openPrice), volume: parseFloat(p.volume) }, cp);
                p.profit = pnli;
                totalUnrealized += pnli;
            });
            const equity = balance + totalUnrealized;
            const freeMargin = equity - newMargin;

            // Escribir KPIs
            if (this.accountData?.account) {
                this.accountData.account.margin = newMargin;
                this.accountData.account.equity = equity;
                this.accountData.account.free_margin = freeMargin;
            } else {
                acc.margin = newMargin;
                acc.equity = equity;
                acc.free_margin = freeMargin;
                this.accountData = acc;
            }

            // Actualizar UI
            this.updatePositionsTable(this.positions);
            this.updateAccountInfo();

            // Cerrar ticket y limpiar
            const orderTicketEl = document.getElementById('orderTicket');
            if (orderTicketEl) { orderTicketEl.classList.remove('active'); orderTicketEl.setAttribute('aria-hidden','true'); }
            formEl && formEl.reset();
            this.showNotification('Orden ejecutada (demo) y posición abierta', 'success');
            return;
        }

        // Flujo real vía backend
        try {
            // Incluir account_number seleccionado
            orderData.account_number = this.activeAccountNumber || null;
            const response = await fetch('api/submit-order.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(orderData)
            });

            const result = await response.json();

            if (result.success) {
                this.showNotification('Orden enviada exitosamente', 'success');
                const orderModalEl = document.getElementById('orderModal');
                if (orderModalEl) { orderModalEl.style.display = 'none'; }
                const orderTicketEl = document.getElementById('orderTicket');
                if (orderTicketEl) { orderTicketEl.classList.remove('active'); orderTicketEl.setAttribute('aria-hidden','true'); }
                formEl && formEl.reset();
                
                // Recargar datos desde backend
                await this.loadInitialData();
            } else {
                this.showNotification(result.message || 'Error enviando orden', 'error');
            }

        } catch (error) {
            console.error('❌ Error enviando orden:', error);
            this.showNotification('Error de conexión', 'error');
        }
    }

    updateOrderPrice() {
        const orderType = document.getElementById('orderType').value;
        const priceInput = document.getElementById('orderPrice');
        
        if (orderType === 'market') {
            priceInput.disabled = true;
            priceInput.value = 'Precio de Mercado';
        } else {
            priceInput.disabled = false;
            priceInput.value = '';
            priceInput.placeholder = 'Ingrese precio';
        }
    }

    updateOrderEstimates() {
        const symbolEl = document.getElementById('orderSymbol');
        const volumeEl = document.getElementById('orderVolume');
        const typeEl = document.getElementById('orderType');
        const side = document.querySelector('input[name="side"]:checked')?.value || 'BUY';
        const requiredMarginEl = document.getElementById('required-margin');
        const pipValueEl = document.getElementById('pip-value');

        if (!symbolEl || !volumeEl || !requiredMarginEl || !pipValueEl) return;

        const symbol = symbolEl.value;
        const orderType = typeEl ? typeEl.value : 'market';
        const volume = parseFloat(volumeEl.value) || 0;
        const contractSize = 100000; // lot estándar

        // Obtener instrumento
        const instrumentsList = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        const instrument = instrumentsList.find(i => i.symbol === symbol);

        // Precio actual según lado
        let price = NaN;
        if (instrument) {
            const bid = parseFloat(instrument.bid);
            const ask = parseFloat(instrument.ask);
            price = side === 'BUY' ? ask : bid;
        } else {
            // Fallback: tomar precio del market watch DOM si existe
            const item = document.querySelector(`.mw-item[data-symbol="${symbol}"]`);
            if (item) {
                const priceText = item.querySelector('.mw-item-price')?.textContent || '';
                const p = parseFloat(priceText);
                price = p;
            }
        }

        // Pip size
        const isJpyPair = /JPY$/.test(symbol);
        const pipSize = instrument?.pip_size ? parseFloat(instrument.pip_size) : (isJpyPair ? 0.01 : 0.0001);

        // Margen requerido (por leverage o por margen del instrumento)
        const leverage = this.accountData?.account?.leverage ? parseFloat(this.accountData.account.leverage) : 100;
        const notional = (!Number.isNaN(price) ? price : 0) * contractSize * volume;
        let marginRequired = 0;
        if (instrument?.margin_requirement) {
            const mr = parseFloat(instrument.margin_requirement);
            marginRequired = notional * mr;
        } else {
            marginRequired = leverage > 0 ? (notional / leverage) : 0;
        }

        // Valor del pip por lote en moneda de cuenta (aprox)
        const accountCurrency = this.accountData?.account?.currency || 'USD';
        const quoteCurrency = instrument?.quote_currency || symbol.slice(-3);
        let pipValuePerLotAccount;
        if (accountCurrency === 'USD') {
            if (quoteCurrency === 'USD') {
                pipValuePerLotAccount = 10; // pip por lote en USD
            } else {
                // Aproximación: convertir pip del par a USD usando precio del par
                pipValuePerLotAccount = (!Number.isNaN(price) && price > 0) ? (contractSize * pipSize) / price : (contractSize * pipSize);
            }
        } else {
            // Fallback genérico si no es USD
            pipValuePerLotAccount = contractSize * pipSize;
        }
        const pipValue = pipValuePerLotAccount * volume;

        // Escribir en UI
        if (requiredMarginEl) {
            requiredMarginEl.textContent = `$${(marginRequired || 0).toFixed(2)}`;
        }
        if (pipValueEl) {
            pipValueEl.textContent = `$${(pipValue || 0).toFixed(2)}`;
        }
    }

    initializePositionsPanel() {
        // Datos de ejemplo para posiciones
        const samplePositions = [
            {
                id: 1,
                symbol: 'EURUSD',
                type: 'BUY',
                volume: '0.10',
                openPrice: '1.08234',
                currentPrice: '1.08456',
                pnl: '+22.20',
                pnlPercent: '+0.20%'
            },
            {
                id: 2,
                symbol: 'GBPUSD',
                type: 'SELL',
                volume: '0.05',
                openPrice: '1.27123',
                currentPrice: '1.26789',
                pnl: '+16.70',
                pnlPercent: '+0.26%'
            }
        ];
        // El modo demo fue eliminado; siempre mostrar posiciones reales del backend
        this.updatePositionsTable(this.positions);
    }

    updatePositionsTable(positions) {
        positions = Array.isArray(positions) ? positions : (Array.isArray(this.positions) ? this.positions : []);
        const tbody = document.getElementById('positions-tbody') || document.querySelector('#positionsTable tbody');
        if (!tbody) return;

        if (!positions.length) {
            tbody.innerHTML = '<tr><td class="center" colspan="9">No hay posiciones abiertas</td></tr>';
            return;
        }
        tbody.innerHTML = '';

        positions.forEach(position => {
            const row = document.createElement('tr');
            const sideText = ((position.side || position.type || '') + '').toLowerCase();
            const sideUpper = sideText ? sideText.toUpperCase() : '';
            const sideBadgeClass = sideText === 'buy' ? 'success' : 'danger';
            const openPrice = position.open_price ?? position.openPrice ?? '-';
            const stopLoss = position.stop_loss ?? position.stopLoss ?? null;
            const takeProfit = position.take_profit ?? position.takeProfit ?? null;
            const pnlVal = parseFloat(position.profit ?? position.pnl ?? '');
            const pnlClass = Number.isFinite(pnlVal) ? (pnlVal >= 0 ? 'profit' : 'loss') : '';
            const normSym = this.normalizeSymbol(position.symbol);
            let curPx = this.currentPriceForSymbol(normSym);
            if (!Number.isFinite(curPx)) {
                const last = this.lastPriceBySymbol[normSym];
                if (Number.isFinite(last)) curPx = last;
            }
            if (!Number.isFinite(curPx)) {
                const valuSide = sideUpper === 'BUY' ? 'SELL' : 'BUY';
                const fallback = this.getSidePrice(normSym, valuSide);
                if (Number.isFinite(fallback)) curPx = fallback;
            }
            const curPriceTxt = Number.isFinite(curPx) ? this.formatPrice(position.symbol, curPx) : '-';
            const openedAtRaw = position.opened_at ?? position.openedAt ?? null;
            const openedAtTxt = openedAtRaw ? new Date(openedAtRaw).toLocaleString() : '-';

            let initialPnl = NaN;
            if (Number.isFinite(curPx)) {
                initialPnl = this.calculatePnL({
                    symbol: normSym,
                    side: sideText,
                    open_price: parseFloat(openPrice),
                    volume: parseFloat(position.volume)
                }, curPx);
            }
            const displayPnl = Number.isFinite(initialPnl) ? initialPnl : (Number.isFinite(pnlVal) ? pnlVal : NaN);
            if (Number.isFinite(displayPnl)) this.lastPnlByPositionId[position.id] = displayPnl;

            row.setAttribute('data-position-id', position.id);
            row.innerHTML = `
                <td class="symbol"><div class="sym-wrap"><span class="sym">${position.symbol}</span><span class="opened-at">${openedAtTxt}</span></div></td>
                <td class="center type-${sideText}"><span class="badge badge-${sideBadgeClass}">${sideUpper}</span></td>
                <td class="numeric">${position.volume}</td>
                <td class="numeric">${openPrice}</td>
                <td class="numeric cur-price">${curPriceTxt}</td>
                <td class="numeric">${stopLoss ?? '-'}</td>
                <td class="numeric">${takeProfit ?? '-'}</td>
                <td class="pnl numeric ${Number.isFinite(displayPnl) ? (displayPnl >= 0 ? 'profit' : 'loss') : ''}">$${Number.isFinite(displayPnl) ? displayPnl.toFixed(2) : '—'}</td>
                <td class="actions center">
                    <button class="btn btn-sm btn-outline-danger close-position" data-id="${position.id}">Cerrar</button>
                </td>
            `;

            tbody.appendChild(row);
        });
    }

    updateOrdersTable() {
        // Datos de ejemplo para órdenes pendientes
        const sampleOrders = [
            {
                id: 1,
                symbol: 'USDJPY',
                type: 'BUY LIMIT',
                volume: '0.20',
                price: '148.500',
                status: 'Pendiente'
            }
        ];

        const tbody = document.querySelector('#ordersTable tbody');
        if (!tbody) return;

        tbody.innerHTML = '';
        
        sampleOrders.forEach(order => {
            const row = document.createElement('tr');
            
            row.innerHTML = `
                <td>${order.symbol}</td>
                <td class="type-${order.type.split(' ')[0].toLowerCase()}">${order.type}</td>
                <td>${order.volume}</td>
                <td>${order.price}</td>
                <td><span class="status pending">${order.status}</span></td>
                <td class="actions">
                    <button class="btn btn-sm btn-outline-danger" onclick="webTrader.cancelOrder(${order.id})">
                        <i class="fas fa-times" aria-hidden="true"></i>
                        <span class="sr-only">Cancelar</span>
                    </button>
                </td>
            `;
            
            tbody.appendChild(row);
        });
    }

    async closePosition(positionId) {
        if (!confirm('¿Está seguro de que desea cerrar esta posición?')) {
            return;
        }

        // Modo demo: cierre inmediato con recálculo de KPIs
        if (this.demoMode) {
            const idNum = parseInt(positionId);
            const idx = Array.isArray(this.positions) ? this.positions.findIndex(p => Number(p.id) === idNum) : -1;
            if (idx < 0) {
                this.showNotification('Posición no encontrada (demo)', 'error');
                return;
            }
            const pos = this.positions[idx];
            const sideUpper = ((pos.side || pos.type) || '').toString().toUpperCase();
            const sideLower = sideUpper.toLowerCase();
            // Cerrar al lado opuesto: BUY cierra a BID, SELL cierra a ASK
            const closeSide = sideUpper === 'BUY' ? 'SELL' : 'BUY';
            const currentPrice = this.getSidePrice(pos.symbol, closeSide);
            const pnl = this.calculatePnL({ side: sideLower, open_price: parseFloat(pos.open_price ?? pos.openPrice), volume: parseFloat(pos.volume) }, currentPrice);

            const acc = this.accountData?.account || this.accountData || {};
            let balance = parseFloat(acc?.balance) || 10000;
            let margin = parseFloat(acc?.margin) || 0;
            balance += pnl;
            const mr = this.computeMarginRequired(pos.symbol, sideUpper, parseFloat(pos.volume));
            margin = Math.max(0, margin - (mr || 0));

            // Eliminar posición
            this.positions.splice(idx, 1);

            // Recalcular P&L no realizado de posiciones restantes
            let unrealized = 0;
            this.positions.forEach(p => {
                const sU = ((p.side || p.type) || '').toString().toUpperCase();
                const sL = sU.toLowerCase();
                const cp = this.getSidePrice(p.symbol, sU);
                const pnli = this.calculatePnL({ side: sL, open_price: parseFloat(p.open_price ?? p.openPrice), volume: parseFloat(p.volume) }, cp);
                p.profit = pnli;
                unrealized += pnli;
            });
            const equity = balance + unrealized;
            const freeMargin = equity - margin;

            // Guardar KPIs
            if (this.accountData?.account) {
                this.accountData.account.balance = balance;
                this.accountData.account.margin = margin;
                this.accountData.account.equity = equity;
                this.accountData.account.free_margin = freeMargin;
            } else {
                acc.balance = balance;
                acc.margin = margin;
                acc.equity = equity;
                acc.free_margin = freeMargin;
                this.accountData = acc;
            }

            // Actualizar UI
            const rowSel = `#positions-tbody tr[data-position-id="${idNum}"]`;
            const altSel = `#positionsTable tbody tr[data-position-id="${idNum}"]`;
            const row = document.querySelector(rowSel) || document.querySelector(altSel);
            if (row) row.remove();
            this.updateAccountInfo();
            this.showNotification('Posición cerrada (demo)', 'success');
            return;
        }

        try {
            const response = await fetch('api/close-position.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ positionId, account_number: this.activeAccountNumber || null })
            });

            const result = await response.json();

        if (result.success) {
            this.showNotification('Posición cerrada exitosamente', 'success');
            // Actualización inmediata sin esperar carga completa
            const idNum = parseInt(positionId);
            const idx = Array.isArray(this.positions) ? this.positions.findIndex(p => Number(p.id) === idNum) : -1;
            if (idx >= 0) {
                const closed = this.positions[idx];
                this.positions.splice(idx, 1);
                this.updatePositionsTable(this.positions);
                // Añadir a cerradas en UI si existe el contenedor
                const closedTbody = document.getElementById('closed-tbody');
                if (closedTbody) {
                    const cd = result.close_details || {};
                    const row = document.createElement('tr');
                    const sideText = ((closed.side || closed.type || '') + '').toLowerCase();
                    const sideUpper = sideText ? sideText.toUpperCase() : '';
                    const sideBadgeClass = sideText === 'buy' ? 'success' : 'danger';
                    const pnlVal = parseFloat((cd.pnl || '').replace(/[+,]/g,''));
                    const pnlClass = Number.isFinite(pnlVal) ? (pnlVal >= 0 ? 'profit' : 'loss') : 'profit';
                    row.setAttribute('data-position-id', idNum);
                    row.innerHTML = `
                        <td>${cd.duration ? cd.duration : ''}</td>
                        <td>${closed.symbol}</td>
                        <td><span class="badge badge-${sideBadgeClass}">${sideUpper}</span></td>
                        <td class="numeric">${Number.parseFloat(closed.volume).toFixed(2)}</td>
                        <td class="numeric">${cd.open_price ?? closed.open_price ?? '-'}</td>
                        <td class="numeric">${cd.close_price ?? '-'}</td>
                        <td class="pnl numeric ${pnlClass}">${cd.pnl ?? '$0.00'}</td>
                        <td class="numeric">${cd.commission ?? '0.00'}</td>
                    `;
                    closedTbody.insertBefore(row, closedTbody.firstChild);
                    const closedHeader = document.querySelector('#closed-tab .positions-header h5');
                    if (closedHeader) {
                        const cur = parseInt((closedHeader.textContent.match(/\((\d+)\)/)||[])[1]||'0');
                        closedHeader.textContent = `Operaciones Cerradas (${cur+1})`;
                    }
                }
                const openHeader = document.querySelector('#positions-tab .positions-header h5');
                if (openHeader) {
                    openHeader.textContent = `Posiciones Abiertas (${this.positions.length})`;
                }
            }
            // KPIs inmediatos desde respuesta
            const accUpd = result.account_update || {};
            if (accUpd && (accUpd.new_balance || accUpd.new_equity)) {
                const acc = this.accountData?.account || this.accountData || {};
                const nb = parseFloat(accUpd.new_balance);
                const ne = parseFloat(accUpd.new_equity);
                if (!this.accountData || this.accountData.account) {
                    if (!this.accountData) this.accountData = { account: {} };
                    this.accountData.account = { ...(this.accountData.account||{}), balance: Number.isFinite(nb)?nb:(acc.balance||0), equity: Number.isFinite(ne)?ne:(acc.equity||0) };
                } else {
                    this.accountData.balance = Number.isFinite(nb)?nb:(acc.balance||0);
                    this.accountData.equity = Number.isFinite(ne)?ne:(acc.equity||0);
                }
                this.updateAccountInfo();
            }
        } else {
            this.showNotification(result.message || 'Error cerrando posición', 'error');
        }

        } catch (error) {
            console.error('❌ Error cerrando posición:', error);
            this.showNotification('Error de conexión', 'error');
        }
    }

    closeAllPositions() {
        if (!this.demoMode) {
            this.showNotification('Cierre masivo disponible sólo en demo', 'info');
            return;
        }
        if (!confirm('¿Cerrar todas las posiciones abiertas?')) return;
        if (!Array.isArray(this.positions) || this.positions.length === 0) {
            this.showNotification('No hay posiciones abiertas', 'info');
            return;
        }

        const acc = this.accountData?.account || this.accountData || {};
        let balance = parseFloat(acc?.balance) || 10000;
        let margin = parseFloat(acc?.margin) || 0;

        this.positions.forEach(p => {
            const sU = ((p.side || p.type) || '').toString().toUpperCase();
            const sL = sU.toLowerCase();
            const cp = this.getSidePrice(p.symbol, sU);
            const pnli = this.calculatePnL({ side: sL, open_price: parseFloat(p.open_price ?? p.openPrice), volume: parseFloat(p.volume) }, cp);
            balance += pnli;
            const mr = this.computeMarginRequired(p.symbol, sU, parseFloat(p.volume));
            margin = Math.max(0, margin - (mr || 0));
        });

        // Al cerrar todas, no hay P&L no realizado
        this.positions = [];
        const equity = balance;
        const freeMargin = equity - margin;

        if (this.accountData?.account) {
            this.accountData.account.balance = balance;
            this.accountData.account.margin = margin;
            this.accountData.account.equity = equity;
            this.accountData.account.free_margin = freeMargin;
        } else {
            acc.balance = balance;
            acc.margin = margin;
            acc.equity = equity;
            acc.free_margin = freeMargin;
            this.accountData = acc;
        }

        this.updatePositionsTable(this.positions);
        this.updateAccountInfo();
        this.showNotification('Todas las posiciones cerradas (demo)', 'success');
    }

    async cancelOrder(orderId) {
        if (!confirm('¿Está seguro de que desea cancelar esta orden?')) {
            return;
        }

        try {
            const response = await fetch('api/cancel-order.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ orderId, account_number: this.activeAccountNumber || null })
            });

            const result = await response.json();

            if (result.success) {
                this.showNotification('Orden cancelada exitosamente', 'success');
                await this.loadInitialData();
            } else {
                this.showNotification(result.message || 'Error cancelando orden', 'error');
            }

        } catch (error) {
            console.error('❌ Error cancelando orden:', error);
            this.showNotification('Error de conexión', 'error');
        }
    }

    updateAccountInfo() {
        if (this.kpiSource === 'backend') return;
        const acc = this.accountData?.account || this.accountData || {};
        const balParsed = parseFloat(acc.balance);
        const balance = Number.isFinite(balParsed) ? balParsed : 0;
        // Beneficio: sumar P&L de posiciones abiertas si está disponible
        let totalPnl = 0;
        if (Array.isArray(this.positions) && this.positions.length) {
            for (const p of this.positions) {
                let val = parseFloat(p?.pnl ?? p?.profit);
                if (!Number.isFinite(val) && this.lastPnlByPositionId) {
                    const prev = this.lastPnlByPositionId[p.id];
                    if (Number.isFinite(prev)) val = prev;
                }
                if (Number.isFinite(val)) totalPnl += val;
            }
        } else if (this.positionsStats && Number.isFinite(this.positionsStats.total_pnl)) {
            totalPnl = parseFloat(this.positionsStats.total_pnl);
        } else if (Number.isFinite(parseFloat(acc.profit_loss))) {
            totalPnl = parseFloat(acc.profit_loss);
        }

        // Equity coherente: balance + P&L no realizado
        const eqParsed = parseFloat(acc.equity);
        const equityCalculated = balance + totalPnl;
        const equity = Number.isFinite(eqParsed) ? eqParsed : equityCalculated;
        const mParsed = parseFloat(acc.margin);
        const margin = Number.isFinite(mParsed) ? mParsed : 0;
        const fmParsed = parseFloat(acc.free_margin);
        const freeMargin = Number.isFinite(fmParsed) ? fmParsed : (equityCalculated - margin);
        const marginLevel = margin > 0 ? (((balance + totalPnl) / margin) * 100) : 0;
        const profit = totalPnl;

        const target = {
            balance: Number(balance),
            equity: Number(balance + totalPnl),
            margin: Number(margin),
            freeMargin: Number((balance + totalPnl) - margin),
            marginLevel: Number(marginLevel),
            profit: Number(profit)
        };
        this.queueKpiRender && this.queueKpiRender(target);
        if (this.queueKpiRender) return;

        // Actualizar resumen en el header (clases existentes)
        const setText = (selector, value) => {
            const el = document.querySelector(selector);
            if (el) el.textContent = `$${Number(value).toFixed(2)}`;
        };
        const setPercent = (selector, value) => {
            const el = document.querySelector(selector);
            if (el) el.textContent = `${Number(value).toFixed(2)}%`;
        };
        const profitEl = document.querySelector('.account-summary .profit');
        if (profitEl) {
            profitEl.textContent = `$${profit.toFixed(2)}`;
            profitEl.classList.toggle('neg', profit < 0);
            profitEl.classList.toggle('pos', profit > 0);
        }
        setText('.account-summary .balance', balance);
        setText('.account-summary .equity', equityCalculated);
        setText('.account-summary .margin', margin);
        setText('.account-summary .free-margin', freeMargin);
        setPercent('.account-summary .margin-level', marginLevel);

        // Mostrar número de cuenta si existe
        const accNum = acc.account_number || acc.accountNumber;
        const accNumEl = document.querySelector('.account-summary .account-number');
        if (accNumEl) {
            accNumEl.textContent = accNum ? `#${accNum}` : '#N/A';
        }
        const selHeader = document.getElementById('accountSelectorHeader');
        if (selHeader && accNum) {
            selHeader.value = accNum;
        }

        // Actualizar badge de cuenta en el header (si existe)
        const badgeNum = document.getElementById('accountNumberBadge');
        if (badgeNum) {
            badgeNum.textContent = accNum ? `#${accNum}` : '#N/A';
        }
        const typeBadge = document.getElementById('accountTypeBadge');
        if (typeBadge) {
            const rawType = (acc.account_type || acc.type || '').toString().toLowerCase();
            const typeLabel = (rawType === 'standard' || rawType === 'estadar') ? 'Estándar' : (rawType ? (rawType.charAt(0).toUpperCase() + rawType.slice(1)) : 'Demo');
            typeBadge.textContent = typeLabel;
        }

        // Si existen elementos específicos para margin level, actualizarlos
        const mlEl = document.getElementById('accountMarginLevel');
        if (mlEl) {
            const text = margin > 0 ? `${marginLevel.toFixed(2)}%` : '—';
            mlEl.textContent = text;
            mlEl.setAttribute('title', `Margin Level: ${margin > 0 ? marginLevel.toFixed(2)+'%' : 'sin margen usado'}`);
            mlEl.classList.remove('good','warn','bad');
            if (margin > 0) {
                if (marginLevel >= 200) mlEl.classList.add('good');
                else if (marginLevel >= 100) mlEl.classList.add('warn');
                else mlEl.classList.add('bad');
            }
        }

        // Estado textual al lado de Margin Level y recomendaciones
        const mlStateEl = document.getElementById('mlStateText');
        const mlStateIcon = document.getElementById('mlStateIcon');
        const riskStateEl = document.getElementById('accountRiskState');
        const currencyBadge = document.getElementById('accountCurrencyBadge');
        const recLink = document.getElementById('mlRecommendationsLink');
        // Determinar estado
        let stateKey = 'ok';
        let stateLabel = 'OK';
        if (margin <= 0) { stateKey = 'ok'; stateLabel = 'Sin margen'; }
        else if (marginLevel >= 200) { stateKey = 'ok'; stateLabel = 'OK'; }
        else if (marginLevel >= 100) { stateKey = 'warn'; stateLabel = 'Atención'; }
        else { stateKey = 'risk'; stateLabel = 'Riesgo'; }
        // Actualizar texto y clases al lado del Margin Level
        if (mlStateEl) {
            mlStateEl.textContent = stateLabel;
            mlStateEl.classList.remove('ok','warn','risk');
            mlStateEl.classList.add(stateKey);
        }
        // Actualizar icono de estado al lado del Margin Level
        if (mlStateIcon) {
            const iconMap = { ok: '✓', warn: '⚠', risk: '⛔' };
            mlStateIcon.textContent = iconMap[stateKey] || '•';
            mlStateIcon.className = `ml-icon ${stateKey}`;
            mlStateIcon.setAttribute('aria-label', `Estado: ${stateLabel}`);
        }
        // Actualizar mini-estado junto al selector/badge
        if (riskStateEl) {
            riskStateEl.textContent = stateLabel;
            riskStateEl.classList.remove('ok','warn','risk');
            riskStateEl.classList.add(stateKey);
        }
        // Actualizar iconos de riesgo (pueden existir múltiples en header/badge)
        document.querySelectorAll('.risk-icon').forEach(icon => {
            const iconMap = { ok: '✓', warn: '⚠', risk: '⛔' };
            icon.textContent = iconMap[stateKey] || '•';
            icon.className = `risk-icon ${stateKey}`;
            icon.setAttribute('aria-label', `Estado: ${stateLabel}`);
        });
        // Actualizar moneda en badge si existe
        if (currencyBadge) {
            const curr = acc.currency || acc.account_currency || 'USD';
            currencyBadge.textContent = curr;
        }
        // Enlace de recomendaciones (abre modal y muestra contenido contextual)
        if (recLink) {
            // Guardar estado actual en dataset para uso del handler
            recLink.dataset.state = stateKey;
            if (!recLink.dataset.bound) {
                recLink.addEventListener('click', (e) => {
                    e.preventDefault();
                    const st = recLink.dataset.state || 'ok';
                    const titleEl = document.getElementById('mlRecTitle');
                    const introEl = document.getElementById('mlRecIntro');
                    const listEl = document.getElementById('mlRecList');
                    if (titleEl) titleEl.textContent = 'Recomendaciones de Margen';
                    if (introEl) {
                        introEl.textContent = (st === 'risk')
                          ? 'Tu margen está en zona de riesgo. Estas acciones pueden ayudarte a estabilizar la cuenta.'
                          : (st === 'warn')
                              ? 'Tu margen requiere atención. Considera estas medidas preventivas.'
                              : 'Tu margen está estable. Mantén buenas prácticas de gestión.';
                    }
                    if (listEl) {
                        listEl.innerHTML = '';
                        const items = (st === 'risk')
                          ? [
                              'Cerrar o reducir posiciones con mayor pérdida latente.',
                              'Evitar nuevas órdenes y reducir apalancamiento inmediato.',
                              'Agregar fondos si planeas mantener exposición actual.'
                            ]
                          : (st === 'warn')
                            ? [
                                'Reducir tamaño de nuevas órdenes.',
                                'Revisar margen disponible antes de abrir operaciones.',
                                'Establecer alertas de margen para actuar con anticipación.'
                              ]
                            : [
                                'Mantener disciplina de riesgo y tamaños consistentes.',
                                'Usar stop-loss y revisar volatilidad de instrumentos.',
                                'Evitar concentrar exposición en pocos activos.'
                              ];
                        items.forEach(text => {
                            const li = document.createElement('li');
                            li.textContent = text;
                            listEl.appendChild(li);
                        });
                    }
                    this.openModal('mlRecommendationsModal');
                });
                recLink.dataset.bound = '1';
            }
        }
        const mlCaret = document.getElementById('mlCaret');
        if (mlCaret && !mlCaret.dataset.bound) {
            mlCaret.addEventListener('click', (e)=>{ e.preventDefault(); this.openModal('mlRecommendationsModal'); });
            mlCaret.dataset.bound = '1';
        }

        // Sincronizar valores en modales de depósito/retiro si están presentes
        const auxItems = document.querySelectorAll('.balance-info .balance-item');
        auxItems.forEach(item => {
            const labelText = (item.querySelector('.label')?.textContent || '').toLowerCase();
            const valEl = item.querySelector('.value');
            if (!valEl) return;
            let val = null;
            if (labelText.includes('balance actual')) val = balance;
            else if (labelText.includes('balance disponible')) val = freeMargin;
            else if (labelText.includes('equity')) val = equity;
            else if (labelText.includes('margen usado')) val = margin;
            if (val !== null) valEl.textContent = `$${Number(val).toFixed(2)}`;
        });
    }

    // Obtener el precio actual según el lado
    getSidePrice(symbol, side) {
        const instrumentsList = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        const instrument = instrumentsList.find(i => i.symbol === symbol);
        if (instrument) {
            const bid = parseFloat(instrument.bid);
            const ask = parseFloat(instrument.ask);
            return side === 'BUY' ? ask : bid;
        }
        const row = document.querySelector(`#marketWatchTable [data-symbol="${symbol}"]`)?.closest('tr');
        if (row) {
            const priceText = row.querySelector('.price')?.textContent || '';
            const price = parseFloat(priceText);
            return price;
        }
        return NaN;
    }

    // Determinar dígitos por símbolo (heurística FX)
    digitsForSymbol(symbol) {
        const s = this.normalizeSymbol(symbol);
        const inst = this.getInstrument(s);
        const cat = String(inst?.category||'').toLowerCase();
        if (cat === 'stocks') return 2;
        // Fallback: construir set de símbolos de acciones desde la lista de instrumentos
        if (!this._stocksSet) {
            this._stocksSet = new Set(
                (Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []))
                    .filter(i => String(i?.category||'').toLowerCase()==='stocks')
                    .map(i => this.normalizeSymbol(i.symbol))
            );
        }
        if (this._stocksSet.has(s)) return 2;
        return s.includes('JPY') ? 3 : 5;
    }

    // Formatear precios con dígitos adecuados
    formatPrice(symbol, value) {
        const digits = this.digitsForSymbol(symbol);
        const num = Number(value);
        if (!Number.isFinite(num)) return '-';
        return num.toFixed(digits);
    }

    // Calcular spread en pips (FX heurística)
    calculateSpreadPips(symbol, bid, ask) {
        const b = Number(bid), a = Number(ask);
        if (!Number.isFinite(b) || !Number.isFinite(a)) return NaN;
        const isJpy = /JPY$/.test((symbol || '').toUpperCase());
        const pipSize = isJpy ? 0.01 : 0.0001;
        return Math.abs(a - b) / pipSize;
    }

    // Coincidencia de búsqueda considerando alias
    searchMatchesInstrument(instrument, q) {
        const sym = (instrument.symbol || '').toLowerCase();
        if (sym.includes(q)) return true;
        const aliasTargets = this.searchAliases?.[q];
        if (Array.isArray(aliasTargets)) return aliasTargets.some(t => t.toLowerCase() === sym);
        return false;
    }

    // Resaltar texto coincidente en símbolo
    highlightSearch(text, q) {
        const idx = text.toLowerCase().indexOf(q);
        if (idx < 0) return text;
        const before = text.slice(0, idx);
        const match = text.slice(idx, idx + q.length);
        const after = text.slice(idx + q.length);
        return `${before}<mark>${match}</mark>${after}`;
    }

    // Iconos/flags por símbolo para lista de instrumentos
    flagsForSymbol(symbol) {
        const s = (symbol || '').toUpperCase();
        const map = {
            'EUR':'🇪🇺','USD':'🇺🇸','GBP':'🇬🇧','JPY':'🇯🇵','CHF':'🇨🇭','AUD':'🇦🇺','CAD':'🇨🇦','NZD':'🇳🇿',
            'XAU':'🟡','XAG':'⚪','WTI':'🛢️','BTC':'₿','US':'🇺🇸','GER':'🇩🇪'
        };
        if (s.length >= 6 && /^[A-Z]{6,}$/.test(s)) {
            const b1 = s.slice(0,3); const b2 = s.slice(3,6);
            const f1 = map[b1] || '•'; const f2 = map[b2] || '';
            return `${f1} ${f2}`.trim();
        }
        if (s.startsWith('US')) return map['US'];
        if (s.startsWith('GER')) return map['GER'];
        if (s.startsWith('BTC')) return map['BTC'];
        if (s.startsWith('XAU')) return map['XAU'];
        if (s.startsWith('XAG')) return map['XAG'];
        if (s.startsWith('WTI')) return map['WTI'];
        return '•';
    }

    // Extraer divisas base y cotizada del símbolo (solo FX AAA/BBB)
    parseSymbolCurrencies(symbol) {
        const s = String(symbol || '').toUpperCase();
        if (/^[A-Z]{6}$/.test(s)) {
            return { base: s.slice(0,3), quote: s.slice(3,6) };
        }
        return null;
    }

    // Renderizar HTML de banderas: base arriba superpuesta sobre la cotizada
    renderFlagStackHTML(symbol) {
        const s = String(symbol || '').toUpperCase();
        const stockMap = { 'AMZN':'AMAZON', 'AAPL':'APPLE', 'TSLA':'TESLA', 'GOOGL':'GOOGLE' };
        if (stockMap[s]) {
            const src = `assets/stocks/${stockMap[s]}.svg`;
            return `<img class="logo-stock" src="${src}" alt="${s}"/>`;
        }
        const cq = this.parseSymbolCurrencies(s);
        if (!cq) return '';
        const baseSrc = `assets/currency/${cq.base}.svg`;
        const quoteSrc = `assets/currency/${cq.quote}.svg`;
        return `
            <div class="flag-stack">
                <img class="flag flag-quote" src="${quoteSrc}" alt="${cq.quote}" />
                <img class="flag flag-base" src="${baseSrc}" alt="${cq.base}" />
            </div>
        `;
    }

    // Establecer ordenación MW con persistencia
    setMarketWatchSort(key) {
        if (this.mwSortKey === key) {
            this.mwSortDir = (this.mwSortDir === 'asc') ? 'desc' : 'asc';
        } else {
            this.mwSortKey = key;
            this.mwSortDir = 'asc';
        }
        try {
            localStorage.setItem('wtMwSortKey', this.mwSortKey);
            localStorage.setItem('wtMwSortDir', this.mwSortDir);
        } catch (e) {}
    }

    // Actualizar iconos de ordenación en cabecera
    updateMarketWatchSortIcons() {
        // Nueva UI no tiene cabecera de tabla; no aplicamos iconos
        return;
    }

    // Formatea una marca de tiempo relativa: hace Xs, Xm, Xh
    formatRelativeTime(ts) {
        const t = Number(ts);
        if (!Number.isFinite(t) || t <= 0) return '—';
        const diff = Date.now() - t;
        if (diff < 0) return '—';
        const s = Math.floor(diff / 1000);
        // Evitar mostrar "hace 0s" en el Market Watch
        if (s < 1) return '';
        if (s < 60) return `hace ${s}s`;
        const m = Math.floor(s / 60);
        if (m < 60) return `hace ${m}m`;
        const h = Math.floor(m / 60);
        return `hace ${h}h`;
    }

    // Calcular margen requerido para un símbolo/lado/volumen
    computeMarginRequired(symbol, side, volume) {
        const contractSize = 100000;
        const price = this.getSidePrice(symbol, side);
        const acc = this.accountData?.account || this.accountData || {};
        const leverage = acc?.leverage ? parseFloat(acc.leverage) : 100;
        const notional = (!Number.isNaN(price) ? price : 0) * contractSize * (parseFloat(volume) || 0);
        const instrumentsList = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        const instrument = instrumentsList.find(i => i.symbol === symbol);
        if (instrument?.margin_requirement) {
            const mr = parseFloat(instrument.margin_requirement);
            return notional * mr;
        }
        return leverage > 0 ? (notional / leverage) : 0;
    }

    initializeEventListeners() {
        // Tabs del panel inferior
        document.querySelectorAll('.tab-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const tabId = e.target.dataset.tab;
                this.switchTab(tabId);
            });
        });

        // Cerrar quick actions al hacer clic fuera del market watch
        document.addEventListener('click', (e) => {
            const insideTable = e.target.closest('#marketWatchTable');
            if (!insideTable) {
                document.querySelectorAll('#marketWatchTable tbody tr').forEach(r => r.classList.remove('actions-open'));
            }
        });

        // Theme toggle button
        const themeToggle = document.getElementById('themeToggle');
        if (themeToggle) {
            themeToggle.addEventListener('click', () => {
                this.toggleTheme();
                const themeIcon = document.getElementById('themeIcon');
                if (themeIcon) {
                    themeIcon.classList.remove('spin-once');
                    // Force reflow to restart animation
                    void themeIcon.offsetWidth;
                    themeIcon.classList.add('spin-once');
                }
            });
        }

        // Settings button opens Configuración modal
        const settingsBtn = document.getElementById('settingsBtn');
        if (settingsBtn) {
            settingsBtn.addEventListener('click', (e) => {
                e.preventDefault();
                this.openModal('settingsModal');
            });
        }

        // Initialize theme from localStorage or default to dark
        this.initializeTheme();

        // Selector de timeframe
        this.setupTimeframeSelector();

        // Delegación de eventos para botones Cerrar en posiciones
        const positionsTbody = document.getElementById('positions-tbody') || document.querySelector('#positionsTable tbody');
        if (positionsTbody) {
            positionsTbody.addEventListener('click', (e) => {
                const closeBtn = e.target.closest('.close-position, .btn-close');
                if (closeBtn) {
                    const idAttr = closeBtn.dataset.id || closeBtn.getAttribute('data-id');
                    const row = closeBtn.closest('tr');
                    const id = parseInt(idAttr || row?.getAttribute('data-position-id') || '0');
                    if (id) {
                        this.closePosition(id);
                    }
                }
            });
        }

        // Cerrar todas las posiciones (si existe botón)
        const closeAllBtn = document.getElementById('close-all-btn');
        if (closeAllBtn) {
            closeAllBtn.addEventListener('click', () => this.closeAllPositions());
        }

        // Cambiar contraseña en Configuración
        const pwdBtn = document.getElementById('wtChangePwdBtn');
        if (pwdBtn) {
            pwdBtn.addEventListener('click', async () => {
                const cur = document.getElementById('wtCurrentPassword')?.value || '';
                const nw = document.getElementById('wtNewPassword')?.value || '';
                const cf = document.getElementById('wtConfirmPassword')?.value || '';
                const msgEl = document.getElementById('wtPwdMsg');
                const setMsg = (t, ok=false) => { if (msgEl){ msgEl.textContent=t; msgEl.style.display='block'; msgEl.style.color = ok ? '#16a34a' : '#d93025'; } };
                if (!cur || !nw || !cf) { setMsg('Completa todos los campos'); return; }
                if (nw.length < 6) { setMsg('La nueva contraseña debe tener al menos 6 caracteres'); return; }
                if (nw !== cf) { setMsg('Las contraseñas no coinciden'); return; }
                try {
                    const res = await fetch('api/change_password.php', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        body: new URLSearchParams({ current_password: cur, new_password: nw, confirm_password: cf }).toString()
                    });
                    const json = await res.json().catch(()=>({success:false}));
                    if (json && json.success) {
                        setMsg('Contraseña actualizada', true);
                        this.showNotification('Contraseña actualizada exitosamente', 'success');
                        try { document.getElementById('wtCurrentPassword').value=''; document.getElementById('wtNewPassword').value=''; document.getElementById('wtConfirmPassword').value=''; } catch(_) {}
                    } else {
                        const msg = (json && json.message) ? json.message : 'Error actualizando contraseña';
                        setMsg(msg);
                        this.showNotification(msg, 'error');
                    }
                } catch (e) {
                    setMsg('Error de conexión');
                    this.showNotification('Error de conexión', 'error');
                }
            });
        }
    }

    // Configurar selector de timeframe (dropdown profesional con fallback a botones)
    setupTimeframeSelector() {
        const container = document.querySelector('.timeframe-selector');
        if (!container) return;

        // Forzar timeframe por defecto a 1M siempre
        this.currentTimeframe = 1;
        try { localStorage.setItem('wtTimeframe', '1'); } catch (e) {}

        // Dropdown avanzado
        const btn = container.querySelector('#tfSelectBtn');
        const menu = container.querySelector('#tfMenu');
        const options = menu ? Array.from(menu.querySelectorAll('.tf-option')) : [];
        const currentLabel = container.querySelector('#tfCurrentLabel');

        const closeMenu = () => {
            if (!btn || !menu) return;
            btn.setAttribute('aria-expanded', 'false');
            menu.classList.remove('open');
        };
        const openMenu = () => {
            if (!btn || !menu) return;
            btn.setAttribute('aria-expanded', 'true');
            menu.classList.add('open');
        };
        const setActiveOption = (minutes) => {
            options.forEach(opt => {
                const isActive = parseInt(opt.getAttribute('data-tf'), 10) === minutes;
                opt.classList.toggle('active', isActive);
                opt.setAttribute('aria-selected', isActive ? 'true' : 'false');
            });
            if (currentLabel) currentLabel.textContent = this.timeframeLabelShort(minutes);
        };

        if (btn && menu && options.length) {
            // Inicializar selección actual
            setActiveOption(this.currentTimeframe);

            btn.addEventListener('click', (e) => {
                e.preventDefault();
                const expanded = btn.getAttribute('aria-expanded') === 'true';
                expanded ? closeMenu() : openMenu();
                // Foco al activo o primero
                const active = menu.querySelector('.tf-option.active');
                const target = active || options[0];
                target && target.focus && target.focus();
            });
            btn.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    openMenu();
                    const active = menu.querySelector('.tf-option.active');
                    const target = active || options[0];
                    target && target.focus && target.focus();
                } else if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    const expanded = btn.getAttribute('aria-expanded') === 'true';
                    expanded ? closeMenu() : openMenu();
                } else if (e.key === 'Escape') {
                    closeMenu();
                }
            });

            options.forEach(opt => {
                opt.setAttribute('tabindex', '0');
                opt.addEventListener('click', async () => {
                    const minutes = parseInt(opt.getAttribute('data-tf'), 10);
                    setActiveOption(minutes);
                    closeMenu();
                    await this.setTimeframe(minutes);
                });
                opt.addEventListener('keydown', async (e) => {
                    const idx = options.indexOf(opt);
                    if (e.key === 'ArrowDown') {
                        e.preventDefault();
                        const next = options[Math.min(options.length - 1, idx + 1)];
                        next && next.focus && next.focus();
                    } else if (e.key === 'ArrowUp') {
                        e.preventDefault();
                        const prev = options[Math.max(0, idx - 1)];
                        prev && prev.focus && prev.focus();
                    } else if (e.key === 'Enter') {
                        e.preventDefault();
                        const minutes = parseInt(opt.getAttribute('data-tf'), 10);
                        setActiveOption(minutes);
                        closeMenu();
                        await this.setTimeframe(minutes);
                    } else if (e.key === 'Escape') {
                        e.preventDefault();
                        closeMenu();
                        btn && btn.focus && btn.focus();
                    }
                });
            });

            // Cerrar al hacer clic fuera
            document.addEventListener('click', (e) => {
                if (!container.contains(e.target)) closeMenu();
            });
        } else {
            // Fallback a botones si no hay dropdown
            const buttons = container.querySelectorAll('.tf-btn');
            buttons.forEach(b => {
                const tf = parseInt(b.dataset.tf || b.getAttribute('data-timeframe') || '1', 10);
                const isActive = tf === this.currentTimeframe;
                b.classList.toggle('active', isActive);
                b.setAttribute('aria-pressed', isActive ? 'true' : 'false');
            });
            buttons.forEach(btn => {
                btn.addEventListener('click', () => {
                    const tf = parseInt(btn.dataset.tf || btn.getAttribute('data-timeframe') || '1', 10);
                    this.setTimeframe(tf);
                    buttons.forEach(b => b.classList.remove('active'));
                    btn.classList.add('active');
                    buttons.forEach(b => b.setAttribute('aria-pressed','false'));
                    btn.setAttribute('aria-pressed','true');
                });
            });
        }

        document.querySelectorAll('.tf-seg-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const tf = parseInt(btn.dataset.tf || btn.getAttribute('data-timeframe') || '1', 10);
                this.setTimeframe(tf);
            });
        });
        this.updateStatusBar();
    }

    // Establecer timeframe en minutos y refrescar gráfico con datos reales
    async setTimeframe(minutes) {
        if (!Number.isFinite(minutes) || minutes <= 0) return;
        this.currentTimeframe = minutes;
        this.renderSymbolFlags(this.currentSymbol);
        try { localStorage.setItem('wtTimeframe', String(minutes)); } catch (e) {}
        this.updateStatusBar();
        this.toggleChartSkeleton(true);
        // Limitar reemplazos globales: preservar histórico y fusionar
        let candles = [];
        try {
            candles = await this.fetchCandles(this.currentSymbol, minutes);
        } catch (e) {
            candles = [];
        }
        if (!Array.isArray(candles) || candles.length === 0) {
            if (minutes === 1 && Array.isArray(this.baseMinuteCandles) && this.baseMinuteCandles.length) {
                candles = this.baseMinuteCandles.slice();
            } else {
                candles = [];
            }
        }
        // Asegurar buffer base 1M para agregación si timeframe != 1
        if (minutes !== 1 && (!Array.isArray(this.baseMinuteCandles) || !this.baseMinuteCandles.length)) {
            try { const m1 = await this.fetchCandles(this.currentSymbol, 1); if (Array.isArray(m1) && m1.length) this.baseMinuteCandles = this.normalizeMinuteCandles(m1).slice(); } catch(_) {}
        }
        if (this.charts.main) {
            const chart = this.charts.main;
            const useAgg = minutes !== 1 && Array.isArray(this.baseMinuteCandles) && this.baseMinuteCandles.length;
            const datasetCandles = useAgg ? this.aggregateCandlesFromBase(minutes) : candles;
            if (chart.config.type === 'candlestick') {
                chart.data.datasets[0].label = this.currentSymbol;
                if (minutes === 1 && Array.isArray(chart.data.datasets[0].data)) {
                    const current = chart.data.datasets[0].data.slice();
                    const merged = datasetCandles.slice();
                    chart.data.datasets[0].data = merged.length ? merged : current;
                } else {
                    chart.data.datasets[0].data = datasetCandles;
                }
                chart.update('none');
            } else {
                chart.data.datasets[0].label = this.currentSymbol;
                chart.data.labels = datasetCandles.map(c => new Date(c.x));
                chart.data.datasets[0].data = datasetCandles.map(c => c.c);
                chart.update('none');
            }
        }
        if (minutes === 1) {
            this.baseMinuteCandles = this.normalizeMinuteCandles(candles).slice();
        }
        // Recentrar en la última vela y reiniciar countdown
        this.followLast = true;
        this.setViewRangeCenteredOnLast(candles);
        if (this.autoScaleOn) this.updateYAxisForVisibleRange();
        this.startCandleCountdown();
        this.stopChartHeartbeat();
        this.startChartHeartbeat();
        try { this.subscribeCandles(this.currentSymbol, this.currentTimeframe); } catch (e) {}
        this.toggleChartSkeleton(false);
        this.lastUpdateTimestamp = Date.now();
        this.updateStatusBar();
        this.updateTimeframeUIActive();
        // Guardar en caché para arranque rápido
        try { const key = `wtCandles:${this.normalizeSymbol(this.currentSymbol)}:${this.currentTimeframe}`; localStorage.setItem(key, JSON.stringify(candles)); } catch(_) {}
    }

    // Atajos de teclado para cambiar timeframe y navegación básica
    setupKeyboardShortcuts() {
        document.addEventListener('keydown', (e) => {
            // Evitar interferir con inputs/modals
            const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
            if (tag === 'input' || tag === 'select' || tag === 'textarea') return;

            const map = {
                '1': 1,    // 1M
                '2': 5,    // 5M
                '3': 15,   // 15M
                '4': 30,   // 30M
                '5': 60,   // 1H
                '6': 240,  // 4H
                '7': 1440  // 1D
            };
            if (map[e.key]) {
                this.setTimeframe(map[e.key]);
                // Marcar activo en UI
                const container = document.querySelector('.timeframe-selector');
                if (container) {
                    const buttons = container.querySelectorAll('.tf-btn');
                    buttons.forEach(b => {
                        const tf = parseInt(b.dataset.tf || b.getAttribute('data-timeframe') || '1', 10);
                        const active = tf === this.currentTimeframe;
                        b.classList.toggle('active', active);
                        b.setAttribute('aria-pressed', active ? 'true' : 'false');
                    });
                    const menu = container.querySelector('#tfMenu');
                    const currentLabel = container.querySelector('#tfCurrentLabel');
                    if (menu && currentLabel) {
                        currentLabel.textContent = this.timeframeLabelShort(this.currentTimeframe);
                        menu.querySelectorAll('.tf-option').forEach(opt => {
                            const tf = parseInt(opt.getAttribute('data-tf'), 10);
                            const isActive = tf === this.currentTimeframe;
                            opt.classList.toggle('active', isActive);
                            opt.setAttribute('aria-selected', isActive ? 'true' : 'false');
                        });
                    }
                }
                e.preventDefault();
            }
        });
    }

    // Copiado rápido de símbolo y precio
    setupCopyable() {
        const sym = document.getElementById('current-symbol');
        const price = document.getElementById('current-price');
        const copy = async (text, label) => {
            try {
                await navigator.clipboard.writeText(text);
                this.showNotification(`${label} copiado: ${text}`, 'info');
            } catch (e) {
                this.showNotification('No se pudo copiar al portapapeles', 'warning');
            }
        };
        if (sym) {
            sym.setAttribute('title','Click para copiar símbolo');
            sym.style.cursor = 'pointer';
            sym.addEventListener('click', () => copy(sym.textContent.trim(), 'Símbolo'));
        }
        if (price) {
            price.setAttribute('title','Click para copiar precio');
            price.style.cursor = 'pointer';
            price.addEventListener('click', () => copy(price.textContent.trim(), 'Precio'));
        }
    }

    // Actualizar barra de estado superior: modo, fuente, timeframe y última actualización
    updateStatusBar() {
        const modeEl = document.getElementById('status-mode');
        const sourceEl = document.getElementById('status-source');
        const tfEl = document.getElementById('status-timeframe');
        const lastEl = document.getElementById('status-last-update');
        if (modeEl) modeEl.textContent = 'Live';
        if (sourceEl) {
            if (this.feedLive) {
                sourceEl.textContent = 'Finage WS LIVE';
            } else {
                sourceEl.textContent = 'Finage API';
            }
        }
        if (tfEl) tfEl.textContent = `Agrupando en ${this.timeframeLabel(this.currentTimeframe)}`;
        if (lastEl) {
            const ts = this.lastUpdateTimestamp ? new Date(this.lastUpdateTimestamp) : null;
            lastEl.textContent = ts ? `Última actualización: ${ts.toLocaleTimeString()}` : 'Última actualización: —';
        }
    }

    updateDataSourceLabel() {
        const ds = document.querySelector('.market-watch-header .data-source');
        if (ds) {
            if (this.feedLive) {
                ds.textContent = 'LIVE';
            } else {
                ds.textContent = 'Finage API';
            }
        }
        this.updateStatusBar();
    }

    renderSymbolFlags(symbol = this.currentSymbol) {
        try {
            const el = document.getElementById('symbol-flags');
            if (el) el.innerHTML = this.renderFlagStackHTML(symbol);
        } catch(_) {}
    }

    timeframeLabel(minutes) {
        if (minutes < 60) return `${minutes} ${minutes === 1 ? 'minuto' : 'minutos'}`;
        if (minutes % 1440 === 0) {
            const days = minutes / 1440;
            return `${days} ${days === 1 ? 'día' : 'días'}`;
        }
        const hours = minutes / 60;
        return `${hours} ${hours === 1 ? 'hora' : 'horas'}`;
    }

    // Etiqueta corta (1M,5M,15M,30M,1H,4H,1D) para el dropdown
    timeframeLabelShort(minutes) {
        if (minutes < 60) return `${minutes}M`;
        if (minutes % 1440 === 0) return `${minutes / 1440}D`;
        return `${minutes / 60}H`;
    }

    // Mostrar/ocultar skeleton del gráfico
    toggleChartSkeleton(show) {
        const sk = document.querySelector('.chart-skeleton');
        if (sk) {
            sk.style.display = show ? 'block' : 'none';
        }
    }

    toggleMarketWatchLoader(show) {
        const el = document.getElementById('mwLoader');
        if (el) el.style.display = show ? 'block' : 'none';
    }
    togglePositionsLoader(show) {
        const el = document.getElementById('positionsLoader');
        if (el) el.style.display = show ? 'block' : 'none';
    }
    toggleOrdersLoader(show) {
        const el = document.getElementById('ordersLoader');
        if (el) el.style.display = show ? 'block' : 'none';
    }

    // Actualización de KPIs de cuenta en tiempo real
    updateAccountKPIsRealtime() {
        if (this.kpiSource === 'backend') return;
        try {
            const balance = Number(this.accountData?.account?.balance ?? this.accountData?.balance ?? 0);
            const leverage = Number(this.accountData?.account?.leverage ?? this.accountData?.leverage ?? 100);
            const positions = Array.isArray(this.positions) ? this.positions : [];
            let pnl = 0;
            let margin = 0;
            for (const pos of positions) {
                const sym = this.normalizeSymbol(pos.symbol || '') || this.currentSymbol;
                const inst = this.getInstrument(sym) || {};
                const contractSize = Number(inst.contract_size) || 100000;
                const price = this.currentPriceForSymbol(sym);
                const open = Number(pos.open_price || 0);
                const vol = Number(pos.volume || 0);
                const type = (pos.type || '').toLowerCase();
                const pipSize = Number(inst.pip_size) || (/JPY$/.test(sym) ? 0.01 : 0.0001);
                const pipValuePerLotUSD = this.getPipValuePerLotUSD(sym);
                // P&L aproximado en USD
                const delta = (type === 'buy') ? (price - open) : (open - price);
                const pips = delta / pipSize;
                pnl += pips * pipValuePerLotUSD * vol;
                // Margen requerido por posición
                const notional = Number(price) * contractSize * vol;
                const marginRate = (inst && inst.margin_rate != null) ? Number(inst.margin_rate) : null;
                margin += marginRate && marginRate > 0 ? (notional * marginRate) : (leverage>0 ? (notional/leverage) : 0);
            }
            const equity = balance + pnl;
            const freeMargin = equity - margin;
            const ml = margin > 0 ? (equity / margin * 100) : 0;
            // No escribir a UI si backend controla KPIs
        } catch (e) {}
    }

    currentPriceForSymbol(symbol) {
        const t = this.latestTicks && this.latestTicks.get(symbol);
        if (t && Number.isFinite(t.bid) && Number.isFinite(t.ask)) return (t.bid + t.ask)/2;
        if (symbol === this.currentSymbol && Number.isFinite(this.currentMidPrice)) return this.currentMidPrice;
        return Number.NaN;
    }

    loadFavorites() {
        try {
            const raw = localStorage.getItem('wtFavorites');
            this.favorites = raw ? JSON.parse(raw) : [];
            if (!Array.isArray(this.favorites)) this.favorites = [];
            // normalizar y desduplicar
            const seen = new Set();
            this.favorites = this.favorites
                .map(s => this.normalizeSymbol(s))
                .filter(s => s && !seen.has(s) && seen.add(s));
        } catch (e) {
            this.favorites = [];
        }
    }

    saveFavorites() {
        try { localStorage.setItem('wtFavorites', JSON.stringify(this.favorites)); } catch (e) {}
    }

    toggleFavorite(symbol, toggleBtn) {
        if (!Array.isArray(this.favorites)) this.favorites = [];
        const norm = this.normalizeSymbol(symbol);
        const idx = this.favorites.indexOf(norm);
        if (idx >= 0) {
            this.favorites.splice(idx, 1);
            if (toggleBtn) toggleBtn.classList.remove('favorite-active');
            this.showNotification(`${norm} retirado de favoritos`, 'info');
        } else {
            this.favorites.push(norm);
            if (toggleBtn) toggleBtn.classList.add('favorite-active');
            this.showNotification(`${norm} agregado a favoritos`, 'success');
        }
        this.saveFavorites();
        this.updateMarketWatchTable(this.instruments);
        // Reajustar suscripciones de precio según nuevos favoritos
        try { this.updatePriceSubscriptions(); } catch (e) {}
    }

    switchTab(tabId) {
        // Remover clase activa de todos los tabs
        const allBtns = document.querySelectorAll('.tab-btn');
        allBtns.forEach(btn => {
            btn.classList.remove('active');
            btn.setAttribute('aria-selected','false');
            btn.setAttribute('tabindex','-1');
        });
        document.querySelectorAll('.tab-content').forEach(content => {
            content.classList.remove('active');
        });

        // Activar tab seleccionado
        const btn = document.querySelector(`[data-tab="${tabId}"]`);
        const panel = document.getElementById(`${tabId}-tab`);
        if (btn) {
            btn.classList.add('active');
            btn.setAttribute('aria-selected','true');
            btn.setAttribute('tabindex','0');
            btn.focus();
        }
        if (panel) panel.classList.add('active');
    }

    quickOrder(symbol, type) {
        const side = String(type || 'buy').toLowerCase();
        const row = this.marketWatchRowsBySymbol?.[this.normalizeSymbol(symbol)] || null;
        const actionsRoot = row ? row.querySelector('.mw-item-actions') : null;
        const sizeValEl = actionsRoot?.querySelector('.size-value');
        const sellPriceEl = actionsRoot?.querySelector('.order-btn.sell .order-price');
        const buyPriceEl = actionsRoot?.querySelector('.order-btn.buy .order-price');
        const volume = parseFloat(sizeValEl?.textContent || '0.01');
        const bid = parseFloat(sellPriceEl?.textContent || '0');
        const ask = parseFloat(buyPriceEl?.textContent || '0');
        const price = side === 'buy' ? ask : bid;
        const payload = {
            account_number: this.activeAccountNumber || null,
            orderSymbol: this.normalizeSymbol(symbol),
            orderType: 'market',
            orderSide: side,
            orderVolume: volume,
            orderPrice: price,
            orderStopLoss: null,
            orderTakeProfit: null,
            orderComment: 'Quick Order'
        };
        try {
            const disabled = (typeof localStorage !== 'undefined') && localStorage.getItem('wt_quick_confirm_disabled') === '1';
            if (disabled) {
                this.executeOrderPayload(payload);
            } else {
                this.showQuickOrderConfirm(payload);
            }
        } catch (_) {
            this.executeOrderPayload(payload);
        }
    }

    showQuickOrderConfirm(payload) {
        try {
            const symEl = document.getElementById('qoSymbol');
            const sideEl = document.getElementById('qoSide');
            const volEl = document.getElementById('qoVolume');
            const priceEl = document.getElementById('qoPrice');
            const chk = document.getElementById('qoDontShow');
            const chkWrap = chk ? chk.closest('.wt-checkbox') : null;
            const btn = document.getElementById('qoConfirmBtn');
            if (symEl) symEl.textContent = payload.orderSymbol;
            if (sideEl) sideEl.textContent = String(payload.orderSide).toUpperCase();
            if (volEl) volEl.textContent = String(payload.orderVolume);
            if (priceEl) priceEl.textContent = String(payload.orderPrice);
            if (chk) {
                try { chk.checked = localStorage.getItem('wt_quick_confirm_disabled') === '1'; } catch(_){}
                if (chkWrap) { chkWrap.classList.toggle('checked', !!chk.checked); chk.setAttribute('aria-checked', chk.checked ? 'true' : 'false'); }
                chk.addEventListener('change', ()=>{ 
                    try { localStorage.setItem('wt_quick_confirm_disabled', chk.checked ? '1':'0'); } catch(_){} 
                    if (chkWrap) { chkWrap.classList.toggle('checked', !!chk.checked); chk.setAttribute('aria-checked', chk.checked ? 'true' : 'false'); }
                });
            }
            if (btn) {
                btn.onclick = () => {
                    this.executeOrderPayload(payload);
                    try { this.closeModal && this.closeModal('quickOrderConfirmModal'); } catch(_){}
                };
            }
            try { this.openModal && this.openModal('quickOrderConfirmModal'); } catch(_){}
        } catch (e) {
            this.executeOrderPayload(payload);
        }
    }

    async executeOrderPayload(payload) {
        try {
            const resp = await fetch('api/submit-order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
            const ct = (resp.headers && resp.headers.get('Content-Type')) || '';
            let json = null; let rawText = '';
            if (ct.indexOf('application/json') !== -1) { try { json = await resp.json(); } catch(_){} } else { try { rawText = await resp.text(); } catch(_){} }
            if (!json || !json.success) {
                const msg = (json && (json.message || json.error)) || (rawText ? 'Respuesta no JSON del servidor' : 'No se pudo ejecutar la orden');
                const details = { payload, response_status: resp.status, response_ok: resp.ok, content_type: ct, body: json || rawText };
                const msgEl = document.getElementById('orderErrorMessage');
                const detEl = document.getElementById('orderErrorDetails');
                if (msgEl) msgEl.textContent = msg;
                if (detEl) detEl.textContent = (typeof details.body === 'string') ? details.body : JSON.stringify(details, null, 2);
                try { this.openModal && this.openModal('orderErrorModal'); } catch(_) {}
                this.showNotification(msg, 'error');
                return;
            }
            const warn = Array.isArray(json.warnings) ? json.warnings : [];
            if (warn.length) {
                const wm = document.getElementById('orderWarnMessage');
                const wd = document.getElementById('orderWarnDetails');
                if (wm) wm.textContent = 'La orden se ejecutó pero hubo advertencias';
                if (wd) wd.textContent = JSON.stringify({ warnings: warn }, null, 2);
                try { this.openModal && this.openModal('orderWarningModal'); } catch(_) {}
            }
            this.showNotification('Orden ejecutada exitosamente', 'success');
            const newPos = { id: json.order_id, symbol: payload.orderSymbol, type: payload.orderSide, volume: payload.orderVolume, open_price: payload.orderPrice, stop_loss: null, take_profit: null, profit: 0 };
            if (!Array.isArray(this.positions)) this.positions = [];
            this.positions.unshift(newPos);
            this.updatePositionsTable(this.positions);
            try { this.updateAccountBalance && this.updateAccountBalance(); } catch(_) {}
        } catch (e) {
            this.showNotification('Error ejecutando orden', 'error');
        }
    }

    startAutoUpdates() {
        const isLive = !!(this.wsClient && (this.wsClient.connected === true || this.feedLive === true));

        if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; }
        if (this.chartUpdateInterval) { clearInterval(this.chartUpdateInterval); this.chartUpdateInterval = null; }

        const cfg0 = (typeof window !== 'undefined' ? (window.webtraderConfig || {}) : {});
        this._restPollBaseMs = Number(cfg0.tick_poll_interval_ms) || 1000;
        this._restPollMaxMs = 10000;
        this._restBackoffFactor = 2;
        if (!Number.isFinite(this._restPollMs)) this._restPollMs = this._restPollBaseMs;

        this.updateDataSourceLabel();
        if (isLive) {
            console.log('🔌 Usando WebSocket para actualizaciones en tiempo real');
            this.feedLive = true;
            try { this.updatePriceSubscriptions(); } catch (e) {}
            this.updateInterval = setInterval(() => {
                try {
                    if (!this.isWsHealthy()) {
                        this.updatePrices();
                        this._restPollMs = Math.min(this._restPollMs * this._restBackoffFactor, this._restPollMaxMs);
                    } else {
                        this._restPollMs = this._restPollBaseMs;
                    }
                } catch (e) {}
            }, Math.max(800, this._restPollMs));
        } else {
            console.log('⚠️ WebSocket no disponible: usando Finage API cada 1s');
            this.feedLive = false;
            this.updateInterval = setInterval(() => { try { this.updatePrices(); } catch (e) {} }, Math.max(500, this._restPollBaseMs));
        }

        // Actualizar cuenta cada 3s para mantener sincronía con CRM
        const accountPeriod = 3000;
        if (this.accountUpdateInterval) { clearInterval(this.accountUpdateInterval); this.accountUpdateInterval = null; }
        if (this.kpiSource !== 'backend') {
            this.accountUpdateInterval = setInterval(() => {
                this.updateAccountBalance();
            }, accountPeriod);
        }

        if (this.mwRefreshInterval) clearInterval(this.mwRefreshInterval);
        const cfg = (typeof window !== 'undefined' ? (window.webtraderConfig || {}) : {});
        const mwMs = Number(cfg.mw_refresh_ms) || 1000;
        this.mwRefreshInterval = setInterval(() => {
            try {
                if (this.latestTicks && this.latestTicks.size) {
                    for (const [sym, t] of this.latestTicks.entries()) {
                        this.updateMarketWatchPrice(sym, t.bid, t.ask, t.change, t.changePercent, t.source);
                    }
                }
            } catch (e) {}
        }, mwMs);
    }

    // Determinar símbolos preferidos: favoritos si existen; si no, defaults
    getPreferredSymbols() {
        const cfg = (typeof window !== 'undefined' && window.webtraderConfig) ? window.webtraderConfig : {};
        const defaultCfg = Array.isArray(cfg.default_symbols) ? cfg.default_symbols.slice() : ['EURUSD','GBPUSD','USDJPY','BTCUSD'];
        let defaults = defaultCfg.map(s => this.normalizeSymbol(s)).filter(Boolean);
        if (!defaults.includes('BTCUSD')) defaults.push('BTCUSD');
        const favs = Array.isArray(this.favorites) ? this.favorites.map(s => this.normalizeSymbol(s)).filter(Boolean) : [];

        // Preferir favoritos; si no hay, usar símbolos visibles del Market Watch; si no, instrumentos; si no, defaults
        let base = favs;
        if (!base.length) {
            try {
                const listEl = (typeof document !== 'undefined') ? document.getElementById('mwList') : null;
                if (listEl) {
                    const visible = Array.from(listEl.querySelectorAll('.mw-item[data-symbol]'))
                        .map(el => this.normalizeSymbol(el.getAttribute('data-symbol')))
                        .filter(Boolean);
                    if (visible.length) base = visible;
                }
            } catch (e) { /* noop */ }
        }
        if (!base.length && Array.isArray(this.instruments) && this.instruments.length) {
            base = this.instruments.map(i => this.normalizeSymbol(i.symbol)).filter(Boolean);
        }
        if (!base.length) base = defaults;
        // Quitar duplicados preservando orden
        const seen = new Set();
        const out = [];
        for (const s of base) { if (s && !seen.has(s)) { seen.add(s); out.push(s); } }
        // Garantizar símbolo activo al frente
        const cur = this.normalizeSymbol(this.currentSymbol);
        if (cur && !seen.has(cur)) {
            out.unshift(cur);
        } else if (cur && out[0] !== cur) {
            // mover a frente si ya estaba
            const idx = out.indexOf(cur);
            if (idx > 0) { out.splice(idx, 1); out.unshift(cur); }
        }
        // Suscribir todos los símbolos visibles/preferidos para actualización en vivo inmediata
        return out;
    }

    // Mantener suscripciones de precios sólo para símbolos preferidos
    updatePriceSubscriptions() {
        const clients = Array.isArray(this.wsClients) && this.wsClients.length ? this.wsClients : (this.wsClient ? [this.wsClient] : []);
        if (!clients.length) return;
        if (!this._priceSubsMap) this._priceSubsMap = new Map();
        const favs = Array.isArray(this.favorites) ? this.favorites.map(s=>this.normalizeSymbol(s)) : [];
        const visibleEls = (typeof document !== 'undefined') ? Array.from(document.querySelectorAll('.mw-item[data-symbol]')) : [];
        const visibleSyms = visibleEls.map(el => this.normalizeSymbol(el.getAttribute('data-symbol'))).filter(Boolean);
        const target = [...new Set([ ...favs, ...visibleSyms ])];
        const targetSet = new Set(target);
        // Desuscribir símbolos que salen
        for (const [sym, idx] of Array.from(this._priceSubsMap.entries())) {
            if (!targetSet.has(sym)) {
                try { clients[idx]?.unsubscribe('prices', sym); } catch (e) {}
                this._priceSubsMap.delete(sym);
            }
        }
        // Suscribir nuevos
        for (const sym of target) {
            if (!this._priceSubsMap.has(sym)) {
                const idx = this.getClientIndexForSymbol(sym);
                try { clients[idx]?.subscribe('prices', sym); } catch (e) {}
                this._priceSubsMap.set(sym, idx);
            }
        }
        // Intentar completar precios faltantes con REST si tras unos segundos siguen vacíos
        clearTimeout(this._missingPriceTimer);
        this._missingPriceTimer = setTimeout(() => { this.ensurePricesForMissingSymbols(); }, 2000);
    }

    async ensurePricesForMissingSymbols() {
        const missing = [];
        const rows = this.marketWatchRowsBySymbol || {};
        (Array.isArray(this.instruments)?this.instruments:[]).forEach(i => {
            const s = this.normalizeSymbol(i.symbol);
            const tick = this.latestTicks && this.latestTicks.get(s);
            const row = rows[s];
            const hasUiPrice = row && row.querySelector && row.querySelector('.mw-price') && row.querySelector('.mw-price').textContent.trim() !== '-';
            if (!tick && !hasUiPrice) missing.push(s);
        });
        for (const sym of missing) {
            try {
                const r = await fetch(`api/candles.php?symbol=${encodeURIComponent(sym)}&timeframe=1&limit=1`);
                const js = await r.json();
                const arr = Array.isArray(js?.data) ? js.data : (Array.isArray(js) ? js : []);
                const last = arr.length? arr[arr.length-1] : null;
                if (last && typeof last.c !== 'undefined') {
                    const px = Number(last.c);
                    if (Number.isFinite(px)) {
                        this.updateMarketWatchPrice(sym, px, px, 0, 0, 'rest');
                    }
                }
            } catch (e) {}
        }
    }

    getClientIndexForSymbol(symbol) {
        const inst = this.getInstrument(symbol);
        const cat = String(inst?.category||'').toLowerCase();
        if (!Array.isArray(this.wsClients) || !this.wsClients.length) return 0;
        if (cat === 'stocks') {
            const idx = this.wsClients.findIndex(c => (c.url||'').includes('xs68rzvrjn.finage.ws:7003'));
            return idx >= 0 ? idx : 0;
        }
        // Forex por defecto al primer cliente
        return 0;
    }

    /**
     * Actualizar precio específico en market watch
     */
    updateMarketWatchPrice(symbol, bid, ask, change = 0, changePercent = 0, source = 'unknown') {
        const targetItem = this.marketWatchRowsBySymbol?.[symbol];
        if (targetItem) {
            const inst = this.getInstrument(symbol);
            const isStocks = String(inst?.category||'').toLowerCase() === 'stocks';
            if (isStocks) {
                const open = this.isMarketOpen('stocks');
                if (!open) {
                    targetItem.dataset.frozen = '1';
                } else {
                    if (targetItem.dataset.frozen === '1') {
                        targetItem.dataset.frozen = '0';
                    }
                }
            }
            if (!this.lastChangePercentBySymbol) this.lastChangePercentBySymbol = {};
            if (!this.lastUiPriceBySymbol) this.lastUiPriceBySymbol = {};
            const priceCell = targetItem.querySelector('.mw-item-price');
            const changePercentCell = targetItem.querySelector('.mw-item-change-percent');
            const sellPriceEl = targetItem.querySelector('.order-btn.sell .order-price');
            const buyPriceEl = targetItem.querySelector('.order-btn.buy .order-price');
            
            if (priceCell) {
                const prevPrice = parseFloat(priceCell.textContent);
                const bVal = parseFloat(bid);
                const aVal = parseFloat(ask);
                let mid = (Number.isFinite(bVal) && Number.isFinite(aVal)) ? ((bVal + aVal) / 2) : (Number.isFinite(bVal) ? bVal : (Number.isFinite(aVal) ? aVal : NaN));

                // Si no hay precio válido, mostrar "-" y limpiar estilos
                if (!Number.isFinite(mid)) {
                    // Mantener el último precio mostrado para evitar parpadeos
                    const lastUi = Number.isFinite(prevPrice) ? prevPrice : this.lastUiPriceBySymbol[symbol];
                    if (Number.isFinite(lastUi)) {
                        priceCell.textContent = this.formatPrice(symbol, lastUi);
                        if (sellPriceEl) sellPriceEl.textContent = Number.isFinite(bVal) ? this.formatPrice(symbol, bVal) : '-';
                        if (buyPriceEl) buyPriceEl.textContent = Number.isFinite(aVal) ? this.formatPrice(symbol, aVal) : '-';
                    } else {
                        priceCell.textContent = '-';
                        if (sellPriceEl) sellPriceEl.textContent = '-';
                        if (buyPriceEl) buyPriceEl.textContent = '-';
                    }
                    // No borrar el % si tenía uno previo válido
                    if (changePercentCell) {
                        const prevPct = this.lastChangePercentBySymbol[symbol];
                        if (Number.isFinite(prevPct)) {
                            const icon = `<span class="trend-icon" aria-hidden="true">${prevPct >= 0 ? '▲' : '▼'}</span>`;
                            const cpTxt = (prevPct >= 0 ? '+' : '') + prevPct.toFixed(2) + '%';
                            changePercentCell.innerHTML = icon + cpTxt;
                            changePercentCell.className = `mw-item-change-percent num ${prevPct >= 0 ? 'positive' : 'negative'}`;
                            changePercentCell.setAttribute('aria-label', `Diario ${prevPct >= 0 ? 'positivo' : 'negativo'} ${prevPct.toFixed(2)}%`);
                        } else {
                            changePercentCell.textContent = '-';
                            changePercentCell.className = 'mw-item-change-percent num';
                            changePercentCell.removeAttribute('aria-label');
                        }
                    }
                    // No modificar clases de fuente
                    if (symbol === this.currentSymbol) {
                        const headerPriceEl = document.getElementById('current-price');
                        const headerChangeEl = document.getElementById('current-change');
                        if (headerPriceEl && Number.isFinite(lastUi)) headerPriceEl.textContent = this.formatPrice(symbol, lastUi);
                        if (headerChangeEl) {
                            const prevPct = this.lastChangePercentBySymbol[symbol];
                            if (Number.isFinite(prevPct)) {
                                const percentTxt = (prevPct >= 0 ? '+' : '') + prevPct.toFixed(2) + '%';
                                headerChangeEl.textContent = percentTxt;
                                headerChangeEl.classList.remove('positive','negative');
                                headerChangeEl.classList.add(prevPct >= 0 ? 'positive' : 'negative');
                            }
                        }
                    }
                    return;
                }

                const last = Number.isFinite(prevPrice) ? prevPrice : mid;
                const target = mid;
                const cfg = (typeof window !== 'undefined' ? (window.webtraderConfig || {}) : {});
                let steps = Number(cfg.mw_smooth_steps);
                if (!Number.isFinite(steps) || steps < 2) steps = 12;
                if (isStocks && targetItem.dataset.frozen === '1') { steps = 1; }
                let i = 0;
                // Actualización inmediata sin interpolación
                priceCell.textContent = this.formatPrice(symbol, target);
                this.lastUiPriceBySymbol[symbol] = target;
                // Actualizar precios BID/ASK en botones de VENTA/COMPRA
                if (sellPriceEl) sellPriceEl.textContent = Number.isFinite(bVal) ? this.formatPrice(symbol, bVal) : '-';
                if (buyPriceEl) buyPriceEl.textContent = Number.isFinite(aVal) ? this.formatPrice(symbol, aVal) : '-';
                
                if (changePercentCell && changePercent !== undefined) {
                    const rawPct = parseFloat(String(changePercent).replace('%',''));
                    const percentValue = Number.isFinite(rawPct) ? rawPct : this.lastChangePercentBySymbol[symbol];
                    if (Number.isFinite(percentValue)) {
                        const icon = `<span class="trend-icon" aria-hidden="true">${percentValue >= 0 ? '▲' : '▼'}</span>`;
                        const cpTxt = (percentValue >= 0 ? '+' : '') + percentValue.toFixed(2) + '%';
                        changePercentCell.innerHTML = icon + cpTxt;
                        changePercentCell.className = `mw-item-change-percent num ${percentValue >= 0 ? 'positive' : 'negative'}`;
                        changePercentCell.setAttribute('aria-label', `Diario ${percentValue >= 0 ? 'positivo' : 'negativo'} ${percentValue.toFixed(2)}%`);
                        this.lastChangePercentBySymbol[symbol] = percentValue;
                    } else {
                        changePercentCell.textContent = '-';
                        changePercentCell.className = 'mw-item-change-percent num';
                        changePercentCell.removeAttribute('aria-label');
                    }
                    // No aplicar cambios de color sobre el precio para evitar distracciones
                }
                
                // Fuente de datos
                if (source === 'finage') {
                    targetItem.classList.add('real-data');
                    targetItem.classList.remove('simulated-data');
                } else {
                    // No marcar como simulado: simplemente sin clase de fuente
                    targetItem.classList.remove('real-data');
                    targetItem.classList.remove('simulated-data');
                }
                // Sin efecto flash; se muestra sólo la interpolación numérica
                // Timestamp del último tick
                this.lastTickBySymbol[symbol] = Date.now();
                const updEl = targetItem.querySelector('.mw-updated');
                if (updEl) {
                    updEl.textContent = this.formatRelativeTime(this.lastTickBySymbol[symbol]);
                }

                // Actualizar header del gráfico si es el símbolo activo
                if (symbol === this.currentSymbol) {
                    const headerPriceEl = document.getElementById('current-price');
                    const headerChangeEl = document.getElementById('current-change');
                    if (headerPriceEl) headerPriceEl.textContent = this.formatPrice(symbol, mid);
                    if (headerChangeEl) {
                        const absChange = Number.isFinite(parseFloat(change)) ? parseFloat(change) : 0;
                        const sign = absChange >= 0 ? '+' : '';
                        const percentValue = parseFloat(String(changePercent).replace('%',''));
                        const percentTxt = (Number.isFinite(percentValue) ? (percentValue >= 0 ? '+' : '') + percentValue.toFixed(2) + '%' : '-')
                        headerChangeEl.textContent = `${sign}${this.formatPrice(symbol, Math.abs(absChange))} (${percentTxt})`;
                        headerChangeEl.classList.remove('positive','negative');
                        if (Number.isFinite(percentValue)) {
                            headerChangeEl.classList.add(percentValue >= 0 ? 'positive' : 'negative');
                        }
                    }
                }
                // Actualizar estimaciones de la tarjeta de orden (margen requerido) si el item está expandido
                try { this.updateOrderCardEstimates(symbol, bid, ask); } catch (e) {}
                try { this.updateAdvancedPanelOnTick(symbol, bid, ask); } catch (e) {}
            }
        }
    }

    // Calcular margen requerido y valores para la tarjeta de orden del Market Watch
    updateOrderCardEstimates(symbol, bid, ask) {
        const item = document.querySelector(`.mw-item[data-symbol="${symbol}"]`);
        if (!item) return;
        const sizeValEl = item.querySelector('.order-size .size-value');
        const sizeSubEl = item.querySelector('.order-size .size-sub');
        if (!sizeValEl || !sizeSubEl) return;
        const volume = parseFloat(sizeValEl.textContent) || 0.01;
        const inst = this.getInstrument(symbol);
        const contractSize = Number(inst?.contract_size) || 100000;
        const leverage = this.accountData?.account?.leverage ? Number(this.accountData.account.leverage) : 100;
        const marginRate = (inst && inst.margin_rate != null) ? Number(inst.margin_rate) : null;
        const mid = (Number(bid) + Number(ask)) / 2;
        const price = Number.isFinite(mid) ? mid : Number(inst?.bid) || 0;
        const notionalQuote = price * contractSize * volume;
        let requiredQuote = 0;
        if (marginRate && marginRate > 0) {
            requiredQuote = notionalQuote * marginRate;
        } else {
            requiredQuote = leverage > 0 ? (notionalQuote / leverage) : 0;
        }
        const quote = inst?.quote_currency || symbol.slice(-3);
        const accCur = this.accountData?.account?.currency || 'USD';
        const conv = this.getConversionRate(quote, accCur);
        const requiredAcc = requiredQuote * conv;
        sizeSubEl.textContent = `≈ ${this.formatCurrency(requiredAcc)}`;
    }

    getInstrument(symbol) {
        const list = Array.isArray(this.instruments) ? this.instruments : (this.instruments?.instruments || []);
        return list.find(i => this.normalizeSymbol(i.symbol) === this.normalizeSymbol(symbol));
    }

    getMidPrice(symbol) {
        const inst = this.getInstrument(symbol);
        if (inst) {
            const b = parseFloat(inst.bid);
            const a = parseFloat(inst.ask);
            if (Number.isFinite(b) && Number.isFinite(a)) return (b + a) / 2;
            const p = parseFloat(inst.price ?? inst.last ?? inst.close);
            if (Number.isFinite(p)) return p;
        }
        const row = document.querySelector(`[data-symbol="${symbol}"] .price`);
        const p = parseFloat(row?.textContent || '');
        return Number.isFinite(p) ? p : NaN;
    }

    getConversionRate(quoteCurrency, accountCurrency) {
        const q = (quoteCurrency || '').toUpperCase();
        const acc = (accountCurrency || '').toUpperCase();
        if (!q || !acc || q === acc) return 1;
        const pair1 = `${acc}${q}`; // e.g. USDCAD
        const r1 = this.getMidPrice(pair1);
        if (Number.isFinite(r1) && r1 > 0) return 1 / r1;
        const pair2 = `${q}${acc}`; // e.g. CADUSD
        const r2 = this.getMidPrice(pair2);
        if (Number.isFinite(r2) && r2 > 0) return r2;
        return 1;
    }

    getPipValuePerLotUSD(symbol) {
        const inst = this.getInstrument(symbol);
        const contractSize = Number(inst?.contract_size) || (/^\w{3}\w{3}$/.test(symbol) ? 100000 : 1);
        const pipSize = Number(inst?.pip_size) || (/JPY$/.test(symbol) ? 0.01 : (/^\w{3}\w{3}$/.test(symbol) ? 0.0001 : 0.01));
        const mid = this.getMidPrice(symbol);
        const quote = inst?.quote_currency || (symbol.length===6 ? symbol.slice(-3) : 'USD');
        const conv = this.getConversionRate(quote, 'USD');
        const pipValueQuote = contractSize * pipSize / Math.max(mid || 1, 1e-9);
        return pipValueQuote * conv;
    }

    formatCurrency(v) {
        try { return `$${Number(v).toFixed(2)}`; } catch (_) { return '$0.00'; }
    }

    computeTargetFromMode(mode, basePrice, dir, valEl, pctEl, ptsEl, pipSize, pipValuePerLotUSD, vol, contractSize) {
        let target = basePrice;
        if (mode === 'precio') {
            target = parseFloat(valEl.value)||basePrice;
        } else if (mode === 'valor') {
            const amount = parseFloat(valEl.value)||0;
            const pips = amount / (pipValuePerLotUSD * vol);
            const delta = pips * pipSize;
            target = basePrice + (dir * delta);
            if (ptsEl) ptsEl.value = Math.round(pips);
            if (pctEl) pctEl.value = ((amount / (basePrice * contractSize * vol)) * 100).toFixed(2);
        } else if (mode === 'porcentaje') {
            const pct = parseFloat(pctEl.value)||0;
            const delta = (basePrice * pct / 100);
            target = basePrice + (dir * delta);
            const pips = delta / pipSize;
            if (ptsEl) ptsEl.value = Math.round(pips);
            if (valEl) valEl.value = (pips * pipValuePerLotUSD * vol).toFixed(2);
        } else if (mode === 'puntos') {
            const pts = parseFloat(ptsEl.value)||0;
            const delta = pts * pipSize;
            target = basePrice + (dir * delta);
            const amount = (pts * pipValuePerLotUSD * vol).toFixed(2);
            if (valEl) valEl.value = amount;
            if (pctEl) pctEl.value = ((delta / basePrice) * 100).toFixed(2);
        }
        return target;
    }

    writePanelMetrics(metricsEl, amountUSD, percent, points) {
        if (!metricsEl) return;
        const amt = metricsEl.querySelector('.sl-amount, .tp-amount');
        const pct = metricsEl.querySelector('.sl-percent-val, .tp-percent-val');
        const pip = metricsEl.querySelector('.sl-pips, .tp-pips');
        if (amt) amt.textContent = `${(amountUSD||0).toFixed(2)} USD`;
        if (pct) pct.textContent = `${(percent||0).toFixed(2)}%`;
        if (pip) pip.textContent = `${Math.round(points||0)} Puntos`;
    }

    initializeAdvancedPanel(panel, symbol) {
        const volInput = panel.querySelector('.vol-input');
        const decVol = panel.querySelector('.vol-dec');
        const incVol = panel.querySelector('.vol-inc');
        const slToggle = panel.querySelector('.sl-toggle');
        const tpToggle = panel.querySelector('.tp-toggle');
        const slControls = panel.querySelector('.sl-controls');
        const tpControls = panel.querySelector('.tp-controls');
        const slPriceEl = panel.querySelector('.sl-price');
        const tpPriceEl = panel.querySelector('.tp-price');
        const slModeEl = panel.querySelector('.sl-mode');
        const tpModeEl = panel.querySelector('.tp-mode');
        const typeModeEl = panel.querySelector('.type-mode');
        const entryControls = panel.querySelector('.entry-controls');
        const entryPriceEl = panel.querySelector('.entry-price');
        const slValEl = panel.querySelector('.sl-value');
        const slPctEl = panel.querySelector('.sl-percent');
        const slPtsEl = panel.querySelector('.sl-points');
        const tpValEl = panel.querySelector('.tp-value');
        const tpPctEl = panel.querySelector('.tp-percent');
        const tpPtsEl = panel.querySelector('.tp-points');
        const slMetrics = panel.querySelector('.sl-metrics');
        const tpMetrics = panel.querySelector('.tp-metrics');
        const parentCardInit = panel.__origParent || panel.closest('.order-card');
        const sellPriceEl = parentCardInit?.querySelector('.order-btn.sell .order-price');
        const buyPriceEl = parentCardInit?.querySelector('.order-btn.buy .order-price');
        const bid = parseFloat(sellPriceEl?.textContent || '') || parseFloat(this.getInstrument(symbol)?.bid) || NaN;
        const ask = parseFloat(buyPriceEl?.textContent || '') || parseFloat(this.getInstrument(symbol)?.ask) || NaN;
        let mid = (Number(bid)+Number(ask))/2;
        if (!Number.isFinite(mid)) mid = this.getMidPrice(symbol);
        const inst = this.getInstrument(symbol);
        // pipValuePerLotUSD disponible desde la inicialización previa en este método
        const pipSize = inst?.pip_size ? Number(inst.pip_size) : (/JPY$/.test(symbol) ? 0.01 : 0.0001);
        if (slPriceEl && Number.isFinite(mid)) slPriceEl.value = (mid - pipSize).toFixed(5);
        if (tpPriceEl && Number.isFinite(mid)) tpPriceEl.value = (mid + pipSize).toFixed(5);
        const slModeWrap = slModeEl ? slModeEl.closest('.calc-select-wrap') : null;
        const tpModeWrap = tpModeEl ? tpModeEl.closest('.calc-select-wrap') : null;
        if (slControls) slControls.style.display = 'none';
        if (tpControls) tpControls.style.display = 'none';
        if (slMetrics) slMetrics.style.display = 'none';
        if (tpMetrics) tpMetrics.style.display = 'none';
        if (slModeWrap) slModeWrap.style.display = 'none';
        if (tpModeWrap) tpModeWrap.style.display = 'none';
        const updateSummary = () => {
            const funds = this.accountData?.account?.free_margin ?? this.accountData?.free_margin ?? 0;
            const spread = Number.isFinite(bid)&&Number.isFinite(ask) ? (ask - bid) : 0;
            const sumMargin = panel.querySelector('.sum-margin');
            const sumFunds = panel.querySelector('.sum-funds');
            const sumSpread = panel.querySelector('.sum-spread');
            const vol = parseFloat(volInput?.value || '0.01');
            const notionalQuote = (Number(mid)||0) * (Number(inst?.contract_size)||100000) * vol;
            const leverage = this.accountData?.account?.leverage ? Number(this.accountData.account.leverage) : 100;
            const marginRate = (inst && inst.margin_rate != null) ? Number(inst.margin_rate) : null;
            const requiredQuote = marginRate && marginRate>0 ? (notionalQuote * marginRate) : (leverage>0 ? (notionalQuote/leverage) : 0);
            const quote = inst?.quote_currency || symbol.slice(-3);
            const accCur = this.accountData?.account?.currency || 'USD';
            const conv = this.getConversionRate(quote, accCur);
            const requiredAcc = requiredQuote * conv;
            if (sumMargin) sumMargin.textContent = `${this.formatCurrency(requiredAcc)} USD`.replace('$$','$');
            if (sumFunds) sumFunds.textContent = `${Number(funds).toFixed(2)} USD`;
            if (sumSpread) sumSpread.textContent = `${spread.toFixed(5)} USD`;
        };
        const volApproxEl = panel.querySelector('.vol-approx');
        if (volApproxEl && !volApproxEl.__initLabel) { volApproxEl.textContent = '— USD'; volApproxEl.__initLabel = true; }
        const percentBar = panel.querySelector('.percent-bar');
        const percentFill = panel.querySelector('.percent-fill');
        const percentLabel = panel.querySelector('.percent-label');
        const updateVolApprox = () => {
            const v = parseFloat(volInput?.value || '0.01');
            const notionalQuote = (Number(mid)||0) * (Number(inst?.contract_size)||100000) * v;
            const leverage = this.accountData?.account?.leverage ? Number(this.accountData.account.leverage) : 100;
            const marginRate = (inst && inst.margin_rate != null) ? Number(inst.margin_rate) : null;
            const requiredQuote = marginRate && marginRate>0 ? (notionalQuote * marginRate) : (leverage>0 ? (notionalQuote/leverage) : 0);
            const quote = inst?.quote_currency || symbol.slice(-3);
            const accCur = this.accountData?.account?.currency || 'USD';
            const conv = this.getConversionRate(quote, accCur);
            const requiredAcc = requiredQuote * conv;
            if (volApproxEl) volApproxEl.textContent = `${this.formatCurrency(requiredAcc)} USD`.replace('$$','$');
            const balance = this.accountData?.account?.balance ?? this.accountData?.balance ?? 0;
            const equityBase = this.accountData?.account?.equity ?? this.accountData?.equity ?? balance;
            const percent = (equityBase > 0) ? Math.min(100, Math.max(0, (requiredAcc / equityBase) * 100)) : 0;
            if (percentFill) percentFill.style.width = `${percent.toFixed(2)}%`;
            if (percentLabel) percentLabel.textContent = `${percent.toFixed(0)}%`;
            if (percentFill) {
                if (percent < 50) {
                    percentFill.style.background = 'linear-gradient(135deg,#2ea6ff,#00c2ff)';
                } else if (percent < 80) {
                    percentFill.style.background = 'linear-gradient(135deg,#fbbf24,#f59e0b)';
                } else {
                    percentFill.style.background = 'linear-gradient(135deg,#ef4444,#dc2626)';
                }
            }
            if (!Number.isFinite(requiredAcc)) {
                const msgEl = document.getElementById('orderErrorMessage');
                const detEl = document.getElementById('orderErrorDetails');
                if (msgEl) msgEl.textContent = 'Error en cálculo de margen (panel)';
                if (detEl) detEl.textContent = JSON.stringify({symbol, v, mid, contract_size: Number(inst?.contract_size)||100000, leverage, marginRate, quote, accCur, conv}, null, 2);
                try { this.openModal('orderErrorModal'); } catch(e){}
            }
        };
        updateSummary();
        // Tipo de orden y precio de entrada
        if (typeModeEl && entryControls) {
            const applyEntryVisible = () => { entryControls.style.display = (typeModeEl.value === 'market') ? 'none' : 'flex'; };
            applyEntryVisible();
            typeModeEl.addEventListener('change', applyEntryVisible);
            if (entryPriceEl && !entryPriceEl.value) entryPriceEl.value = (ask || mid || 0).toFixed(5);
            const adjEntry = (dir) => { let v = parseFloat(entryPriceEl.value)||mid; const pip = pipSize; v = v + (dir * pip); entryPriceEl.value = v.toFixed(5); };
            const entryDec = panel.querySelector('.entry-dec');
            const entryInc = panel.querySelector('.entry-inc');
            entryDec && entryDec.addEventListener('click',(e)=>{ e.stopPropagation(); adjEntry(-1); });
            entryInc && entryInc.addEventListener('click',(e)=>{ e.stopPropagation(); adjEntry(+1); });
        }
        updateVolApprox();
        slModeEl && slModeEl.addEventListener('change', ()=>{ if (slToggle?.checked) computeSl(); });
        tpModeEl && tpModeEl.addEventListener('change', ()=>{ if (tpToggle?.checked) computeTp(); });
        const minVol = Number(inst?.min_volume) || 0.01;
        const maxVol = Number(inst?.max_volume) || 100.0;
        const stepVol = 0.01;
        const changeVol = (d) => {
            const v = parseFloat(volInput?.value || '0.01');
            let nv = Number((v + d).toFixed(2));
            nv = Math.max(minVol, Math.min(maxVol, nv));
            nv = Math.round(nv / stepVol) * stepVol;
            if (volInput) volInput.value = nv.toFixed(2);
            updateSummary();
            updateVolApprox();
            if (slToggle?.checked) computeSl();
            if (tpToggle?.checked) computeTp();
        };
        let _holdTimer = null;
        const startHold = (dir)=>{
            changeVol(dir);
            if (_holdTimer) clearInterval(_holdTimer);
            _holdTimer = setInterval(()=>changeVol(dir), 120);
        };
        const stopHold = ()=>{ if (_holdTimer) { clearInterval(_holdTimer); _holdTimer=null; } };
        incVol && incVol.addEventListener('mousedown', (e)=>{ e.preventDefault(); e.stopPropagation(); startHold(+stepVol); });
        decVol && decVol.addEventListener('mousedown', (e)=>{ e.preventDefault(); e.stopPropagation(); startHold(-stepVol); });
        incVol && incVol.addEventListener('touchstart', (e)=>{ e.preventDefault(); startHold(+stepVol); });
        decVol && decVol.addEventListener('touchstart', (e)=>{ e.preventDefault(); startHold(-stepVol); });
        incVol && incVol.addEventListener('mouseleave', stopHold);
        decVol && decVol.addEventListener('mouseleave', stopHold);
        document.addEventListener('mouseup', stopHold);
        document.addEventListener('touchend', stopHold);
        document.addEventListener('touchcancel', stopHold);
        volInput && volInput.addEventListener('input', ()=>{ updateSummary(); updateVolApprox(); if (slToggle?.checked) computeSl(); if (tpToggle?.checked) computeTp(); });
        slToggle && slToggle.addEventListener('change', ()=>{ if (slControls) slControls.style.display = slToggle.checked ? 'flex':'none'; if (slMetrics) slMetrics.style.display = slToggle.checked ? 'flex':'none'; if (slModeWrap) slModeWrap.style.display = slToggle.checked ? 'inline-flex':'none'; slControls && slControls.classList.toggle('active', slToggle.checked); if (slToggle.checked) computeSl(); });
        tpToggle && tpToggle.addEventListener('change', ()=>{ if (tpControls) tpControls.style.display = tpToggle.checked ? 'flex':'none'; if (tpMetrics) tpMetrics.style.display = tpToggle.checked ? 'flex':'none'; if (tpModeWrap) tpModeWrap.style.display = tpToggle.checked ? 'inline-flex':'none'; tpControls && tpControls.classList.toggle('active', tpToggle.checked); if (tpToggle.checked) computeTp(); });
        const adj = (el, dir) => { let v = parseFloat(el.value)||mid; v = v + (dir*pipSize); el.value = v.toFixed(5); };
        const slDec = panel.querySelector('.sl-dec'); const slInc = panel.querySelector('.sl-inc');
        const tpDec = panel.querySelector('.tp-dec'); const tpInc = panel.querySelector('.tp-inc');
        slDec && slDec.addEventListener('click',(e)=>{ e.stopPropagation(); adj(slPriceEl,-1); computeSl(); });
        slInc && slInc.addEventListener('click',(e)=>{ e.stopPropagation(); adj(slPriceEl,+1); computeSl(); });
        tpDec && tpDec.addEventListener('click',(e)=>{ e.stopPropagation(); adj(tpPriceEl,-1); computeTp(); });
        tpInc && tpInc.addEventListener('click',(e)=>{ e.stopPropagation(); adj(tpPriceEl,+1); computeTp(); });

        const pipValuePerLotUSD = this.getPipValuePerLotUSD(symbol);
        const volFactor = ()=> parseFloat(volInput?.value || '0.01');

        const modeTarget = (mode, basePrice, dir, vEl, pEl, ptEl) => this.computeTargetFromMode(mode, basePrice, dir, vEl, pEl, ptEl, pipSize, pipValuePerLotUSD, volFactor(), (Number(inst?.contract_size)||100000));

        const writeMetrics = (metricsEl, amountUSD, percent, points)=>{
            const amt = metricsEl.querySelector('.sl-amount, .tp-amount');
            const pct = metricsEl.querySelector('.sl-percent-val, .tp-percent-val');
            const pip = metricsEl.querySelector('.sl-pips, .tp-pips');
            if (amt) amt.textContent = `${(amountUSD||0).toFixed(2)} USD`;
            if (pct) pct.textContent = `${(percent||0).toFixed(2)}%`;
            if (pip) pip.textContent = `${Math.round(points||0)} Puntos`;
        };

        const computeSl = ()=>{
            const mode = slModeEl?.value || 'precio';
            const base = mid;
            slPriceEl.style.display = (mode==='precio')?'inline-block':'none';
            slValEl.style.display = (mode==='valor')?'inline-block':'none';
            slPctEl.style.display = (mode==='porcentaje')?'inline-block':'none';
            slPtsEl.style.display = (mode==='puntos')?'inline-block':'none';
            [slPriceEl, slValEl, slPctEl, slPtsEl].forEach(el=>{ if(!el) return; el.classList.remove('display-main'); });
            const activeEl = (mode==='precio')?slPriceEl:(mode==='valor')?slValEl:(mode==='porcentaje')?slPctEl:slPtsEl;
            if (activeEl) activeEl.classList.add('display-main');
            const target = modeTarget(mode, base, -1, slValEl, slPctEl, slPtsEl);
            if (slPriceEl) slPriceEl.value = (mode==='precio') ? slPriceEl.value : Number(target).toFixed(5);
            const amount = (Math.abs(Number(target)-base)/pipSize) * pipValuePerLotUSD * volFactor();
            const percent = ((Math.abs(Number(target)-base)/base) * 100);
            const points = Math.abs(Number(target)-base) / pipSize;
            this.writePanelMetrics(slMetrics, amount, percent, points);
        };
        const computeTp = ()=>{
            const mode = tpModeEl?.value || 'precio';
            const base = mid;
            tpPriceEl.style.display = (mode==='precio')?'inline-block':'none';
            tpValEl.style.display = (mode==='valor')?'inline-block':'none';
            tpPctEl.style.display = (mode==='porcentaje')?'inline-block':'none';
            tpPtsEl.style.display = (mode==='puntos')?'inline-block':'none';
            [tpPriceEl, tpValEl, tpPctEl, tpPtsEl].forEach(el=>{ if(!el) return; el.classList.remove('display-main'); });
            const activeEl2 = (mode==='precio')?tpPriceEl:(mode==='valor')?tpValEl:(mode==='porcentaje')?tpPctEl:tpPtsEl;
            if (activeEl2) activeEl2.classList.add('display-main');
            const target = modeTarget(mode, base, +1, tpValEl, tpPctEl, tpPtsEl);
            if (tpPriceEl) tpPriceEl.value = (mode==='precio') ? tpPriceEl.value : Number(target).toFixed(5);
            const amount = (Math.abs(Number(target)-base)/pipSize) * pipValuePerLotUSD * volFactor();
            const percent = ((Math.abs(Number(target)-base)/base) * 100);
            const points = Math.abs(Number(target)-base) / pipSize;
            this.writePanelMetrics(tpMetrics, amount, percent, points);
        };

        const closeBtn = panel.querySelector('.adv-close');
        if (closeBtn) {
            closeBtn.addEventListener('click', (e)=>{
                e.stopPropagation();
                const overlay = document.getElementById('mwAdvOverlay');
                const mwContainer = document.querySelector('.market-watch-container');
                const origParent = panel.__origParent || panel.closest('.order-card');
                const orderMain = origParent?.querySelector('.order-main');
                const orderFooter = origParent?.querySelector('.order-footer');
                const quickActionsEl = origParent?.closest('.mw-item-actions')?.querySelector('.mw-quick-actions');
                const orderCard = origParent;
                panel.classList.remove('open');
                if (origParent) { origParent.appendChild(panel); panel.__origParent = null; }
                if (orderMain) { orderMain.classList.remove('hidden'); orderMain.setAttribute('aria-hidden','false'); orderMain.style.display = ''; }
                if (orderFooter) { orderFooter.classList.remove('hidden'); orderFooter.setAttribute('aria-hidden','false'); orderFooter.style.display = ''; }
                if (quickActionsEl) { quickActionsEl.classList.remove('hidden'); quickActionsEl.setAttribute('aria-hidden','false'); quickActionsEl.style.display = ''; }
                if (orderCard) { orderCard.classList.remove('adv-open'); }
                if (overlay && mwContainer) {
                    overlay.style.display = 'none';
                    overlay.innerHTML = '';
                    mwContainer.classList.remove('adv-mode');
                    const listHeader = document.querySelector('.mw-list-header');
                    if (listHeader) { listHeader.style.display = ''; }
                    const mwList = document.getElementById('mwList');
                    if (mwList) { Array.from(mwList.children).forEach(ch=>{ if (ch.id !== 'mwAdvOverlay') ch.style.display = ''; }); }
                }
            });
        }
        slModeEl && slModeEl.addEventListener('change', computeSl);
        tpModeEl && tpModeEl.addEventListener('change', computeTp);
        slValEl && slValEl.addEventListener('input', computeSl);
        slPctEl && slPctEl.addEventListener('input', computeSl);
        slPtsEl && slPtsEl.addEventListener('input', computeSl);
        slPriceEl && slPriceEl.addEventListener('input', computeSl);
        tpValEl && tpValEl.addEventListener('input', computeTp);
        tpPctEl && tpPctEl.addEventListener('input', computeTp);
        tpPtsEl && tpPtsEl.addEventListener('input', computeTp);
        tpPriceEl && tpPriceEl.addEventListener('input', computeTp);

        computeSl();
        computeTp();
    }

    updateAdvancedPanelOnTick(symbol, bid, ask) {
        const overlay = document.getElementById('mwAdvOverlay');
        const panel = overlay && overlay.querySelector('.order-advanced-panel.open') ? overlay.querySelector('.order-advanced-panel.open') : (document.querySelector(`.mw-item[data-symbol="${symbol}"]`)?.querySelector('.order-advanced-panel.open') || null);
        if (!panel) return;
        const parentCard = panel.__origParent || panel.closest('.order-card');
        const sellPriceEl = parentCard?.querySelector('.order-btn.sell .order-price');
        const buyPriceEl = parentCard?.querySelector('.order-btn.buy .order-price');
        const b = Number.isFinite(parseFloat(bid)) ? parseFloat(bid) : parseFloat(sellPriceEl?.textContent || '0');
        const a = Number.isFinite(parseFloat(ask)) ? parseFloat(ask) : parseFloat(buyPriceEl?.textContent || '0');
        const mid = (Number(b)+Number(a))/2;
        const inst = this.getInstrument(symbol);
        const pipSize = inst?.pip_size ? Number(inst.pip_size) : (/JPY$/.test(symbol) ? 0.01 : 0.0001);
        const contractSize = Number(inst?.contract_size)||100000;
        const pipValuePerLotUSD = this.getPipValuePerLotUSD(symbol);
        const volInput2 = panel.querySelector('.vol-input');
        const vol = parseFloat(volInput2?.value || '0.01');
        const recompute = (toggleSel, modeSel, priceEl, valEl, pctEl, ptsEl, metricsEl, dir)=>{
            const togg = panel.querySelector(toggleSel);
            if (!togg || !togg.checked) return;
            const mode = panel.querySelector(modeSel)?.value || 'precio';
            const target = this.computeTargetFromMode(mode, mid, dir, valEl, pctEl, ptsEl, pipSize, pipValuePerLotUSD, vol, contractSize);
            if (priceEl && mode!=='precio') priceEl.value = Number(target).toFixed(5);
            const amount = (Math.abs(Number(target)-mid)/pipSize) * pipValuePerLotUSD * vol;
            const percent = ((Math.abs(Number(target)-mid)/mid) * 100);
            const points = Math.abs(Number(target)-mid) / pipSize;
            this.writePanelMetrics(metricsEl, amount, percent, points);
            const volApproxEl = panel.querySelector('.vol-approx');
            const percentFill = panel.querySelector('.percent-fill');
            const percentLabel = panel.querySelector('.percent-label');
            if (volApproxEl || percentFill || percentLabel) {
                const notionalQuote = (Number(mid)||0) * contractSize * vol;
                const leverage2 = this.accountData?.account?.leverage ? Number(this.accountData.account.leverage) : 100;
                const marginRate2 = (inst && inst.margin_rate != null) ? Number(inst.margin_rate) : null;
                const requiredQuote = marginRate2 && marginRate2>0 ? (notionalQuote * marginRate2) : (leverage2>0 ? (notionalQuote/leverage2) : 0);
                const quote = inst?.quote_currency || symbol.slice(-3);
                const accCur = this.accountData?.account?.currency || 'USD';
                const conv = this.getConversionRate(quote, accCur);
                const requiredAcc = requiredQuote * conv;
                if (volApproxEl) volApproxEl.textContent = `${this.formatCurrency(requiredAcc)} USD`.replace('$$','$');
                const balance = this.accountData?.account?.balance ?? this.accountData?.balance ?? 0;
                const equityBase = this.accountData?.account?.equity ?? this.accountData?.equity ?? balance;
                const percent2 = (equityBase > 0) ? Math.min(100, Math.max(0, (requiredAcc / equityBase) * 100)) : 0;
                if (percentFill) {
                    percentFill.style.width = `${percent2.toFixed(2)}%`;
                    if (percent2 < 50) percentFill.style.background = 'linear-gradient(135deg,#2ea6ff,#00c2ff)';
                    else if (percent2 < 80) percentFill.style.background = 'linear-gradient(135deg,#fbbf24,#f59e0b)';
                    else percentFill.style.background = 'linear-gradient(135deg,#ef4444,#dc2626)';
                }
                if (percentLabel) percentLabel.textContent = `${percent2.toFixed(0)}%`;
            }
        };
        recompute('.sl-toggle', '.sl-mode', panel.querySelector('.sl-price'), panel.querySelector('.sl-value'), panel.querySelector('.sl-percent'), panel.querySelector('.sl-points'), panel.querySelector('.sl-metrics'), -1);
        recompute('.tp-toggle', '.tp-mode', panel.querySelector('.tp-price'), panel.querySelector('.tp-value'), panel.querySelector('.tp-percent'), panel.querySelector('.tp-points'), panel.querySelector('.tp-metrics'), +1);
        const sellExec = panel.querySelector('.adv-exec-sell .exec-price');
        const buyExec = panel.querySelector('.adv-exec-buy .exec-price');
        if (sellExec && Number.isFinite(b)) sellExec.textContent = this.formatPrice(symbol, b);
        if (buyExec && Number.isFinite(a)) buyExec.textContent = this.formatPrice(symbol, a);
    }

    /**
     * Actualizar precio en gráfico
     */
    updateChartPrice(symbol, bid, ask) {
        if (!this.charts.main || symbol !== this.currentSymbol) return;
        const midPrice = (parseFloat(bid) + parseFloat(ask)) / 2;
        if (!Number.isFinite(midPrice)) return;
        this.currentMidPrice = midPrice;
        this.onTick(midPrice);
        this.refreshChartFromBaseBuffer();
    }

    /**
     * Actualizar P&L de posiciones con nuevos precios
     */
    updatePositionsPnL(symbol, bid, ask) {
        // Recalcular P&L por símbolo y refrescar KPIs
        let totalUnrealized = 0;
        let symbolUnrealized = 0;
        const priceRow = document.querySelector(`#marketWatchTable [data-symbol="${symbol}"]`)?.closest('tr');
        const singlePriceText = priceRow?.querySelector('.price')?.textContent || '';
        const singlePrice = parseFloat(singlePriceText);

        this.positions.forEach(position => {
            if (position.symbol !== symbol) return;
            const sideUpper = ((position.side || position.type) || '').toString().toUpperCase();

            let currentPrice;
            if (Number.isFinite(bid) && Number.isFinite(ask)) {
                currentPrice = sideUpper === 'BUY' ? bid : ask;
            } else if (Number.isFinite(singlePrice)) {
                currentPrice = singlePrice;
            } else {
                const last = this.lastPriceBySymbol[symbol];
                if (Number.isFinite(last)) currentPrice = last;
            }
            if (!Number.isFinite(currentPrice)) {
                const valuSide = sideUpper === 'BUY' ? 'SELL' : 'BUY';
                currentPrice = this.getSidePrice(symbol, valuSide);
            }

            let pnl = this.calculatePnL({
                side: (position.side || position.type || 'buy').toString().toLowerCase(),
                open_price: parseFloat(position.open_price ?? position.openPrice),
                volume: parseFloat(position.volume)
            }, currentPrice);
            if (!Number.isFinite(pnl)) {
                const last = this.lastPnlByPositionId[position.id];
                if (Number.isFinite(last)) pnl = last;
            }

            // Persistir P&L para posición
            position.profit = pnl;
            totalUnrealized += pnl;
            if (position.symbol === symbol) symbolUnrealized += pnl;

            // Actualizar display de la posición
            const selectors = [
                `#positions-tbody tr[data-position-id="${position.id}"]`,
                `#positionsTable tbody tr[data-position-id="${position.id}"]`
            ];
            const positionRow = document.querySelector(selectors[0]) || document.querySelector(selectors[1]);
            if (positionRow) {
                const pnlCell = positionRow.querySelector('.pnl');
                if (pnlCell && Number.isFinite(pnl)) {
                    pnlCell.textContent = `$${pnl.toFixed(2)}`;
                    pnlCell.className = `pnl numeric ${pnl >= 0 ? 'profit' : 'loss'}`;
                    this.lastPnlByPositionId[position.id] = pnl;
                }
                const curCell = positionRow.querySelector('.cur-price');
                if (curCell && Number.isFinite(currentPrice)) {
                    curCell.textContent = this.formatPrice(symbol, currentPrice);
                }
            }
        });

        // Recalcular KPIs de cuenta basados en P&L no realizado
        if (Number.isFinite(totalUnrealized)) {
            const acc = this.accountData?.account || this.accountData || {};
            const balance = parseFloat(acc?.balance) || 10000;
            const margin = parseFloat(acc?.margin) || 0;
            const equity = balance + totalUnrealized;
            const freeMargin = equity - margin;

            if (this.accountData?.account) {
                this.accountData.account.equity = equity;
                this.accountData.account.free_margin = freeMargin;
            } else {
                acc.equity = equity;
                acc.free_margin = freeMargin;
                this.accountData = acc;
            }
            this.updateAccountInfo();
        }

        if (symbol === this.currentSymbol && Number.isFinite(symbolUnrealized)) {
            this.updateToolbarPnlForCurrentSymbol(symbolUnrealized);
        }
    }

    updateToolbarPnlForCurrentSymbol(forceValue) {
        const sym = this.currentSymbol;
        let sum = Number(forceValue);
        if (!Number.isFinite(sum)) {
            sum = 0;
            (Array.isArray(this.positions)?this.positions:[]).forEach(p => {
                if (this.normalizeSymbol(p.symbol) === sym) {
                    const v = parseFloat(p?.pnl ?? p?.profit);
                    if (Number.isFinite(v)) sum += v;
                }
            });
        }
        const pnlEl = document.getElementById('current-pnl');
        const cntEl = document.getElementById('current-open-count');
        if (pnlEl) {
            pnlEl.textContent = `$${sum.toFixed(2)}`;
            pnlEl.classList.remove('profit','loss');
            pnlEl.classList.add(sum >= 0 ? 'profit' : 'loss');
        }
        if (cntEl) {
            const count = (Array.isArray(this.positions)?this.positions:[]).filter(p => this.normalizeSymbol(p.symbol)===sym).length;
            cntEl.textContent = `(${count})`;
        }
    }

    /**
     * Calcular P&L de una posición
     */
    calculatePnL(position, currentPrice) {
        const symbol = this.normalizeSymbol(position.symbol || this.currentSymbol);
        const openPrice = parseFloat(position.open_price);
        const volume = parseFloat(position.volume);
        const side = (position.side || 'buy').toLowerCase();
        if (!Number.isFinite(openPrice) || !Number.isFinite(volume) || !Number.isFinite(currentPrice)) return 0;

        const inst = this.getInstrument(symbol) || {};
        const pipSize = Number(inst?.pip_size) || (/JPY$/.test(symbol) ? 0.01 : (/^[A-Z]{6}$/.test(symbol) ? 0.0001 : 0.01));
        const quote = inst?.quote_currency || (symbol.length === 6 ? symbol.slice(-3) : 'USD');
        const accountCur = this.accountData?.account?.currency || 'USD';
        const pipValuePerLotUSD = this.getPipValuePerLotUSD(symbol); // ya convertido a USD

        const priceDiff = side === 'buy' ? (currentPrice - openPrice) : (openPrice - currentPrice);
        const pips = priceDiff / pipSize;
        const pnlUSD = pips * pipValuePerLotUSD * volume;
        return pnlUSD;
    }

    /**
     * Cargar órdenes (método auxiliar para WebSocket)
     */
    async loadOrders() {
        try {
            const params = new URLSearchParams();
            if (this.activeAccountNumber) params.set('account_number', this.activeAccountNumber);
            const response = await fetch(`api/orders.php${params.toString() ? `?${params.toString()}` : ''}`);
            let data = {};
            try {
                const ct = (response.headers && response.headers.get('Content-Type')) || '';
                if (ct && ct.indexOf('application/json') !== -1) {
                    data = await response.json();
                } else {
                    data = {};
                }
            } catch(e) {
                data = {};
            }
            
            if (data.success) {
                this.orders = data.orders || [];
                this.updateOrdersTable();
            }
        } catch (error) {
            console.error('Error cargando órdenes:', error);
        }
    }

    /**
     * Cargar posiciones (método auxiliar para WebSocket)
     */
    async loadPositions() {
        try {
            const params = new URLSearchParams();
            if (this.activeAccountNumber) params.set('account_number', this.activeAccountNumber);
            const response = await fetch(`api/positions.php${params.toString() ? `?${params.toString()}` : ''}`);
            let data = {};
            try {
                const ct = (response.headers && response.headers.get('Content-Type')) || '';
                if (ct && ct.indexOf('application/json') !== -1) {
                    data = await response.json();
                } else {
                    data = {};
                }
            } catch(e) {
                data = {};
            }
            
            if (data.success) {
                const incoming = Array.isArray(data.positions) ? data.positions : [];
                if (incoming.length > 0 || this.positions.length === 0) {
                    this.positions = incoming;
                }
                const stats = data.statistics || null;
                this.positionsStats = stats;
                if (stats && typeof stats.total_pnl === 'number') {
                    const acc = this.accountData?.account || this.accountData || {};
                    const bal = parseFloat(acc.balance) || 0;
                    const mar = parseFloat(acc.margin) || 0;
                    const eq = bal + Number(stats.total_pnl);
                    const fm = eq - mar;
                    if (this.accountData?.account) {
                        this.accountData.account.equity = eq;
                        this.accountData.account.free_margin = fm;
                    } else {
                        acc.equity = eq;
                        acc.free_margin = fm;
                        this.accountData = acc;
                    }
                    this.updateAccountInfo();
                }
                this.updatePositionsTable(this.positions);
            }
        } catch (error) {
            console.error('Error cargando posiciones:', error);
        }
    }

    updatePrices() {
        try {
            if (this.wsClient && this.wsClient.connected && this.isWsHealthy()) return;
            if (this._tickPending) return; this._tickPending = true;
            try { this._tickController && this._tickController.abort(); } catch(_){}
            this._tickController = (typeof AbortController !== 'undefined') ? new AbortController() : null;
            const allSymbols = (Array.isArray(this.instruments) ? this.instruments : []).map(i => this.normalizeSymbol(i.symbol)).filter(Boolean);
            if (!allSymbols.length) return;
            const batchSize = (typeof window !== 'undefined' && window.webtraderConfig && window.webtraderConfig.tick_batch_size) ? Number(window.webtraderConfig.tick_batch_size) : 30;
            const ts = Date.now();
            const chunks = [];
            for (let i = 0; i < allSymbols.length; i += batchSize) chunks.push(allSymbols.slice(i, i + batchSize));
            const run = async (syms) => {
                const params = new URLSearchParams({ action: 'prices', symbols: syms.join(',') });
                const json = await fetch(`api/finage_proxy.php?${params.toString()}`, { signal: this._tickController?.signal }).then(r => r.json()).catch(() => null);
                const data = (json && json.data) || {};
                for (const sym of syms) {
                    const p = data[sym];
                    if (!p) continue;
                    const bid = parseFloat(p.bid);
                    const ask = parseFloat(p.ask);
                    const change = parseFloat(p.change || 0);
                    const changePercent = parseFloat(p.change_percent || 0);
                    if (!Number.isFinite(bid) || !Number.isFinite(ask)) continue;
                    this.updateMarketWatchPrice(sym, bid, ask, change, changePercent, 'api');
                    this.updatePositionsPnL(sym, bid, ask);
                    if (sym === this.currentSymbol) {
                        const mid = (bid + ask) / 2;
                        if (Number.isFinite(mid)) {
                            this.currentMidPrice = mid;
                            this.onTick(mid, ts, sym);
                            this.refreshChartFromBaseBuffer();
                            this.lastUpdateTimestamp = Date.now();
                            this.updateStatusBar();
                        }
                    }
                }
            };
            (async () => { 
                for (const c of chunks) { 
                    if (this._tickController?.signal?.aborted) break; 
                    await run(c); 
                } 
                this._tickPending = false; 
            })();
        } catch (_) {}
    }

    updateChart() {
        // Recalcular y refrescar desde el buffer base al timeframe actual
        this.refreshChartFromBaseBuffer();
    }

    // Funciones para modales
    openModal(modalId) {
        const modal = document.getElementById(modalId);
        if (modal) {
            try {
                const dd = document.getElementById('userDropdown');
                const btn = document.getElementById('userMenuBtn');
                if (dd) dd.classList.remove('show');
                if (btn) btn.classList.remove('active');
            } catch(_) {}
            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
            document.body.classList.add('modal-open');
            this.activeModalId = modalId;
            if (modalId === 'depositModal' || modalId === 'withdrawModal') {
                this.bindModalTiles(modalId);
            }
            
            // Agregar event listener para cerrar con ESC
            document.addEventListener('keydown', this.boundHandleModalKeydown);
            
            // Cerrar modal al hacer clic fuera del contenido
            modal.addEventListener('click', (e) => {
                if (e.target === modal) {
                    this.closeModal(modalId);
                }
            }, { once: true });
        }
    }

    closeModal(modalId) {
        const modal = document.getElementById(modalId);
        if (modal) {
            modal.classList.remove('active');
            document.body.style.overflow = 'auto';
            document.body.classList.remove('modal-open');
            if (this.activeModalId === modalId) this.activeModalId = null;
            document.removeEventListener('keydown', this.boundHandleModalKeydown);
        }
    }

    handleModalKeydown(e) {
        if (e.key === 'Escape') {
            // Cerrar el modal activo
            const activeModal = document.querySelector('.modal.active');
            if (activeModal) {
                this.closeModal(activeModal.id);
            }
        }
    }

    // Funciones para perfil
    showProfile() {
        this.openModal('profileModal');
    }

    // Funciones para historial
    showHistory() {
        this.openModal('historyModal');
        this.loadTradingHistory();
    }

    loadTradingHistory() {
        // Simular carga de historial
        console.log('Cargando historial de trading...');
        // Aquí se implementaría la carga real de datos
    }

    // Funciones para reportes
    showReports() {
        this.openModal('reportsModal');
    }

    downloadReport() {
        const reportType = document.getElementById('reportType').value;
        const reportPeriod = document.getElementById('reportPeriod').value;
        const reportFormat = document.getElementById('reportFormat').value;
        
        console.log(`Descargando reporte: ${reportType}, período: ${reportPeriod}, formato: ${reportFormat}`);
        
        // Simular descarga
        this.showNotification('Generando reporte...', 'info');
        
        setTimeout(() => {
            this.showNotification('Reporte descargado exitosamente', 'success');
            this.closeModal('reportsModal');
        }, 2000);
    }

    // Funciones para configuración
    showSettings() {
        this.openModal('settingsModal');
    }

    saveSettings() {
        console.log('Guardando configuración...');
        
        // Simular guardado
        this.showNotification('Configuración guardada exitosamente', 'success');
        this.closeModal('settingsModal');
    }

    // Funciones para depósito
    showDeposit() {
        const acc = this.accountData?.account || this.accountData || {};
        const balParsed = parseFloat(acc.balance);
        const balance = Number.isFinite(balParsed) ? balParsed : 0;
        const eqParsed = parseFloat(acc.equity);
        const equity = Number.isFinite(eqParsed) ? eqParsed : balance;
        this.openModal('depositModal');
        const cur = (acc.currency || 'USD').toUpperCase();
        const amt = document.getElementById('depositAmount');
        if (amt) { amt.placeholder = `0.00 ${cur}`; }
        const minEl = document.getElementById('depositMinText');
        if (minEl) { minEl.textContent = `Monto mínimo: 10 ${cur}`; }
        const methodsContainer = document.getElementById('depositMethods');
        const preloaded = methodsContainer && methodsContainer.getAttribute('data-loaded') === '1';
        if (!preloaded) this.updateDepositMethods(cur);
        setTimeout(()=>{ try { this.loadDepositHistory(); } catch(_) {} }, 600);
        const vals = document.querySelectorAll('#depositModal .balance-info .balance-item .value');
        if (vals && vals.length >= 2) {
            vals[0].textContent = `$${balance.toFixed(2)}`;
            vals[1].textContent = `$${equity.toFixed(2)}`;
        }
        this.updateDepositState();
    }

    async updateDepositMethods(currency = null) {
        const container = document.getElementById('depositMethods');
        if (!container) return;
        const cur = (currency || (this.accountData?.account?.currency || 'USD')).toUpperCase();
        try {
            const cached = localStorage.getItem(`wtPaymentMethods_${cur}`);
            if (cached) {
                const parsed = JSON.parse(cached);
                const arr = Array.isArray(parsed?.methods) ? parsed.methods : [];
                if (arr.length) {
                    const tiles = arr.map(m => {
                        const val = String(m.provider_key || m.name || '').toLowerCase();
                        const title = m.display_name || m.name || val;
                        const logo = m.logo_path ? m.logo_path : null;
                        const imgHtml = logo ? `<img class="method-logo" src="${logo}" alt="${title}"/>` : '<i class="fas fa-credit-card"></i>';
                        return `<label class="method-chip" data-value="${val}"><input class="method-input" type="radio" name="depositMethod" value="${val}">${imgHtml}</label>`;
                    }).join('');
                    container.innerHTML = `<div class="method-group"><div class="method-tiles">${tiles}</div></div>`;
                    container.setAttribute('data-loaded','1');
                }
            }
        } catch(_) {}
        if (container.getAttribute('data-loaded') !== '1') {
            container.innerHTML = '<div class="skeleton" style="height:96px;border-radius:8px"></div>';
        }
        try {
            const qp = currency ? `?type=deposit&currency=${encodeURIComponent(currency)}` : '?type=deposit';
            const res = await fetch('api/payment_methods.php' + qp);
            const data = await res.json().catch(()=>({success:false, methods:[]}));
            let methods = (data && data.success && Array.isArray(data.methods)) ? data.methods : [];
            if (!methods.length) { container.innerHTML = '<div class="alert alert-info">No hay métodos activos configurados.</div>'; return; }
            const logoHtml = (key) => {
                const k = String(key||'').toLowerCase();
                if (k === 'paypal') return '<i class="fab fa-paypal"></i>';
                if (k === 'bank' || k === 'transfer') return '<i class="fas fa-university"></i>';
                if (k === 'crypto' || k === 'btc' || k === 'usdt') return '<i class="fas fa-coins"></i>';
                // big4pay o card
                return '<i class="fas fa-credit-card"></i>';
            };
            const toGroup = (catRaw) => {
                const c = String(catRaw||'').toLowerCase();
                if (['card','cards','visa','mastercard'].includes(c)) return 'Cards';
                if (['e-payments','epayments','wallet','paypal','bank','transfer','jeton','webmoney','perfect_money','volet','payeer'].includes(c)) return 'E-payments';
                if (['crypto','cryptocurrency','usdt','tether','btc','binance','binance_pay'].includes(c)) return 'Crypto currency';
                return 'Others';
            };
            const selectedVal = this._depositMethodSelected || (document.querySelector('input[name="depositMethod"]:checked')?.value) || '';
            const groups = {};
            methods.forEach(m => {
                const g = toGroup(m.category);
                if (!groups[g]) groups[g] = [];
                groups[g].push(m);
            });
            const order = ['Cards','E-payments','Crypto currency','Others'];
            const groupIcon = (g) => {
                if (g==='Cards') return '<i class="fas fa-credit-card"></i>';
                if (g==='E-payments') return '<i class="fas fa-wallet"></i>';
                if (g==='Crypto currency') return '<i class="fas fa-coins"></i>';
                return '<i class="fas fa-ellipsis-h"></i>';
            };
            const renderTile = (m) => {
                const val = String(m.provider_key || m.name || '').toLowerCase();
                const title = m.display_name || m.name || val;
                let logo = m.logo_path ? (m.logo_path.startsWith('http') ? m.logo_path : (m.logo_path.startsWith('/') ? m.logo_path : ((location.pathname.indexOf('/simple_crm/')!==-1?'/simple_crm/':'/') + m.logo_path))) : null;
                const imgHtml = logo ? `<img class="method-logo" src="${logo}" alt="${title}"/>` : logoHtml(val);
                const sc = String(m.supported_currencies || '').trim();
                const firstCur = sc ? (sc.split(/[\s,;|]+/).filter(Boolean)[0] || '') : '';
                const curChip = firstCur ? `<span class="badge badge-info">${firstCur}</span>` : (currency ? `<span class="badge badge-info">${currency}</span>` : '');
                const checked = selectedVal && selectedVal === val ? 'checked' : '';
                const selectedCls = selectedVal && selectedVal === val ? ' selected' : '';
                const min = (typeof m.min_amount !== 'undefined' && m.min_amount !== null) ? parseFloat(m.min_amount) : null;
                const max = (typeof m.max_amount !== 'undefined' && m.max_amount !== null) ? parseFloat(m.max_amount) : null;
                const eta = (m.eta_label || '').trim();
                return `<label class="method-chip${selectedCls}" data-value="${val}">
                    <input class="method-input" type="radio" name="depositMethod" value="${val}" ${checked}>
                    <span class="chip-contents">${imgHtml}</span>
                </label>`;
            };
            const html = order.filter(g => groups[g] && groups[g].length).map(g => {
                const tiles = groups[g].map(renderTile).join('');
                return `<div class="method-group"><div class="method-group-title">${groupIcon(g)}<span>${g}</span></div><div class="method-tiles">${tiles}</div></div>`;
            }).join('');
            container.innerHTML = html;
            container.querySelectorAll('.method-chip').forEach(el => {
                el.addEventListener('click', ()=>{
                    const inp = el.querySelector('input.method-input');
                    if (inp) { inp.checked = true; }
                    container.querySelectorAll('.method-chip').forEach(x=>x.classList.remove('selected'));
                    el.classList.add('selected');
                    this._depositMethodSelected = el.getAttribute('data-value');
                    this.updateDepositState();
                });
            });
            container.setAttribute('data-loaded','1');
            try { localStorage.setItem(`wtPaymentMethods_${cur}`, JSON.stringify({ methods })); } catch(_) {}
            /* country chips removed */
        } catch (e) {
            container.innerHTML = '<div class="alert alert-error">Error cargando métodos</div>';
        }
    }

    async preCachePaymentMethods(currency = 'USD') {
        try {
            const cur = String(currency || 'USD').toUpperCase();
            const res = await fetch('api/payment_methods.php?type=deposit&currency=' + encodeURIComponent(cur));
            const data = await res.json().catch(()=>({success:false, methods:[]}));
            const methods = (data && data.success && Array.isArray(data.methods)) ? data.methods : [];
            if (methods.length) {
                localStorage.setItem(`wtPaymentMethods_${cur}`, JSON.stringify({ methods }));
            }
        } catch(_) {}
    }

    renderDepositCountry(currency) {
        const cont = document.getElementById('depositCountrySelect');
        if (!cont) return;
        const countries = [
            {code:'MX', name:'MEXICO', label:'México'},
            {code:'AR', name:'ARGENTINA', label:'Argentina'},
            {code:'BR', name:'BRAZIL', label:'Brazil'},
            {code:'CL', name:'CHILE', label:'Chile'},
            {code:'CO', name:'COLOMBIA', label:'Colombia'},
            {code:'PE', name:'PERU', label:'Perú'},
            {code:'EC', name:'ECUADOR', label:'Ecuador'},
            {code:'BO', name:'BOLIVIA', label:'Bolivia'},
            {code:'UY', name:'URUGUAY', label:'Uruguay'}
        ];
        cont.innerHTML = countries.map(c => {
            const src = `assets/currency/${c.name}.svg`;
            const img = `<img class="flag-svg" src="${src}" alt="${c.label}" width="22" height="16" fetchpriority="high">`;
            return `<label class="country-chip" data-code="${c.code}"><input type="radio" name="depositCountry" value="${c.code}">${img}<span>${c.label}</span></label>`;
        }).join('');
        cont.querySelectorAll('.country-chip').forEach(el => {
            el.addEventListener('click', ()=>{
                const inp = el.querySelector('input[type="radio"]');
                if (inp) inp.checked = true;
                cont.querySelectorAll('.country-chip').forEach(x=>x.classList.remove('selected'));
                el.classList.add('selected');
                this._depositCountrySelected = el.getAttribute('data-code');
                this.updateDepositState();
            });
        });
        if (!this._depositCountrySelected) this._depositCountrySelected = 'MX';
        const def = cont.querySelector(`.country-chip[data-code="${this._depositCountrySelected}"]`);
        if (def) {
            const inp = def.querySelector('input');
            if (inp) inp.checked = true;
            def.classList.add('selected');
        }
    }

    processDeposit(event) {
        event.preventDefault();
        
        const method = (document.querySelector('input[name="depositMethod"]:checked')?.value) || '';
        const amount = parseFloat(document.getElementById('depositAmount').value || '0');
        const currency = (this.accountData?.account?.currency || 'USD');
        const accNum = (this.activeAccountNumber || this.accountData?.account?.account_number || '');
        const country = (String(currency).toUpperCase() === 'MXN') ? 'MX' : 'US';
        
        if (!Number.isFinite(amount) || amount < 10) {
            this.showNotification('El monto mínimo de depósito es $10.00', 'error');
            return;
        }
        if (!method) {
            this.showNotification('Selecciona un método.', 'error');
            return;
        }
        this.showNotification('Iniciando pago...', 'info');
        const submitBtn = document.querySelector('#depositModal .form-actions .btn-success');
        if (submitBtn) submitBtn.disabled = true;
        fetch('api/deposit_initiate.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({ method, amount: String(amount), currency, account_number: accNum, country }).toString()
        }).then(r=>r.json()).then(json=>{
            if (json && json.success && json.redirect_url) {
                window.location.href = json.redirect_url;
            } else {
                const msg = (json && json.message) ? json.message : 'No se pudo iniciar el pago';
                this.showNotification(msg, 'error');
            }
        }).catch(err=>{
            this.showNotification('Error al iniciar el pago', 'error');
        }).finally(()=>{ if (submitBtn) submitBtn.disabled = false; });
    }

    updateDepositState() {
        const methodChecked = !!document.querySelector('input[name="depositMethod"]:checked');
        const amtEl = document.getElementById('depositAmount');
        const amount = parseFloat(amtEl?.value || '0');
        const submitBtn = document.querySelector('#depositModal .form-actions .btn-success');
        const formGroup = amtEl ? amtEl.closest('.form-group') : null;
        const modal = document.getElementById('depositModal');
        if (modal) { modal.querySelectorAll('.inline-error').forEach(n => n.remove()); }
        const ok = methodChecked && Number.isFinite(amount) && amount >= 10;
        if (submitBtn) submitBtn.disabled = !ok;
        if (!ok && amtEl) {
            const cur = (this.accountData?.account?.currency || 'USD').toUpperCase();
            let msg = '';
            if (!methodChecked) msg = 'Selecciona un método.';
            else if (!Number.isFinite(amount)) msg = 'Número inválido';
            else if (amount <= 0) msg = 'Debe ser mayor a 0';
            // Para monto < mínimo, no mostrar error en rojo; sólo deshabilitar botón (ya hay texto fijo de mínimo)
            if (msg) {
                const small = document.createElement('small');
                small.className = 'inline-error';
                small.textContent = msg;
                (formGroup || amtEl.parentElement).appendChild(small);
            }
        }
        amtEl && amtEl.addEventListener('input', ()=>{ this.updateDepositState(); }, { once: true });
    }

    // Funciones para retiro
    showWithdraw() {
        try {
            const dd = document.getElementById('userDropdown');
            const btn = document.getElementById('userMenuBtn');
            if (dd) dd.classList.remove('show');
            if (btn) btn.classList.remove('active');
        } catch(_) {}
        const acc = this.accountData?.account || this.accountData || {};
        const balParsed = parseFloat(acc.balance);
        const balance = Number.isFinite(balParsed) ? balParsed : 0;
        const mParsed = parseFloat(acc.margin);
        const margin = Number.isFinite(mParsed) ? mParsed : 0;
        const fmParsed = parseFloat(acc.free_margin);
        const freeMargin = Number.isFinite(fmParsed) ? fmParsed : (Number.isFinite(parseFloat(acc.equity)) ? parseFloat(acc.equity) - margin : balance - margin);
        this.openModal('withdrawModal');
        const vals = document.querySelectorAll('#withdrawModal .balance-info .balance-item .value');
        if (vals && vals.length >= 3) {
            vals[0].textContent = `$${balance.toFixed(2)}`;
            vals[1].textContent = `$${margin.toFixed(2)}`;
            vals[2].textContent = `$${freeMargin.toFixed(2)}`;
        }
        this.updateWithdrawState();
    }

    processWithdraw(event) {
        event.preventDefault();
        
        const method = (document.querySelector('input[name="withdrawMethod"]:checked')?.value) || (document.getElementById('withdrawMethod')?.value) || 'bank';
        const amount = document.getElementById('withdrawAmount').value;
        
        if (!amount || amount < 50) {
            this.showNotification('El monto mínimo de retiro es $50.00', 'error');
            return;
        }
        
        if (amount > 9000) {
            this.showNotification('Monto excede el disponible para retiro', 'error');
            return;
        }
        
        console.log(`Procesando retiro: $${amount} via ${method}`);
        
        // Simular procesamiento
        this.showNotification('Procesando solicitud de retiro...', 'info');
        
        setTimeout(() => {
            this.showNotification(`Solicitud de retiro de $${amount} enviada exitosamente`, 'success');
            this.closeModal('withdrawModal');
            
            // Actualizar balance (simulado)
            this.updateAccountBalance();
        }, 2000);
    }

    updateWithdrawState() {
        const methodChecked = !!document.querySelector('input[name="withdrawMethod"]:checked');
        const amtEl = document.getElementById('withdrawAmount');
        const amount = parseFloat(amtEl?.value || '0');
        const submitBtn = document.querySelector('#withdrawModal .form-actions .btn-warning');
        const errSel = document.querySelector('#withdrawAmount')?.nextElementSibling;
        const hasErr = errSel && errSel.classList && errSel.classList.contains('inline-error');
        const ok = methodChecked && Number.isFinite(amount) && amount >= 50;
        if (submitBtn) submitBtn.disabled = !ok;
        if (hasErr) errSel.remove();
        if (!ok && amtEl) {
            const msg = !methodChecked ? 'Selecciona un método.' : 'Monto mínimo: $50.00';
            const small = document.createElement('small');
            small.className = 'inline-error';
            small.textContent = msg;
            amtEl.parentElement.appendChild(small);
        }
        amtEl && amtEl.addEventListener('input', ()=>{ this.updateWithdrawState(); }, { once: true });
    }

    // Función para actualizar balance (simulada)
    async updateAccountBalance() {
        try {
            const params = new URLSearchParams();
            if (this.activeAccountNumber) params.set('account_number', this.activeAccountNumber);
            const qp = params.toString() ? `?${params.toString()}` : '';

            const resp = await fetch(`api/account.php${qp}`);
            const ct = (resp.headers && resp.headers.get('Content-Type')) || '';
            let json = {};
            if (ct.indexOf('application/json') !== -1) {
                try { json = await resp.json(); } catch (_) { json = {}; }
            } else {
                try {
                    const txt = await resp.text();
                    try { json = JSON.parse(txt); } catch (_) { json = {}; }
                } catch (_) { json = {}; }
            }
            const acc = json?.account || json?.data || json || {};
            // Mezclar datos nuevos en estructura existente respetando valores 0
            if (!this.accountData || typeof this.accountData !== 'object') this.accountData = {};
            this.accountData = { ...this.accountData, account: { ...(this.accountData.account || {}), ...acc } };
            this.updateAccountInfo();
            console.log('🔄 Balance/Equity/Margen actualizados desde backend');
        } catch (e) {
            console.warn('No se pudo actualizar el balance desde backend', e);
            this.showNotification('No se pudo actualizar el balance', 'error');
        }
    }

    // Función mejorada para mostrar notificaciones
    showNotification(message, type = 'info') {
        // Remover notificaciones existentes
        const existingNotifications = document.querySelectorAll('.notification');
        existingNotifications.forEach(notification => {
            notification.remove();
        });

        const notification = document.createElement('div');
        notification.className = `notification notification-${type}`;
        
        const icon = type === 'success' ? 'fas fa-check-circle' : 
                    type === 'error' ? 'fas fa-exclamation-circle' : 
                    'fas fa-info-circle';
        
        notification.innerHTML = `
            <i class="${icon}"></i>
            <span>${message}</span>
        `;
        
        document.body.appendChild(notification);
        
        // Mostrar notificación con animación
        setTimeout(() => {
            notification.classList.add('show');
        }, 100);
        
        // Remover después de 4 segundos
        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => {
                if (notification.parentNode) {
                    notification.remove();
                }
            }, 300);
        }, 4000);
    }

    // Inicialización de menú de usuario y acciones de cabecera
    initializeUserMenu() {
        const userMenuBtn = document.getElementById('userMenuBtn');
        const userDropdown = document.getElementById('userDropdown');
        if (userMenuBtn && userDropdown) {
            userMenuBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                userDropdown.classList.toggle('show');
                userMenuBtn.classList.toggle('active');
            });
            // Cerrar menú al hacer clic fuera
            document.addEventListener('click', (e) => {
                if (document.body.classList.contains('modal-open')) return;
                if (!userMenuBtn.contains(e.target) && !userDropdown.contains(e.target)) {
                    userDropdown.classList.remove('show');
                    userMenuBtn.classList.remove('active');
                }
            });
        }
        this.setupUserMenuDropdown();
    }

    setupUserMenuDropdown() {
        document.getElementById('profileBtn')?.addEventListener('click', (e) => {
            e.preventDefault();
            this.openProfileModal();
        });
        document.getElementById('historyBtn')?.addEventListener('click', (e) => {
            e.preventDefault();
            this.openHistoryModal();
        });
        document.getElementById('reportsBtn')?.addEventListener('click', (e) => {
            e.preventDefault();
            // Abrir el modal de reportes; el botón interno llama a downloadReport()
            this.openModal('reportsModal');
        });
        document.getElementById('accountSettingsBtn')?.addEventListener('click', (e) => {
            e.preventDefault();
            this.openAccountSettings();
        });
        document.getElementById('withdrawMenuBtn')?.addEventListener('click', (e) => {
            e.preventDefault();
            this.showWithdraw();
        });
    }

    // Inicializar botones de depósito y retiro en el header
    initializeDepositWithdraw() {
        const depositBtn = document.getElementById('depositBtn');
        const withdrawBtn = document.getElementById('withdrawBtn');
        if (depositBtn) {
            depositBtn.addEventListener('click', () => this.showDeposit());
        }
        if (withdrawBtn) {
            withdrawBtn.addEventListener('click', () => this.showWithdraw());
        }
    }

    openProfileModal() {
        this.openModal('profileModal');
    }

    openHistoryModal() {
        this.openModal('historyModal');
    }

    openAccountSettings() {
        this.openModal('settingsModal');
    }

    // Tema (claro/oscuro)
    initializeTheme() {
        const savedTheme = localStorage.getItem('webtrader-theme') || 'dark';
        this.setTheme(savedTheme);
    }

    toggleTheme() {
        const current = document.documentElement.getAttribute('data-theme') || 'dark';
        const next = current === 'dark' ? 'light' : 'dark';
        this.setTheme(next);
        this.showNotification(`Tema cambiado a ${next === 'dark' ? 'oscuro' : 'claro'}`, 'info');
    }

    setTheme(theme) {
        document.documentElement.setAttribute('data-theme', theme);
        localStorage.setItem('webtrader-theme', theme);
        const themeIcon = document.getElementById('themeIcon');
        if (themeIcon) {
            themeIcon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
            themeIcon.classList.remove('spin-once');
            void themeIcon.offsetWidth;
            themeIcon.classList.add('spin-once');
        }
        if (this.charts.main) {
            this.updateChartTheme(theme);
        }
    }

    // Animar badge de alertas al hacer click y cuando llega un evento
    initializeAlertBadge() {
        const alertsBtn = document.getElementById('alertsBtn');
        const badge = document.getElementById('alertCount');
        if (alertsBtn && badge) {
            alertsBtn.addEventListener('click', () => {
                // Animación
                badge.classList.remove('bump');
                void badge.offsetWidth;
                badge.classList.add('bump');
                // Abrir tab de alertas y cargar lista
                try {
                    this.switchTab('alerts');
                    this.loadAlertsList && this.loadAlertsList();
                } catch (e) {}
            });
        }
        // Si tenemos WebSocket, escuchar eventos de alerta
        if (this.wsClient) {
            this.wsClient.on('alert', (data) => {
                if (!badge) return;
                const current = parseInt(badge.textContent || '0', 10) || 0;
                badge.textContent = String(current + 1);
                badge.classList.remove('bump');
                void badge.offsetWidth;
                badge.classList.add('bump');
            });
        }
    }

    async loadAlertsList() {
        const container = document.getElementById('alerts-list');
        if (!container) return;
        container.innerHTML = '<div class="skeleton" style="height:48px;border-radius:8px"></div>';

        const url = 'api/alerts.php';
        try {
            const res = await fetch(url);
            const data = await res.json().catch(() => ({ success: false, alerts: [] }));

            if (!data.success) {
                container.innerHTML = '<div class="alert alert-error">No se pudieron cargar las alertas</div>';
                return;
            }

            const alerts = Array.isArray(data.alerts) ? data.alerts : [];
            if (alerts.length === 0) {
                container.innerHTML = '<div class="alert alert-info">No tienes alertas activas.</div>';
                return;
            }

            const typeMap = {
                above: 'Por encima',
                below: 'Por debajo',
                cross_up: 'Cruce al alza',
                cross_down: 'Cruce a la baja'
            };

            const rows = alerts.map(a => {
                const typeLabel = typeMap[a.condition_type] || a.condition_type;
                const tp = Number(a.target_price).toFixed(5);
                const cp = a.current_price != null ? Number(a.current_price).toFixed(5) : '-';
                const msg = a.message ? a.message : '';
                return `
                    <div class="alert-item">
                        <div class="alert-left">
                            <strong>${a.symbol}</strong>
                            <span class="muted">${typeLabel}</span>
                        </div>
                        <div class="alert-center">
                            <span>Objetivo: ${tp}</span>
                            <span>Actual: ${cp}</span>
                        </div>
                        <div class="alert-right">
                            <span class="muted">${msg}</span>
                        </div>
                    </div>
                `;
            }).join('');

            container.innerHTML = `<div class="alerts-container">${rows}</div>`;
        } catch (e) {
            container.innerHTML = '<div class="alert alert-error">Error de red cargando alertas</div>';
        }
    }

    updateChartTheme(theme) {
        const chart = this.charts.main;
        if (!chart) return;
        const isDark = theme === 'dark';
        const gridColor = isDark ? '#2b3139' : '#e1e5e9';
        const textColor = isDark ? '#c7ccd1' : '#4a5568';
        chart.options.scales.x.grid.color = gridColor;
        chart.options.scales.y.grid.color = gridColor;
        chart.options.scales.x.ticks.color = textColor;
        chart.options.scales.y.ticks.color = textColor;
        chart.options.plugins.legend.labels.color = textColor;
        chart.update('none');
    }

    // Mini buscador en el header con filtrado en vivo para Market Watch
    initializeHeaderSearch() {
        const input = document.getElementById('headerSearchInput');
        if (!input) return;
        input.value = this.mwSearch || '';
        let debounce;
        const syncMainSearch = () => {
            const main = document.getElementById('mwSearch');
            if (main) main.value = this.mwSearch;
        };
        input.addEventListener('input', () => {
            this.mwSearch = input.value.trim();
            try { localStorage.setItem('wtMwSearch', this.mwSearch); } catch (e) {}
            syncMainSearch();
            clearTimeout(debounce);
            debounce = setTimeout(() => this.updateMarketWatchTable(this.instruments), 120);
        });
    }

    destroy() {
        // Limpiar intervalos
        if (this.updateInterval) {
            clearInterval(this.updateInterval);
        }
        if (this.chartUpdateInterval) {
            clearInterval(this.chartUpdateInterval);
        }

        // Cerrar WebSocket si existe
        if (this.websocket) {
            this.websocket.close();
        }

        console.log('🛑 WebTrader destruido');
    }
}

// Inicializar WebTrader cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
    // Evitar doble instanciación si el layout ya creó la app
    if (window.webTraderApp) {
        window.webTrader = window.webTraderApp;
    } else {
        const app = new WebTraderApp(window.webtraderConfig);
        window.webTraderApp = app;
        window.webTrader = app;
    }
});

// Limpiar al salir de la página
window.addEventListener('beforeunload', () => {
    const app = window.webTraderApp || window.webTrader;
    if (app && typeof app.destroy === 'function') {
        app.destroy();
    }
});

async function wtPlaceAdvancedOrder(app, symbol, side, actionsRoot) {
    try {
        const panel = actionsRoot.querySelector('.order-advanced-panel');
        const volInput = panel?.querySelector('.vol-input');
        const volume = parseFloat(volInput?.value || '0.01');
        const sellPriceEl = actionsRoot.querySelector('.order-btn.sell .order-price');
        const buyPriceEl = actionsRoot.querySelector('.order-btn.buy .order-price');
        const bid = parseFloat(sellPriceEl?.textContent || '0');
        const ask = parseFloat(buyPriceEl?.textContent || '0');
        const price = side === 'buy' ? ask : bid;
        const slOn = panel?.querySelector('.sl-toggle')?.checked;
        const tpOn = panel?.querySelector('.tp-toggle')?.checked;
        const inst = app.getInstrument(symbol);
        const pipSize = inst?.pip_size ? Number(inst.pip_size) : (/JPY$/.test(symbol) ? 0.01 : 0.0001);
        const contractSize = Number(inst?.contract_size)||100000;
        const pipValuePerLotUSD = app.getPipValuePerLotUSD(symbol);
        const vol = volume;
        const modeTarget = (mode, basePrice, dir, vEl, pEl, ptEl) => app.computeTargetFromMode(mode, basePrice, dir, vEl, pEl, ptEl, pipSize, pipValuePerLotUSD, vol, contractSize);
        let stopLoss = null, takeProfit = null;
        if (slOn) {
            const mode = panel.querySelector('.sl-mode')?.value || 'precio';
            const slPriceEl = panel.querySelector('.sl-price');
            const slValEl = panel.querySelector('.sl-value');
            const slPctEl = panel.querySelector('.sl-percent');
            const slPtsEl = panel.querySelector('.sl-points');
            stopLoss = mode === 'precio' ? parseFloat(slPriceEl?.value || '0') : modeTarget(mode, price, side==='buy' ? -1 : +1, slValEl, slPctEl, slPtsEl);
        }
        if (tpOn) {
            const mode = panel.querySelector('.tp-mode')?.value || 'precio';
            const tpPriceEl = panel.querySelector('.tp-price');
            const tpValEl = panel.querySelector('.tp-value');
            const tpPctEl = panel.querySelector('.tp-percent');
            const tpPtsEl = panel.querySelector('.tp-points');
            takeProfit = mode === 'precio' ? parseFloat(tpPriceEl?.value || '0') : modeTarget(mode, price, side==='buy' ? +1 : -1, tpValEl, tpPctEl, tpPtsEl);
        }
        const typeEl = panel?.querySelector('.type-mode');
        const entryEl = panel?.querySelector('.entry-price');
        const orderType = (typeEl?.value || 'market');
        const finalPrice = (orderType === 'market') ? price : parseFloat(entryEl?.value || String(price));
        const payload = {
            account_number: app.activeAccountNumber || null,
            orderSymbol: symbol,
            orderType: orderType,
            orderSide: side,
            orderVolume: volume,
            orderPrice: finalPrice,
            orderStopLoss: stopLoss,
            orderTakeProfit: takeProfit,
            orderComment: 'Advanced Panel'
        };
        const resp = await fetch('api/submit-order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
        const ct = (resp.headers && resp.headers.get('Content-Type')) || '';
        let json = null;
        let rawText = '';
        if (ct.indexOf('application/json') !== -1) {
            try { json = await resp.json(); } catch (_) { json = null; }
        } else {
            try { rawText = await resp.text(); } catch (_) { rawText = ''; }
        }
        if (!json || !json.success) {
            const msg = (json && (json.message || json.error)) || (rawText ? 'Respuesta no JSON del servidor' : 'No se pudo ejecutar la orden');
            const details = { payload, response_status: resp.status, response_ok: resp.ok, content_type: ct, body: json || rawText };
            const msgEl = document.getElementById('orderErrorMessage');
            const detEl = document.getElementById('orderErrorDetails');
            if (msgEl) msgEl.textContent = msg;
            if (detEl) detEl.textContent = (typeof details.body === 'string') ? details.body : JSON.stringify(details, null, 2);
            try { (app || window.webTraderApp).openModal && (app || window.webTraderApp).openModal('orderErrorModal'); } catch(_) {}
            app.showNotification(msg, 'error');
            return;
        }
        const warn = Array.isArray(json.warnings) ? json.warnings : [];
        if (warn.length) {
            const wm = document.getElementById('orderWarnMessage');
            const wd = document.getElementById('orderWarnDetails');
            if (wm) wm.textContent = 'La orden se ejecutó pero hubo advertencias';
            if (wd) wd.textContent = JSON.stringify({ warnings: warn }, null, 2);
            try { (app || window.webTraderApp).openModal && (app || window.webTraderApp).openModal('orderWarningModal'); } catch(_) {}
        }
        app.showNotification('Orden ejecutada exitosamente', 'success');
        // Actualización inmediata sin esperar carga completa
        const newPos = {
            id: json.order_id,
            symbol,
            type: side,
            volume,
            open_price: finalPrice,
            stop_loss: stopLoss,
            take_profit: takeProfit,
            profit: 0
        };
        if (!Array.isArray(app.positions)) app.positions = [];
        app.positions.unshift(newPos);
        app.updatePositionsTable(app.positions);
        const openHeader = document.querySelector('#positions-tab .positions-header h5');
        if (openHeader) openHeader.textContent = `Posiciones Abiertas (${app.positions.length})`;
        // KPIs: ajustar margen/free_margin inmediatamente acorde a cálculo local
        try { app.updateAccountBalance && app.updateAccountBalance(); } catch(_) {}
    } catch (e) {
        try { (app || window.webTraderApp).showNotification('Error ejecutando orden', 'error'); } catch (_) {}
    }
}

try { window.wtPlaceAdvancedOrder = wtPlaceAdvancedOrder; } catch (e) {}
WebTraderApp.prototype.bindModalTiles = function(modalId) {
        const scope = document.getElementById(modalId);
        if (!scope) return;
        const tiles = scope.querySelectorAll('.method-tile');
        const radios = scope.querySelectorAll('.method-input');
        tiles.forEach(tile=>tile.addEventListener('click', ()=>{
            tiles.forEach(t=>t.classList.remove('selected'));
            tile.classList.add('selected');
            const input = tile.querySelector('.method-input');
            if (input) input.checked = true;
            if (modalId === 'depositModal') this.updateDepositState(); else this.updateWithdrawState();
        }));
        radios.forEach(r=>r.addEventListener('change', ()=>{ if (modalId==='depositModal') this.updateDepositState(); else this.updateWithdrawState(); }));
    }

WebTraderApp.prototype.renderRecentTransactions = function(modalId) {
        const list = document.querySelector(`#${modalId} .txn-list`);
        if (!list) return;
        list.innerHTML = '';
        const acc = this.accountData?.account || this.accountData || {};
        const txns = acc.transactions || [];
        if (!Array.isArray(txns) || txns.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'txn-item';
            empty.textContent = 'No hay movimientos recientes';
            list.appendChild(empty);
            return;
        }
        txns.slice(0,5).forEach(t=>{
            const row = document.createElement('div');
            row.className = 'txn-item';
            row.innerHTML = `<span>${t.type === 'deposit' ? 'Depósito' : 'Retiro'}</span><span>$${Number(t.amount||0).toFixed(2)}</span><span>${t.date||''}</span>`;
            list.appendChild(row);
        });
    }
WebTraderApp.prototype.queueKpiRender = function(targets) {
    this._kpiTarget = targets;
    if (!this._kpiCurrent) {
        this._kpiCurrent = targets;
        this.renderKpis(targets);
        return;
    }
    if (this._kpiAnimating) return;
    this._kpiAnimating = true;
    const duration = 300;
    const start = performance.now();
    const from = this._kpiCurrent;
    const to = this._kpiTarget;
    const lerp = (a,b,t)=>a+(b-a)*t;
    const step = (ts)=>{
        const t = Math.min(1, (ts - start) / duration);
        const cur = {
            balance: lerp(from.balance, to.balance, t),
            equity: lerp(from.equity, to.equity, t),
            margin: lerp(from.margin, to.margin, t),
            freeMargin: lerp(from.freeMargin, to.freeMargin, t),
            marginLevel: lerp(from.marginLevel, to.marginLevel, t),
            profit: lerp(from.profit, to.profit, t)
        };
        this.renderKpis(cur);
        if (t < 1) {
            requestAnimationFrame(step);
        } else {
            this._kpiAnimating = false;
            this._kpiCurrent = to;
            this.renderKpis(to);
        }
    };
    requestAnimationFrame(step);
};

WebTraderApp.prototype.renderKpis = function(values) {
    const setText = (selector, value) => {
        const el = document.querySelector(selector);
        if (el) el.textContent = `$${Number(value).toFixed(2)}`;
    };
    const setPercent = (selector, value) => {
        const el = document.querySelector(selector);
        if (el) el.textContent = `${Number(value).toFixed(2)}%`;
    };
    const profitEl = document.querySelector('.account-summary .profit');
    if (profitEl) {
        profitEl.textContent = `$${values.profit.toFixed(2)}`;
        profitEl.classList.toggle('neg', values.profit < 0);
        profitEl.classList.toggle('pos', values.profit > 0);
    }
    setText('.account-summary .balance', values.balance);
    setText('.account-summary .equity', values.equity);
    setText('.account-summary .margin', values.margin);
    setText('.account-summary .free-margin', values.freeMargin);
    setPercent('.account-summary .margin-level', values.marginLevel);
};

WebTraderApp.prototype.startKpiHeartbeat = function() {
    this.kpiSource = 'backend';
    try { if (this._kpiInterval) clearInterval(this._kpiInterval); } catch(e){}
    this._kpiPending = false;
    const run = async ()=>{
        const accNum = this.activeAccountNumber || (this.accountData?.account?.account_number) || '';
        if (!accNum) return;
        if (this._kpiPending) return;
        this._kpiPending = true;
        try {
            const resp = await fetch(`api/account_kpis.php?account_number=${encodeURIComponent(accNum)}`);
            const ct = (resp.headers && resp.headers.get('Content-Type')) || '';
            let json = { success: false };
            if (ct.indexOf('application/json') !== -1) {
                try { json = await resp.json(); } catch (_) { json = { success: false }; }
            } else {
                try {
                    const txt = await resp.text();
                    try { json = JSON.parse(txt); } catch (_) { json = { success: false }; }
                } catch (_) { json = { success: false }; }
            }
            if (json && json.success && json.kpis) {
                const k = json.kpis;
                const targets = {
                    balance: Number(k.balance||0),
                    equity: Number(k.equity||0),
                    margin: Number(k.margin||0),
                    freeMargin: Number(k.free_margin||0),
                    marginLevel: Number(k.margin_level||0),
                    profit: Number(k.profit||0)
                };
                if (!this.accountData || this.accountData.account) {
                    if (!this.accountData) this.accountData = { account: {} };
                    this.accountData.account = { ...(this.accountData.account||{}), balance: targets.balance, equity: targets.equity, margin: targets.margin, free_margin: targets.freeMargin, margin_level: targets.marginLevel };
                } else {
                    this.accountData.balance = targets.balance;
                    this.accountData.equity = targets.equity;
                    this.accountData.margin = targets.margin;
                    this.accountData.free_margin = targets.freeMargin;
                    this.accountData.margin_level = targets.marginLevel;
                }
                this.queueKpiRender ? this.queueKpiRender(targets) : this.renderKpis(targets);
            }
        } catch(e) { /* reintento en próximo ciclo */ } finally { this._kpiPending = false; }
    };
    this._kpiInterval = setInterval(run, 2000);
    run();
};

WebTraderApp.prototype.startPositionsHeartbeat = function() {
    if (this._posInterval) clearInterval(this._posInterval);
    const run = async () => {
        if (this._posPending) return;
        this._posPending = true;
        try {
            const params = new URLSearchParams();
            if (this.activeAccountNumber) params.set('account_number', this.activeAccountNumber);
            const qp = params.toString() ? `?${params.toString()}` : '';
            // Abiertas
            const respOpen = await fetch(`api/positions.php${qp}`);
            const ctOpen = (respOpen.headers && respOpen.headers.get('Content-Type')) || '';
            let jsonOpen = { positions: [] };
            if (ctOpen.indexOf('application/json') !== -1) {
                try { jsonOpen = await respOpen.json(); } catch (_) { jsonOpen = { positions: [] }; }
            } else {
                try { const txt = await respOpen.text(); jsonOpen = JSON.parse(txt); } catch (_) { jsonOpen = { positions: [] }; }
            }
            const openList = Array.isArray(jsonOpen) ? jsonOpen : (jsonOpen.positions || []);
            this.positions = openList;
            this.updatePositionsTable(openList);
            // Actualizar contador en header
            const openHeader = document.querySelector('#positions-tab .positions-header h5');
            if (openHeader) openHeader.textContent = `Posiciones Abiertas (${openList.length})`;

            // Cerradas: solo consultar si el tab está activo/visible
            const closedTab = document.getElementById('closed-tab');
            const shouldFetchClosed = !!(closedTab && closedTab.classList.contains('active'));
            let respClosed, ctClosed;
            if (shouldFetchClosed) {
                respClosed = await fetch(`api/positions.php${qp ? qp + '&' : '?'}status=closed`);
                ctClosed = (respClosed.headers && respClosed.headers.get('Content-Type')) || '';
            }
            let jsonClosed = { positions: [] };
            let closedList = [];
            if (shouldFetchClosed) {
                if (ctClosed.indexOf('application/json') !== -1) {
                    try { jsonClosed = await respClosed.json(); } catch (_) { jsonClosed = { positions: [] }; }
                } else {
                    try { const txt = await respClosed.text(); jsonClosed = JSON.parse(txt); } catch (_) { jsonClosed = { positions: [] }; }
                }
                closedList = Array.isArray(jsonClosed) ? jsonClosed : (jsonClosed.positions || []);
            }
            const closedTbody = document.getElementById('closed-tbody');
            if (closedTbody) {
                closedTbody.innerHTML = '';
                closedList.forEach(p => {
                    const row = document.createElement('tr');
                    const sideText = ((p.side || p.type || '') + '').toLowerCase();
                    const sideUpper = sideText ? sideText.toUpperCase() : '';
                    const sideBadgeClass = sideText === 'buy' ? 'success' : 'danger';
                    const vol = parseFloat(p.volume);
                    const pnlVal = parseFloat(p.profit ?? p.pnl ?? '0');
                    const pnlClass = pnlVal >= 0 ? 'profit' : 'loss';
                    row.setAttribute('data-position-id', p.id);
                    row.innerHTML = `
                        <td>${p.closed_at ? p.closed_at : ''}</td>
                        <td>${p.symbol}</td>
                        <td><span class="badge badge-${sideBadgeClass}">${sideUpper}</span></td>
                        <td class="numeric">${Number.isFinite(vol) ? vol.toFixed(2) : p.volume}</td>
                        <td class="numeric">${p.open_price ?? '-'}</td>
                        <td class="numeric">${p.close_price ?? '-'}</td>
                        <td class="pnl numeric ${pnlClass}">$${Number.isFinite(pnlVal) ? pnlVal.toFixed(2) : '0.00'}</td>
                        <td class="numeric">${Number.isFinite(parseFloat(p.commission)) ? parseFloat(p.commission).toFixed(2) : '0.00'}</td>
                    `;
                    closedTbody.appendChild(row);
                });
            }
            const closedHeader = document.querySelector('#closed-tab .positions-header h5');
            if (closedHeader) closedHeader.textContent = `Operaciones Cerradas (${closedList.length})`;
        } catch(_) { /* ignorar errores y reintentar */ } finally { this._posPending = false; }
    };
    this._posInterval = setInterval(run, 4000);
    run();
};
WebTraderApp.prototype.loadDepositHistory = async function() {
        const accNum = this.activeAccountNumber || (this.accountData?.account?.account_number) || '';
        const box = document.getElementById('depositHistory');
        if (!box) return;
        box.innerHTML = '<div class="skeleton" style="height:36px;border-radius:8px"></div>';
        try {
            const qp = accNum ? ('?account_number=' + encodeURIComponent(accNum)) : '';
            const res = await fetch('api/deposit_history.php' + qp);
            const data = await res.json();
            const rows = (data && data.success && Array.isArray(data.attempts)) ? data.attempts : [];
            if (!rows.length) { box.innerHTML = '<div class="alert alert-info">Sin intentos aún</div>'; return; }
            box.innerHTML = rows.map(r=>{
                const dt = r.updated_at || r.created_at;
                const st = String(r.status||'').toUpperCase();
                return `<div class="attempt-row"><span>${dt}</span><span>${r.method}</span><span>${st}</span><span>${r.amount} ${r.currency}</span></div>`;
            }).join('');
        } catch(e) { box.innerHTML = '<div class="alert alert-error">Error cargando historial</div>'; }
}
