← Back
// ==========================================
// Mini-Charts v3 — Full Market Screener
// Uses IntersectionObserver to only render visible charts
// IndexedDB persistent cache for instant chart loading
// ==========================================

// ---- IndexedDB Klines Cache ----
const IDB = {
    db: null,
    DB_NAME: 'FuturesScreenerKlines',
    STORE: 'klines',
    VERSION: 1,

    async open() {
        if (this.db) return this.db;
        return new Promise((resolve, reject) => {
            const req = indexedDB.open(this.DB_NAME, this.VERSION);
            req.onupgradeneeded = (e) => {
                const db = e.target.result;
                if (!db.objectStoreNames.contains(this.STORE)) {
                    db.createObjectStore(this.STORE); // key = "BTCUSDT:5m"
                }
            };
            req.onsuccess = (e) => { this.db = e.target.result; resolve(this.db); };
            req.onerror = () => { console.warn('[IDB] Open failed'); resolve(null); };
        });
    },

    async get(sym, tf) {
        try {
            const db = await this.open();
            if (!db) return null;
            return new Promise((resolve) => {
                const tx = db.transaction(this.STORE, 'readonly');
                const req = tx.objectStore(this.STORE).get(`${sym}:${tf}`);
                req.onsuccess = () => resolve(req.result || null);
                req.onerror = () => resolve(null);
            });
        } catch(e) { return null; }
    },

    async put(sym, tf, candles) {
        try {
            const db = await this.open();
            if (!db || !candles || candles.length === 0) return;
            const tx = db.transaction(this.STORE, 'readwrite');
            tx.objectStore(this.STORE).put({
                candles,
                lastTime: candles[candles.length - 1].time,
                count: candles.length,
                updated: Date.now()
            }, `${sym}:${tf}`);
        } catch(e) { /* quota exceeded or other — ignore */ }
    },

    async clearTF(tf) {
        try {
            const db = await this.open();
            if (!db) return;
            const tx = db.transaction(this.STORE, 'readwrite');
            const store = tx.objectStore(this.STORE);
            const req = store.openCursor();
            req.onsuccess = (e) => {
                const cursor = e.target.result;
                if (!cursor) return;
                if (cursor.key.endsWith(`:${tf}`)) cursor.delete();
                cursor.continue();
            };
        } catch(e) { /* ignore */ }
    }
};
// Pre-open IndexedDB
IDB.open();
const FLAG_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7', '#ec4899'];
const mc = {
    sortBy: 'change',
    sortDir: 'asc',
    globalTF: '5m',
    loaded: false,
    allPairs: [],        // all fetched pairs (unfiltered)
    filteredPairs: [],   // after filters applied
    charts: {},          // { sym: { chart, series, lines[] } } — only visible chart instances
    dataCache: {},       // { sym: { candles, volume, tf } } — persists across scroll (never deleted until TF change)
    loadedData: {},      // { sym: true } — tracks which symbols have been loaded
    observer: null,      // IntersectionObserver
    loadQueue: [],       // queue for staggered loading
    loadingActive: false,
    filters: { minVol: 50, minNatr: 0, minTrades: 0 },
    searchQuery: '',     // coin search filter
    flags: {},           // { sym: '#color' } — color flags, persisted in localStorage
    ws: null,            // Binance kline WebSocket
    wsStreams: new Set(), // currently subscribed streams
    wsPending: new Set(), // streams waiting to subscribe
    srData: {},          // { sym: { type, pct, price, touches } } — nearest S/R distance
    tlData: {},          // { sym: { type, pct, price, touches } } — nearest auto-trendline distance
};

// Load flags from localStorage
try { mc.flags = JSON.parse(localStorage.getItem('mc_flags') || '{}'); } catch(e) { mc.flags = {}; }

// --- Settings helpers ---
const _sp = () => typeof settingsPanel !== 'undefined' ? settingsPanel : null
function spGet(key, fallback) { const sp = _sp(); return sp ? sp.get(key) : fallback }
function getGridOpts() {
  const show = spGet('showGrid', true)
  const c = show ? 'rgba(255,255,255,0.03)' : 'transparent'
  return { vertLines: { color: c }, horzLines: { color: c } }
}
function getVolScaleTop() { return 1 - (spGet('volumeHeight', 15) / 100) }
function getPriceScaleMode() { return spGet('logScale', false) ? 1 : 0 }
function addMainSeries(chart, prec, minMove) {
  const type = spGet('candleType', 'Candlestick')
  const up = spGet('candleUp', '#22c55e')
  const down = spGet('candleDown', '#ef4444')
  const pf = { type: 'price', precision: prec, minMove }
  if (type === 'Line') return chart.addSeries(LightweightCharts.LineSeries, { color: up, lineWidth: 2, priceFormat: pf })
  if (type === 'Area') return chart.addSeries(LightweightCharts.AreaSeries, { topColor: up + '66', bottomColor: up + '0d', lineColor: up, lineWidth: 2, priceFormat: pf })
  if (type === 'Bar') return chart.addSeries(LightweightCharts.BarSeries, { upColor: up, downColor: down, priceFormat: pf })
  return chart.addSeries(LightweightCharts.CandlestickSeries, { upColor: up, downColor: down, borderVisible: false, wickUpColor: up, wickDownColor: down, priceFormat: pf })
}

function saveFlags() {
    lsSet('mc_flags', JSON.stringify(mc.flags));
}

// --- Sidebar & Grid settings ---
const CARD_HEIGHTS = { compact: 200, normal: 270, large: 360 }

function applyGridColumns(n) {
    const grid = document.querySelector('.mc-grid')
    if (grid) grid.style.gridTemplateColumns = `repeat(${n}, 1fr)`
}

function applyCardSize(size) {
    const grid = document.querySelector('.mc-grid')
    if (grid) grid.style.gridAutoRows = (CARD_HEIGHTS[size] || 270) + 'px'
    // Resize existing charts
    Object.values(mc.charts).forEach(c => { try { c.chart.resize() } catch(e) {} })
    setTimeout(() => Object.values(mc.charts).forEach(c => { try { c.chart.applyOptions({ autoSize: true }) } catch(e) {} }), 100)
}

function applySidebarColumns() {
    const showChg = spGet('colChg', true)
    const showNatr = spGet('colNatr', true)
    const showVol = spGet('colVol', true)

    // Build grid-template-columns: flag(20px) name(1fr) ★(18px) copy(16px) [chg 50px] [natr 34px] [vol 40px]
    let cols = '20px 1fr 18px 16px'
    if (showChg) cols += ' 50px'
    if (showNatr) cols += ' 34px'
    if (showVol) cols += ' 40px'

    // Apply to header
    const hdr = document.getElementById('mcColHeaders')
    if (hdr) {
        hdr.style.gridTemplateColumns = cols
        // Show/hide header spans
        const spans = hdr.querySelectorAll('.mc-col-hdr')
        spans.forEach(s => {
            if (s.dataset.sort === 'change') s.style.display = showChg ? '' : 'none'
            if (s.dataset.sort === 'natr') s.style.display = showNatr ? '' : 'none'
            if (s.dataset.sort === 'volume') s.style.display = showVol ? '' : 'none'
        })
    }

    // Apply to all coin items
    document.querySelectorAll('.mc-coin-item').forEach(item => {
        item.style.gridTemplateColumns = cols
        const chgEl = item.querySelector('.mc-coin-change')
        const natrEl = item.querySelector('.mc-coin-natr')
        const volEl = item.querySelector('.mc-coin-vol')
        if (chgEl) chgEl.style.display = showChg ? '' : 'none'
        if (natrEl) natrEl.style.display = showNatr ? '' : 'none'
        if (volEl) volEl.style.display = showVol ? '' : 'none'
    })
}

function applyAllLayoutSettings() {
    applyGridColumns(spGet('cardsPerRow', 4))
    applyCardSize(spGet('cardSize', 'normal'))
    applySidebarColumns()
}

// Rebuild all visible charts (called when chart settings change)
function rebuildAllCharts() {
    // Destroy all mini-chart instances, remember which were loaded
    const syms = Object.keys(mc.charts)
    syms.forEach(sym => {
        try { mc.charts[sym].chart.remove() } catch(e) {}
    })
    mc.charts = {}
    mc.loadedData = {}

    // Re-create them
    syms.forEach(sym => {
        createChartInstance(sym)
        mc.loadQueue.push(sym)
    })
    processLoadQueue()

    // Rebuild modal if open
    if (modal.chart && modal.currentSym) {
        loadModalChart(modal.currentSym, modal.currentTF)
    }

    // Rebuild multi-chart slots
    if (typeof mch !== 'undefined' && mch.slots) {
        mch.slots.forEach((slot, i) => {
            if (slot.chart && slot.sym) {
                try { slot.chart.remove() } catch(e) {}
                slot.chart = null
                slot.series = null
                slot.volSeries = null
                createSlotChart(i)
                loadSlotChart(i)
            }
        })
    }

    showSettingsToast('Settings applied ✓')
}

function showSettingsToast(msg) {
    const sp = _sp()
    if (sp) sp.showToast(msg)
}

async function initMiniCharts() {
    if (!mc.loaded) {
        mc.loaded = true;

        // Apply saved theme
        const savedTheme = spGet('theme', 'dark')
        if (savedTheme !== 'dark') document.body.classList.add('theme-' + savedTheme)

        // Apply saved data settings
        mc.sortBy = spGet('defaultSort', 'change')
        mc.sortDir = spGet('defaultSortDir', 'asc')
        mc.filters.minVol = spGet('minVolume', 50)

        // Sync sort headers with settings
        const colHeaders = document.getElementById('mcColHeaders')
        if (colHeaders) {
            colHeaders.querySelectorAll('.mc-col-hdr').forEach(h => {
                h.classList.remove('active', 'asc', 'desc')
                if (h.dataset.sort === mc.sortBy) h.classList.add('active', mc.sortDir)
            })
        }
        // Sync volume dropdown
        const volSel = el('mcFilterVol')
        if (volSel) volSel.value = mc.filters.minVol

        // Apply saved layout settings
        applyAllLayoutSettings()

        // Apply default TF from settings
        const savedTF = spGet('defaultTF', '5m')
        if (savedTF) mc.globalTF = savedTF

        // Listen for settings changes
        const sp = _sp()
        if (sp) {
            sp.onChange((key, val) => {
                const chartKeys = ['candleType', 'logScale', 'volumeHeight', 'showGrid', 'showWatermark', 'candleUp', 'candleDown']
                if (chartKeys.includes(key)) {
                    rebuildAllCharts()
                } else if (key === 'defaultTF') {
                    mc.globalTF = val
                    wsUnsubscribeAll() // unsub old TF streams
                    mc.dataCache = {} // TF changed, clear cache
                    mc.srData = {}
                    mc.tlData = {}
                    // Update active TF button
                    const tfGroup = el('mcGlobalTF')
                    if (tfGroup) {
                        tfGroup.querySelectorAll('.mc-tf-btn').forEach(b => b.classList.toggle('active', b.dataset.tf === val))
                    }
                    mc.loadedData = {}
                    mc.loadingActive = false // reset so processLoadQueue can run
                    Object.keys(mc.charts).forEach(sym => mc.loadQueue.push(sym))
                    processLoadQueue()
                    showSettingsToast('Timeframe → ' + val)
                } else if (key === 'densityEnabled') {
                    if (!val) {
                        // Remove density lines from all charts
                        Object.values(mc.charts).forEach(c => {
                            if (c.densityLines) { c.densityLines.forEach(pl => { try { c.series.removePriceLine(pl) } catch(e){} }); c.densityLines = [] }
                        })
                        if (modal.chart && modal.densityLines) { modal.densityLines.forEach(pl => { try { modal.series.removePriceLine(pl) } catch(e){} }); modal.densityLines = [] }
                    } else {
                        // Re-apply densities to all visible charts
                        const visibleSyms = Object.keys(mc.charts).filter(s => mc.charts[s] && mc.charts[s].series)
                        if (visibleSyms.length > 0) applyDensityToBatch(visibleSyms)
                        if (modal.chart && modal.currentSym) applyDensityToModal()
                    }
                    showSettingsToast(val ? 'Densities enabled' : 'Densities disabled')
                } else if (['densityDepthPct', 'densityTTLMin', 'densitySeveritySmall', 'densitySeverityMedium', 'densitySeverityLarge', 'densityBlacklist'].includes(key)) {
                    // Re-apply densities with new filters
                    if (spGet('densityEnabled', true)) {
                        const visibleSyms = Object.keys(mc.charts).filter(s => mc.charts[s] && mc.charts[s].series)
                        if (visibleSyms.length > 0) applyDensityToBatch(visibleSyms)
                        if (modal.chart && modal.currentSym) applyDensityToModal()
                    }
                    showSettingsToast('Density filter updated')
                } else if (key === 'signalMinRatio') {
                    showSettingsToast('Signal ratio → ' + val + 'x')
                } else if (key === 'cardsPerRow') {
                    applyGridColumns(val)
                    showSettingsToast('Cards per row → ' + val)
                } else if (key === 'cardSize') {
                    applyCardSize(val)
                    showSettingsToast('Card size → ' + val)
                } else if (key === 'colChg' || key === 'colNatr' || key === 'colVol') {
                    applySidebarColumns()
                    renderSidebar()
                    showSettingsToast('Sidebar updated')
                } else if (key === 'defaultSort') {
                    mc.sortBy = val
                    renderSidebar()
                    showSettingsToast('Sort → ' + val)
                } else if (key === 'defaultSortDir') {
                    mc.sortDir = val
                    renderSidebar()
                    showSettingsToast('Sort direction → ' + val)
                } else if (key === 'minVolume') {
                    mc.filters.minVol = val
                    // Sync the toolbar dropdown if exists
                    const volSel = el('mcFilterVol')
                    if (volSel) volSel.value = val
                    applyFiltersAndRebuild()
                    showSettingsToast(val > 0 ? 'Min volume → $' + val + 'M' : 'Volume filter OFF')
                } else if (key === 'layout') {
                    // Map settings layout to multi-chart layout
                    if (val === '1') {
                        switchLayout('grid')
                    } else {
                        switchLayout(val)
                    }
                    showSettingsToast('Layout → ' + val)
                } else if (key === 'theme') {
                    document.body.className = document.body.className.replace(/theme-\w+/g, '')
                    if (val !== 'dark') document.body.classList.add('theme-' + val)
                    showSettingsToast('Theme → ' + val)
                } else if (key === 'indicatorOI' || key === 'indicatorOIColor') {
                    // Modal
                    if (modal.chart && modal.currentSym) applyOIOverlay(modal.chart, modal.currentSym)
                    // Mini-charts
                    const visSyms = Object.keys(mc.charts).filter(s => mc.charts[s] && mc.charts[s].chart)
                    if (visSyms.length > 0) applyOIToBatch(visSyms)
                    // Multi-chart slots
                    if (typeof mch !== 'undefined') mch.slots.forEach((slot, i) => { if (slot.chart && slot.sym) applyOI(slot, slot.sym, slot.tf) })
                    showSettingsToast(key === 'indicatorOI' ? (val ? 'OI indicator ON' : 'OI indicator OFF') : 'OI color updated')
                } else if (key === 'watchlistOnly') {
                    renderSidebar()
                } else if (key === '__watchlist') {
                    renderSidebar()
                }
            })
        }

        // Global TF buttons — sync active with settings default
        const tfGroup = el('mcGlobalTF');
        if (tfGroup) {
            tfGroup.querySelectorAll('.mc-tf-btn').forEach(b => b.classList.toggle('active', b.dataset.tf === mc.globalTF));
            tfGroup.addEventListener('click', (e) => {
                const btn = e.target.closest('.mc-tf-btn');
                if (!btn) return;
                tfGroup.querySelectorAll('.mc-tf-btn').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');
                mc.globalTF = btn.dataset.tf;
                console.log('[TF-SWITCH] Switching to', mc.globalTF, 'charts:', Object.keys(mc.charts).length, 'wsStreams:', mc.wsStreams.size);
                // Cancel any pending destroy timers before TF switch
                Object.keys(mc.charts).forEach(s => {
                    if (mc.charts[s]?._destroyTimer) { clearTimeout(mc.charts[s]._destroyTimer); mc.charts[s]._destroyTimer = null; }
                });
                // Unsubscribe old TF streams — critical! Otherwise old TF data
                // keeps arriving and new TF streams may not subscribe properly
                wsUnsubscribeAll();
                // Clear data cache — TF changed, all cached candles are stale
                mc.dataCache = {};
                mc.srData = {};
                mc.tlData = {};
                // Reload all currently visible charts with new TF
                mc.loadedData = {};
                mc.loadingActive = false; // reset so processLoadQueue can run
                Object.keys(mc.charts).forEach(sym => {
                    mc.loadQueue.push(sym);
                });
                processLoadQueue();
                // Refresh NATR for new TF
                fetchServerNATR(mc.globalTF);
            });
        }

        // Sort by column headers in sidebar
        const colHeaders2 = document.getElementById('mcColHeaders');
        if (colHeaders2) {
            colHeaders2.querySelectorAll('.mc-col-hdr').forEach(hdr => {
                hdr.addEventListener('click', () => {
                    const key = hdr.dataset.sort;
                    // Toggle direction if same column, else set desc
                    if (mc.sortBy === key) {
                        mc.sortDir = mc.sortDir === 'desc' ? 'asc' : 'desc';
                    } else {
                        mc.sortBy = key;
                        mc.sortDir = 'desc';
                    }
                    // Update header classes
                    colHeaders2.querySelectorAll('.mc-col-hdr').forEach(h => h.classList.remove('active', 'asc', 'desc'));
                    hdr.classList.add('active', mc.sortDir);
                    // Clear toolbar sort buttons
                    ['mcSortSR', 'mcSortTL', 'mcSortSig'].forEach(id => { const b = el(id); if (b) b.classList.remove('active'); });
                    applyFiltersAndRebuild();
                });
            });
        }

        // Filters
        ['mcFilterVol', 'mcFilterNatr', 'mcFilterTrades'].forEach(id => {
            const sel = el(id);
            if (sel) {
                sel.addEventListener('change', () => {
                    mc.filters.minVol = parseFloat(el('mcFilterVol').value);
                    mc.filters.minNatr = parseFloat(el('mcFilterNatr').value);
                    mc.filters.minTrades = parseFloat(el('mcFilterTrades').value);
                    applyFiltersAndRebuild();
                });
            }
        });

        // Refresh button
        const refreshBtn = el('mcRefreshBtn');
        if (refreshBtn) {
            refreshBtn.addEventListener('click', () => refreshMiniCharts());
        }

        // Density toggle checkbox
        const densityToggle = el('mcDensityToggle');
        if (densityToggle) {
            densityToggle.checked = spGet('densityEnabled', true);
            densityToggle.addEventListener('change', () => {
                const enabled = densityToggle.checked;
                const sp = _sp();
                if (sp) sp.set('densityEnabled', enabled);
                if (!enabled) {
                    // Remove density lines from all mini-charts
                    Object.values(mc.charts).forEach(c => {
                        if (c.densityLines) { c.densityLines.forEach(pl => { try { c.series.removePriceLine(pl) } catch(e){} }); c.densityLines = [] }
                    });
                    // Remove from modal
                    if (modal.densityLines) { modal.densityLines.forEach(pl => { try { modal.series.removePriceLine(pl) } catch(e){} }); modal.densityLines = [] }
                } else {
                    // Re-apply densities
                    const visibleSyms = Object.keys(mc.charts).filter(s => mc.charts[s].series);
                    if (visibleSyms.length > 0) applyDensityToBatch(visibleSyms);
                    if (modal.chart && modal.currentSym) applyDensityToModal();
                }
            });
        }

        // Levels toggle checkbox
        const levelsToggle = el('mcLevelsToggle');
        if (levelsToggle) {
            levelsToggle.checked = spGet('levelsEnabled', true);
            levelsToggle.addEventListener('change', () => {
                const enabled = levelsToggle.checked;
                const sp = _sp();
                if (sp) sp.set('levelsEnabled', enabled);
                if (!enabled) {
                    // Remove auto levels from all mini-charts
                    Object.keys(mc.charts).forEach(sym => removeAutoLevels(sym));
                    // Remove from modal
                    removeModalAutoLevels();
                } else {
                    // Re-apply levels
                    Object.keys(mc.charts).forEach(sym => applyAutoLevels(sym));
                    if (modal.chart && modal.currentSym) applyModalAutoLevels();
                }
            });
        }

        // Sort S/R button
        const sortSRBtn = el('mcSortSR');
        if (sortSRBtn) {
            sortSRBtn.addEventListener('click', () => {
                if (mc.sortBy === 'sr') {
                    mc.sortBy = 'change';
                    mc.sortDir = 'desc';
                    sortSRBtn.classList.remove('active');
                } else {
                    mc.sortBy = 'sr';
                    mc.sortDir = 'desc';
                    sortSRBtn.classList.add('active');
                }
                // Clear other sort buttons
                const sortTL = el('mcSortTL');
                if (sortTL) sortTL.classList.remove('active');
                const sortSig = el('mcSortSig');
                if (sortSig) sortSig.classList.remove('active');
                const colHeaders = document.getElementById('mcColHeaders');
                if (colHeaders) colHeaders.querySelectorAll('.mc-col-hdr').forEach(h => h.classList.remove('active', 'asc', 'desc'));
                if (mc.sortBy !== 'sr') {
                    const chgHdr = colHeaders?.querySelector('[data-sort="change"]');
                    if (chgHdr) chgHdr.classList.add('active', 'desc');
                }
                applyFiltersAndRebuild();
            });
        }

        // Trendlines toggle checkbox
        const trendlinesToggle = el('mcTrendlinesToggle');
        if (trendlinesToggle) {
            trendlinesToggle.checked = spGet('trendlinesEnabled', false);
            trendlinesToggle.addEventListener('change', () => {
                const enabled = trendlinesToggle.checked;
                const sp = _sp();
                if (sp) sp.set('trendlinesEnabled', enabled);
                if (!enabled) {
                    Object.keys(mc.charts).forEach(sym => removeAutoTrendlines(sym));
                    removeModalAutoTrendlines();
                } else {
                    Object.keys(mc.charts).forEach(sym => applyAutoTrendlines(sym));
                    if (modal.chart && modal.currentSym) applyModalAutoTrendlines();
                }
            });
        }

        // Channels dropdown (multi-select per channel type)
        const chBtn = el('mcChannelsBtn');
        const chDrop = el('mcChannelsDrop');
        if (chBtn && chDrop) {
            chBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                chDrop.classList.toggle('open');
            });
            document.addEventListener('click', () => chDrop.classList.remove('open'));
            chDrop.addEventListener('click', (e) => e.stopPropagation());

            // Channel type map: key → { apply, remove, applyModal, removeModal }
            const chTypes = {
                keltner:    { apply: applyKeltnerChannel,    remove: removeKeltnerChannel,    applyModal: applyModalKeltnerChannel,    removeModal: removeModalKeltnerChannel },
                regression: { apply: applyRegressionChannel, remove: removeRegressionChannel, applyModal: applyModalRegressionChannel, removeModal: removeModalRegressionChannel },
            };

            const chBoxes = chDrop.querySelectorAll('input[data-ch]');
            chBoxes.forEach(cb => {
                const chKey = cb.dataset.ch;
                cb.checked = spGet('ch_' + chKey, false);
                cb.addEventListener('change', () => {
                    const sp = _sp();
                    if (sp) sp.set('ch_' + chKey, cb.checked);
                    const fns = chTypes[chKey];
                    if (!fns) return;
                    if (cb.checked) {
                        Object.keys(mc.charts).forEach(sym => fns.apply(sym));
                        if (modal.chart && modal.currentSym) fns.applyModal();
                    } else {
                        Object.keys(mc.charts).forEach(sym => fns.remove(sym));
                        fns.removeModal();
                    }
                    // Update button glow
                    const anyActive = [...chBoxes].some(c => c.checked);
                    chBtn.classList.toggle('has-active', anyActive);
                });
            });
            // Init button glow
            const anyActive = [...chBoxes].some(c => c.checked);
            chBtn.classList.toggle('has-active', anyActive);
        }

        // Sort by trendline proximity button
        const sortTLBtn = el('mcSortTL');
        if (sortTLBtn) {
            sortTLBtn.addEventListener('click', () => {
                if (mc.sortBy === 'tl') {
                    mc.sortBy = 'change';
                    mc.sortDir = 'desc';
                    sortTLBtn.classList.remove('active');
                } else {
                    mc.sortBy = 'tl';
                    mc.sortDir = 'desc';
                    sortTLBtn.classList.add('active');
                }
                // Clear col header highlights + other sort buttons
                const colHeaders = document.getElementById('mcColHeaders');
                if (colHeaders) colHeaders.querySelectorAll('.mc-col-hdr').forEach(h => h.classList.remove('active', 'asc', 'desc'));
                const sortSR = el('mcSortSR');
                if (sortSR) sortSR.classList.remove('active');
                const sortSig2 = el('mcSortSig');
                if (sortSig2) sortSig2.classList.remove('active');
                if (mc.sortBy !== 'tl') {
                    const chgHdr = colHeaders?.querySelector('[data-sort="change"]');
                    if (chgHdr) chgHdr.classList.add('active', 'desc');
                }
                applyFiltersAndRebuild();
            });
        }

        // Sort by latest signal button
        const sortSigBtn = el('mcSortSig');
        if (sortSigBtn) {
            sortSigBtn.addEventListener('click', () => {
                if (mc.sortBy === 'signals') {
                    mc.sortBy = 'change';
                    mc.sortDir = 'desc';
                    sortSigBtn.classList.remove('active');
                } else {
                    mc.sortBy = 'signals';
                    mc.sortDir = 'desc';
                    sortSigBtn.classList.add('active');
                }
                // Clear col header highlights + other sort buttons
                const colHeaders = document.getElementById('mcColHeaders');
                if (colHeaders) colHeaders.querySelectorAll('.mc-col-hdr').forEach(h => h.classList.remove('active', 'asc', 'desc'));
                const sortSR = el('mcSortSR');
                if (sortSR) sortSR.classList.remove('active');
                const sortTL = el('mcSortTL');
                if (sortTL) sortTL.classList.remove('active');
                if (mc.sortBy !== 'signals') {
                    const chgHdr = colHeaders?.querySelector('[data-sort="change"]');
                    if (chgHdr) chgHdr.classList.add('active', 'desc');
                }
                applyFiltersAndRebuild();
            });
        }

        // Mobile sidebar toggle (hamburger)
        const sidebarToggle = el('mcSidebarToggle');
        const sidebarOverlay = el('mcSidebarOverlay');
        const sidebar = el('mcSidebar');
        if (sidebarToggle && sidebar) {
            const openSidebar = () => {
                sidebar.classList.add('mobile-open');
                if (sidebarOverlay) sidebarOverlay.classList.add('active');
            };
            const closeSidebar = () => {
                sidebar.classList.remove('mobile-open');
                if (sidebarOverlay) sidebarOverlay.classList.remove('active');
            };
            sidebarToggle.addEventListener('click', () => {
                sidebar.classList.contains('mobile-open') ? closeSidebar() : openSidebar();
            });
            if (sidebarOverlay) {
                sidebarOverlay.addEventListener('click', closeSidebar);
            }
            // Close sidebar when coin is clicked (mobile)
            sidebar.addEventListener('click', (e) => {
                if (e.target.closest('.mc-coin-item') && window.innerWidth <= 768) {
                    closeSidebar();
                }
            });
        }

        // Search input (debounced 300ms)
        const searchInput = el('mcSearchInput');
        if (searchInput) {
            let _searchTimer = null;
            searchInput.addEventListener('input', (e) => {
                clearTimeout(_searchTimer);
                _searchTimer = setTimeout(() => {
                    mc.searchQuery = e.target.value.trim().toUpperCase();
                    renderSidebar();
                }, 300);
            });
        }

        // Close flag popups on outside click
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.mc-flag-btn') && !e.target.closest('.mc-flag-popup')) {
                document.querySelectorAll('.mc-flag-popup').forEach(p => p.remove());
            }
        });

        // Init modal events
        initModalEvents();

        // Init layout picker (multi-chart)
        initLayoutPicker();

        // Setup IntersectionObserver with debounced batch loading
        mc._queueFlushTimer = null;
        mc.observer = new IntersectionObserver((entries) => {
            let added = 0;
            entries.forEach(entry => {
                const sym = entry.target.dataset.symbol;
                if (!sym) return;
                if (entry.isIntersecting) {
                    // Card scrolled back into view — cancel pending destroy
                    if (mc.charts[sym] && mc.charts[sym]._destroyTimer) {
                        clearTimeout(mc.charts[sym]._destroyTimer);
                        mc.charts[sym]._destroyTimer = null;
                    }
                    // Card scrolled into view — create chart & queue for batch load
                    if (!mc.charts[sym]) {
                        createChartInstance(sym);
                        mc.loadQueue.push(sym);
                        added++;
                    }
                } else {
                    // Card scrolled out — delay destruction (avoids jerk on scroll-back)
                    if (mc.charts[sym] && !mc.charts[sym]._destroyTimer) {
                        mc.charts[sym]._destroyTimer = setTimeout(() => {
                            if (!mc.charts[sym]) return;
                            // Save candle data to cache before destroying chart
                            if (mc.charts[sym].candleData && mc.charts[sym].candleData.length > 0) {
                                mc.dataCache[sym] = {
                                    candles: mc.charts[sym].candleData,
                                    volume: extractVolume(mc.charts[sym].candleData),
                                    tf: mc.globalTF
                                };
                            }
                            if (mc.charts[sym]._infiniteUnsub) {
                                mc.charts[sym]._infiniteUnsub();
                            }
                            mc.charts[sym].chart.remove();
                            delete mc.charts[sym];
                            delete mc.loadedData[sym];
                            // Unsub this symbol's stream (debounced)
                            const stream = `${sym.toLowerCase()}@kline_${mc.globalTF}`;
                            mc.wsStreams.delete(stream);
                            _wsPendingSub.delete(stream);
                            if (!mc._wsUnsubPending) mc._wsUnsubPending = new Set();
                            mc._wsUnsubPending.add(stream);
                            clearTimeout(mc._wsUnsubTimer);
                            mc._wsUnsubTimer = setTimeout(() => {
                                if (mc._wsUnsubPending.size > 0 && mc.ws && mc.ws.readyState === WebSocket.OPEN) {
                                    mc.ws.send(JSON.stringify({ method: 'UNSUBSCRIBE', params: [...mc._wsUnsubPending], id: Date.now() }));
                                }
                                mc._wsUnsubPending.clear();
                            }, 200);
                        }, 5000); // 5s grace period before destroy
                    }
                }
            });
            // Debounce: wait 50ms for all visible cards to register, then flush as one big batch
            if (added > 0) {
                clearTimeout(mc._queueFlushTimer);
                mc._queueFlushTimer = setTimeout(() => processLoadQueue(), 50);
            }
        }, {
            root: null,
            rootMargin: '200px', // preload 200px before visible
            threshold: 0
        });
    }

    if (mc.allPairs.length === 0) {
        await refreshMiniCharts();
    }
}

async function refreshMiniCharts() {
    const status = el('mcStatus');
    if (status) status.textContent = 'Loading...';

    try {
        const res = await fetch('/api/ticker24hr');
        if (!res.ok) throw new Error(`ticker24hr: ${res.status}`);
        const data = await res.json();

        let pairs = data.filter(d => d.symbol.endsWith('USDT') && !d.symbol.includes('_'));

        pairs.forEach(p => {
            const h = parseFloat(p.highPrice);
            const l = parseFloat(p.lowPrice);
            p.proxyNatr = l > 0 ? ((h - l) / l * 100) : 0;
            p.quoteVol = parseFloat(p.quoteVolume);
            p.tradesCount = parseInt(p.count);
            p.priceChange = parseFloat(p.priceChangePercent);
            p.lastPrice = parseFloat(p.lastPrice);
        });

        // Filter out frozen/halted/delisted pairs
        // Frozen pairs have closeTime far in the past (trading stopped)
        const now = Date.now();
        pairs = pairs.filter(p => {
            const closeTime = parseInt(p.closeTime);
            const age = now - closeTime;
            // If last trade was >1 hour ago, pair is frozen
            if (age > 3600000) return false;
            // Also filter flat pairs where high == low
            if (parseFloat(p.highPrice) === parseFloat(p.lowPrice)) return false;
            return true;
        });

        mc.allPairs = pairs;
        applyFiltersAndRebuild();

        if (status) {
            status.textContent = `${mc.filteredPairs.length}/${pairs.length}`;
        }

        // Fetch real NATR from server (background)
        fetchServerNATR(mc.globalTF);

        // Prefetch top-3 klines (BTC/ETH/SOL) — warm server cache for instant modal open
        prefetchTopKlines(mc.globalTF);
    } catch (e) {
        console.error('Mini-Charts fetch error:', e);
        if (status) status.textContent = 'Error';
    }
}

async function fetchServerNATR(tf) {
    try {
        const res = await fetch(`/api/natr?interval=${tf}`);
        if (!res.ok) return;
        const natrMap = await res.json();
        if (!natrMap || typeof natrMap !== 'object') return;

        // Update all pairs with real NATR
        mc.allPairs.forEach(p => {
            if (natrMap[p.symbol] !== undefined) {
                p.proxyNatr = natrMap[p.symbol];
            }
        });

        // Update displayed values on visible chart cards
        Object.keys(natrMap).forEach(sym => {
            const card = document.getElementById(`mc-card-${sym}`);
            if (card) {
                const span = card.querySelector('.mc-natr');
                if (span) span.textContent = natrMap[sym].toFixed(1) + '%';
            }
        });

        // Update sidebar NATR values
        const sidebarItems = document.querySelectorAll('.mc-coin-item');
        sidebarItems.forEach(item => {
            const sym = item.dataset.symbol;
            if (sym && natrMap[sym] !== undefined) {
                const natrSpan = item.querySelector('.mc-coin-natr');
                if (natrSpan) natrSpan.textContent = natrMap[sym].toFixed(1);
            }
        });

        // Re-sort if sorting by NATR
        if (mc.sortBy === 'natr') {
            sortPairs();
            renderSidebar();
            // Reorder cards in DOM without destroying charts
            const grid = el('chartsGrid');
            if (grid) {
                mc.filteredPairs.forEach(p => {
                    const card = document.getElementById(`mc-card-${p.symbol}`);
                    if (card) grid.appendChild(card);
                });
            }
        }
    } catch(e) {
        console.error('NATR fetch error:', e);
    }
}

const PREFETCH_SYMS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'];
function prefetchTopKlines(tf) {
    // Fire-and-forget: warm server klines cache for top coins so modal opens instantly
    PREFETCH_SYMS.forEach(sym => {
        fetch(`/api/klines?symbol=${sym}&interval=${tf}&limit=1000`)
            .then(r => r.ok && console.log(`[prefetch] ${sym} ${tf} warmed`))
            .catch(() => {}); // silent — prefetch is best-effort
    });
}

function applyFiltersAndRebuild() {
    mc.filteredPairs = mc.allPairs.filter(p => {
        if (mc.filters.minVol > 0 && p.quoteVol < mc.filters.minVol * 1e6) return false;
        if (mc.filters.minNatr > 0 && p.proxyNatr < mc.filters.minNatr) return false;
        if (mc.filters.minTrades > 0 && p.tradesCount < mc.filters.minTrades) return false;
        return true;
    });
    rebuildGrid();
    const status = el('mcStatus');
    if (status) status.textContent = `${mc.filteredPairs.length}/${mc.allPairs.length}`;
}

function rebuildGrid() {
    // Unsubscribe all WS streams
    wsUnsubscribeAll();

    // Destroy all existing charts (cancel pending destroy timers first)
    Object.keys(mc.charts).forEach(sym => {
        if (mc.charts[sym]._destroyTimer) clearTimeout(mc.charts[sym]._destroyTimer);
        mc.charts[sym].chart.remove();
        delete mc.charts[sym];
    });
    mc.loadedData = {};
    mc.loadQueue = [];

    sortPairs();
    renderSidebar();

    const grid = el('chartsGrid');
    if (!grid) return;

    // Disconnect old observations
    mc.observer.disconnect();

    // Render ALL cards (lightweight — just header + empty body)
    grid.innerHTML = mc.filteredPairs.map(p => {
        const sym = p.symbol;
        const ticker = sym.replace('USDT', '');
        const chg = p.priceChange;
        const chgClass = chg >= 0 ? 'mc-metric-green' : 'mc-metric-red';
        const chgSign = chg >= 0 ? '+' : '';
        const vol = p.quoteVol >= 1e9 ? (p.quoteVol / 1e9).toFixed(1) + 'B' : (p.quoteVol / 1e6).toFixed(0) + 'M';
        const natr = p.proxyNatr ? p.proxyNatr.toFixed(1) : '—';
        const trades = p.tradesCount >= 1e6 ? (p.tradesCount / 1e6).toFixed(1) + 'M' : p.tradesCount >= 1e3 ? (p.tradesCount / 1e3).toFixed(0) + 'K' : p.tradesCount;

        return `<div class="mc-chart-card" data-symbol="${sym}" id="mc-card-${sym}">
            <div class="mc-chart-header">
                <span class="mc-chart-symbol">${ticker}</span>
                <button class="mc-copy-btn mc-copy-card" data-ticker="${sym.toLowerCase()}" title="Copy ${sym.toLowerCase()}"><svg width="10" height="10" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
                <div class="mc-chart-metrics">
                    <span class="${chgClass}">${chgSign}${chg.toFixed(2)}%</span>
                    <span class="mc-metric-muted" title="24h Volume"><svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:1px"><rect x="1" y="5" width="2" height="5" fill="currentColor" opacity="0.5"/><rect x="4" y="2" width="2" height="8" fill="currentColor" opacity="0.7"/><rect x="7" y="0" width="2" height="10" fill="currentColor"/></svg>${vol}</span>
                    <span class="mc-metric-muted" title="NATR Volatility"><svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:1px"><path d="M0 7L3 3L5 6L7 1L10 5" stroke="currentColor" stroke-width="1.3" fill="none"/></svg><span class="mc-natr">—</span></span>
                    <span class="mc-metric-muted" title="24h Trades"><svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:1px"><path d="M1 3h3M6 3h3M1 7h3M6 7h3" stroke="currentColor" stroke-width="1.2"/></svg>${trades}</span>
                </div>
            </div>
            <div class="mc-chart-body" id="mc-body-${sym}"></div>
        </div>`;
    }).join('');

    // Copy ticker on mini-chart cards
    grid.querySelectorAll('.mc-copy-card').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            navigator.clipboard.writeText(btn.dataset.ticker).then(() => {
                btn.classList.add('mc-copy-ok');
                setTimeout(() => btn.classList.remove('mc-copy-ok'), 800);
            });
        });
    });

    // Observe all cards + click to open modal
    grid.querySelectorAll('.mc-chart-card').forEach(card => {
        mc.observer.observe(card);
        card.querySelector('.mc-chart-header').addEventListener('click', () => {
            handleSidebarCoinClick(card.dataset.symbol);
        });
    });
}

/** Signal data per symbol: count + latest timestamp (for sorting by recency) */
function _getSignalData() {
    if (typeof sigState === 'undefined' || !sigState.signals) return {};
    const data = {};
    for (const s of sigState.signals) {
        if (!data[s.symbol]) {
            data[s.symbol] = { count: 0, latest: 0 };
        }
        data[s.symbol].count++;
        const ts = new Date(s.created_at.includes('T') ? s.created_at : s.created_at.replace(' ', 'T') + 'Z').getTime() || 0;
        if (ts > data[s.symbol].latest) data[s.symbol].latest = ts;
    }
    return data;
}

function sortPairs() {
    const dir = mc.sortDir === 'desc' ? 1 : -1;
    const _sigD = mc.sortBy === 'signals' ? _getSignalData() : null;
    const sorter = (a, b) => {
        if (mc.sortBy === 'symbol') return dir * a.symbol.localeCompare(b.symbol);
        if (mc.sortBy === 'natr') return dir * (b.proxyNatr - a.proxyNatr);
        if (mc.sortBy === 'trades') return dir * (b.tradesCount - a.tradesCount);
        if (mc.sortBy === 'change') return dir * (b.priceChange - a.priceChange);
        if (mc.sortBy === 'sr') {
            const sa = mc.srData[a.symbol]?.pct ?? 999;
            const sb = mc.srData[b.symbol]?.pct ?? 999;
            return dir * (sa - sb); // closest to S/R first when desc
        }
        if (mc.sortBy === 'tl') {
            const ta = mc.tlData[a.symbol]?.pct ?? 999;
            const tb = mc.tlData[b.symbol]?.pct ?? 999;
            return dir * (ta - tb); // closest to trendline first when desc
        }
        if (mc.sortBy === 'signals') {
            return dir * ((_sigD[b.symbol]?.latest || 0) - (_sigD[a.symbol]?.latest || 0));
        }
        return dir * (b.quoteVol - a.quoteVol); // volume default
    };
    mc.allPairs.sort(sorter);
    mc.filteredPairs.sort(sorter);
}

function renderSidebar() {
    const list = el('mcCoinList');
    const countEl = el('mcCoinCount');
    if (!list) return;

    // Filter by search query
    let pairs = mc.filteredPairs;
    if (mc.searchQuery) {
        pairs = pairs.filter(p => p.symbol.replace('USDT', '').includes(mc.searchQuery));
    }

    // Watchlist-only filter
    const sp = _sp()
    if (sp && spGet('watchlistOnly', false)) {
        pairs = pairs.filter(p => sp.wlHas(p.symbol));
    }

    // Sort: watchlist first, then flagged, then by current sort column
    const dir = mc.sortDir === 'desc' ? 1 : -1;
    pairs = [...pairs].sort((a, b) => {
        // Watchlist coins first
        const wa = sp && sp.wlHas(a.symbol) ? 1 : 0;
        const wb = sp && sp.wlHas(b.symbol) ? 1 : 0;
        if (wb !== wa) return wb - wa;
        const fa = mc.flags[a.symbol] ? 1 : 0;
        const fb = mc.flags[b.symbol] ? 1 : 0;
        if (fb !== fa) return fb - fa;
        if (mc.sortBy === 'symbol') return dir * a.symbol.localeCompare(b.symbol);
        if (mc.sortBy === 'natr') return dir * ((b.proxyNatr || 0) - (a.proxyNatr || 0));
        if (mc.sortBy === 'change') return dir * (b.priceChange - a.priceChange);
        if (mc.sortBy === 'sr') {
            const sa = mc.srData[a.symbol]?.pct ?? 999;
            const sb = mc.srData[b.symbol]?.pct ?? 999;
            return dir * (sa - sb);
        }
        if (mc.sortBy === 'signals') {
            const _sd = _getSignalData();
            return dir * ((_sd[b.symbol]?.latest || 0) - (_sd[a.symbol]?.latest || 0));
        }
        return dir * (b.quoteVol - a.quoteVol);
    });

    if (countEl) countEl.textContent = pairs.length;

    // Pre-compute signal data for sidebar display
    const _sidebarSigData = _getSignalData();

    list.innerHTML = pairs.map(p => {
        const sym = p.symbol;
        const ticker = sym.replace('USDT', '');
        const chg = p.priceChange;
        const chgClass = chg >= 0 ? 'mc-metric-green' : 'mc-metric-red';
        const chgSign = chg >= 0 ? '+' : '';
        const vol = p.quoteVol >= 1e9 ? (p.quoteVol / 1e9).toFixed(1) + 'B' : (p.quoteVol / 1e6).toFixed(0) + 'M';
        const natr = p.proxyNatr ? p.proxyNatr.toFixed(1) : '—';
        const flagColor = mc.flags[sym] || '';
        const flagStyle = flagColor ? `background:${flagColor}; border-color:transparent;` : '';
        const flagClass = flagColor ? 'mc-flag-btn flagged' : 'mc-flag-btn';

        const showChg = spGet('colChg', true)
        const showNatr = spGet('colNatr', true)
        const showVol = spGet('colVol', true)
        const isWl = sp && sp.wlHas(sym)

        return `<div class="mc-coin-item${isWl ? ' mc-wl' : ''}" data-symbol="${sym}">
            <button class="${flagClass}" style="${flagStyle}" data-flag="${sym}" title="Set color flag"></button>
            <span class="mc-coin-name">${ticker}</span>
            <button class="mc-wl-btn${isWl ? ' active' : ''}" data-wl="${sym}" title="${isWl ? 'Remove from watchlist' : 'Add to watchlist'}">★</button>
            <button class="mc-copy-btn" data-ticker="${ticker.toLowerCase()}usdt" title="Copy ${ticker.toLowerCase()}usdt">
                <svg width="10" height="10" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
            </button>
            ${showChg ? `<span class="mc-coin-change ${chgClass}">${chgSign}${chg.toFixed(1)}%</span>` : ''}
            ${showNatr ? `<span class="mc-coin-natr">${natr}</span>` : ''}
            ${showVol ? `<span class="mc-coin-vol">${vol}</span>` : ''}
        </div>`;
    }).join('');

    // Flag button click — show color picker
    list.querySelectorAll('.mc-flag-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const sym = btn.dataset.flag;
            document.querySelectorAll('.mc-flag-popup').forEach(p => p.remove());
            const popup = document.createElement('div');
            popup.className = 'mc-flag-popup';
            popup.innerHTML = FLAG_COLORS.map(c =>
                `<div class="mc-flag-color" data-color="${c}" style="background:${c};"></div>`
            ).join('') + `<button class="mc-flag-clear" title="Remove flag">&times;</button>`;
            btn.style.position = 'relative';
            btn.appendChild(popup);
            popup.querySelectorAll('.mc-flag-color').forEach(dot => {
                dot.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    mc.flags[sym] = dot.dataset.color;
                    saveFlags();
                    popup.remove();
                    renderSidebar();
                });
            });
            popup.querySelector('.mc-flag-clear').addEventListener('click', (ev) => {
                ev.stopPropagation();
                delete mc.flags[sym];
                saveFlags();
                popup.remove();
                renderSidebar();
            });
        });
    });

    // Watchlist ★ button
    list.querySelectorAll('.mc-wl-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const sym = btn.dataset.wl;
            if (sp) {
                sp.wlToggle(sym);
                btn.classList.toggle('active');
                const item = btn.closest('.mc-coin-item');
                if (item) item.classList.toggle('mc-wl');
            }
        });
    });

    // Copy ticker button
    list.querySelectorAll('.mc-copy-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const ticker = btn.dataset.ticker;
            navigator.clipboard.writeText(ticker).then(() => {
                btn.classList.add('mc-copy-ok');
                setTimeout(() => btn.classList.remove('mc-copy-ok'), 800);
            });
        });
    });

    // Click handler — open coin modal or assign to multi-chart slot
    list.querySelectorAll('.mc-coin-item').forEach(item => {
        item.addEventListener('click', (e) => {
            if (e.target.closest('.mc-flag-btn') || e.target.closest('.mc-flag-popup') || e.target.closest('.mc-copy-btn') || e.target.closest('.mc-wl-btn')) return;
            handleSidebarCoinClick(item.dataset.symbol);
        });
    });

    // Apply column visibility from settings
    applySidebarColumns()
}


function getPricePrecision(price) {
    if (price >= 1000) return 1;
    if (price >= 100) return 2;
    if (price >= 1) return 3;
    if (price >= 0.01) return 4;
    if (price >= 0.001) return 5;
    return 6;
}

function createChartInstance(sym) {
    const chartEl = el(`mc-body-${sym}`);
    if (!chartEl || mc.charts[sym]) return;

    // Get price for precision
    const pair = mc.allPairs.find(p => p.symbol === sym);
    const price = pair ? pair.lastPrice : 1;
    const prec = getPricePrecision(price);
    const minMove = parseFloat((1 / Math.pow(10, prec)).toFixed(prec));

    const chart = LightweightCharts.createChart(chartEl, {
        autoSize: true,
        ...localChartOptions,
        layout: { background: { type: 'solid', color: 'transparent' }, textColor: '#64748b', fontSize: 9 },
        grid: getGridOpts(),
        crosshair: { mode: 0 },
        rightPriceScale: { borderColor: 'rgba(255,255,255,0.06)', scaleMargins: { top: 0.1, bottom: 0.1 }, minimumWidth: 32, mode: getPriceScaleMode() },
        timeScale: { borderColor: 'rgba(255,255,255,0.06)', timeVisible: true, secondsVisible: false, rightOffset: 50, shiftVisibleRangeOnNewBar: true, lockVisibleTimeRangeOnResize: true, tickMarkFormatter: localTickFormatter },
        handleScroll: { mouseWheel: true, pressedMouseMove: true, vertTouchDrag: true, horzTouchDrag: true },
        handleScale: { mouseWheel: true, pinch: true, axisPressedMouseMove: { price: true, time: true }, axisDoubleClickReset: { price: true, time: true } },
    });

    const series = addMainSeries(chart, prec, minMove);

    // Volume histogram
    const volSeries = chart.addSeries(LightweightCharts.HistogramSeries, {
        priceFormat: { type: 'volume' },
        priceScaleId: 'vol',
        color: 'rgba(100,116,139,0.3)',
    });
    chart.priceScale('vol').applyOptions({
        scaleMargins: { top: getVolScaleTop(), bottom: 0 },
        drawTicks: false,
        borderVisible: false,
    });

    mc.charts[sym] = { chart, series, volSeries, lines: [], oiSeries: null, candleData: null };

    // Drawing tools on mini-chart — switch context on interaction
    // Guard: prevent duplicate listeners on scroll in/out cycles
    if (!chartEl._miniListenersAttached) {
        chartEl.addEventListener('mousedown', () => {
            if (drawCtx.source !== 'mini:' + sym) {
                setDrawCtxMini(sym);
                renderDrawToolbar(chartEl);
            }
        }, true);
        chartEl.addEventListener('touchstart', () => {
            if (drawCtx.source !== 'mini:' + sym) {
                setDrawCtxMini(sym);
                renderDrawToolbar(chartEl);
            }
        }, { capture: true, passive: true });

        // Attach ruler + drawing handlers
        attachRuler(chartEl, chart, series);
        setupDrawingHandlers(chartEl);
        chartEl._miniListenersAttached = true;
    }
}

// TF string → milliseconds lookup for staleness check & countdown
const TF_MS = { '1m': 60000, '3m': 180000, '5m': 300000, '15m': 900000, '30m': 1800000, '1h': 3600000, '2h': 7200000, '4h': 14400000, '6h': 21600000, '8h': 28800000, '12h': 43200000, '1d': 86400000 };

// Render chart from cached data (instant, no network)
function renderFromCache(sym) {
    const cache = mc.dataCache[sym];
    if (!cache || !mc.charts[sym]) return false;
    if (cache.tf !== mc.globalTF) return false; // TF changed, cache stale

    // Check cache freshness: if last candle is older than 2× TF, skip cache → fetch fresh
    // This prevents gaps when returning to mini-charts after tab-away
    if (cache.candles && cache.candles.length > 0) {
        const lastCandleTime = cache.candles[cache.candles.length - 1].time * 1000; // LWC uses seconds
        const tfMs = TF_MS[mc.globalTF] || 900000;
        const age = Date.now() - lastCandleTime;
        if (age > tfMs * 3) {
            // Cache too stale — force server fetch to avoid gap
            delete mc.dataCache[sym];
            return false;
        }
    }

    const c = mc.charts[sym];
    c.candleData = cache.candles;
    c.series.setData(cache.candles);
    c.volSeries?.setData(cache.volume);
    const visibleCount = Math.min(100, cache.candles.length);
    c.chart.timeScale().setVisibleLogicalRange({
        from: cache.candles.length - visibleCount,
        to: cache.candles.length - 1 + 10
    });
    mc.loadedData[sym] = true;
    applyDrawingsToMiniChart(sym);
    applySignalMarkers(sym, c.series, cache.candles);
    wsSubscribe(sym);
    return true;
}

// Infinite scroll: load more history when user scrolls to left edge
// Works for both mini-charts and modal. Uses official TradingView pattern.
const HISTORY_TARGET = 20000;
const HISTORY_BARS_THRESHOLD = 10; // load when fewer than N bars remain on left

function setupInfiniteScroll(chartObj, seriesObj, volSeriesObj, sym, tf, getCandleData, setCandleData, gaplessBehavior) {
    let loading = false;

    const unsub = chartObj.timeScale().subscribeVisibleLogicalRangeChange(logicalRange => {
        if (!logicalRange || loading) return;
        if (logicalRange.from >= HISTORY_BARS_THRESHOLD) return;

        const candleData = getCandleData();
        if (!candleData || candleData.length === 0 || candleData.length >= HISTORY_TARGET) return;

        // Guard: don't trigger if user hasn't zoomed in (fitContent shows all data)
        const visibleBars = logicalRange.to - logicalRange.from;
        if (visibleBars >= candleData.length) return;

        loading = true;
        const oldestTime = candleData[0].time * 1000;

        fetch(`/api/klines?symbol=${sym}&interval=${tf}&limit=1500&endTime=${oldestTime - 1}`)
            .then(r => r.json())
            .then(json => {
                if (!Array.isArray(json) || json.length === 0) return;
                const olderData = parseKlines(json);
                const freshData = getCandleData() || candleData;
                const oldestExisting = freshData[0]?.time || 0;
                const filtered = olderData.filter(c => c.time < oldestExisting);
                if (filtered.length === 0) return;
                const newData = [...filtered, ...freshData];
                setCandleData(newData);

                // Save visible range BEFORE setData
                const rangeBefore = chartObj.timeScale().getVisibleLogicalRange();
                const prependCount = filtered.length;

                // setData rebuilds gapless maps — all old indices shift by prependCount
                seriesObj.setData(newData);
                if (volSeriesObj) volSeriesObj.setData(extractVolume(newData));

                // Restore viewport — defer to next frame so LWC finishes internal layout
                if (rangeBefore) {
                    const newFrom = rangeBefore.from + prependCount;
                    const newTo = rangeBefore.to + prependCount;
                    requestAnimationFrame(() => {
                        try {
                            chartObj.timeScale().setVisibleLogicalRange({ from: newFrom, to: newTo });
                        } catch(_) {}
                    });
                }
                console.log(`[InfiniteScroll] ${sym} ${tf}: +${filtered.length} → ${newData.length}`);
            })
            .catch(() => {})
            .finally(() => { loading = false; });
    });

    return unsub;
}

// Apply parsed candles to a chart (shared logic)
function applyDataToChart(sym, parsed, tf) {
    if (!mc.charts[sym] || parsed.length === 0) return false;
    mc.charts[sym].candleData = parsed;
    mc.charts[sym].series.setData(parsed);
    mc.charts[sym].volSeries?.setData(extractVolume(parsed));
    const visibleCount = Math.min(200, parsed.length);
    mc.charts[sym].chart.timeScale().setVisibleLogicalRange({
        from: parsed.length - visibleCount,
        to: parsed.length - 1 + 10
    });
    mc.loadedData[sym] = true;
    mc.dataCache[sym] = { candles: parsed, volume: extractVolume(parsed), tf };
    const realNatr = calcNATR(parsed);
    if (realNatr > 0) updateCardNATR(sym, realNatr);
    wsSubscribe(sym);
    // Save to IndexedDB in background (fire-and-forget)
    IDB.put(sym, tf, parsed);
    // Defer overlays to next animation frame (single batch render, no jerk)
    requestAnimationFrame(() => {
        if (!mc.charts[sym]) return;
        applyDrawingsToMiniChart(sym);
        applySignalMarkers(sym, mc.charts[sym].series, parsed);
        applyAutoLevels(sym);
        applyAutoTrendlines(sym);
        applyKeltnerChannel(sym);
        applyRegressionChannel(sym);
    });
    // Infinite scroll on mini-charts — load history when user scrolls left
    if (mc.charts[sym] && !mc.charts[sym]._infiniteUnsub) {
        const c = mc.charts[sym];
        mc.charts[sym]._infiniteUnsub = setupInfiniteScroll(
            c.chart, c.series, c.volSeries, sym, tf,
            () => mc.charts[sym]?.candleData,
            (newData) => {
                if (mc.charts[sym]) {
                    mc.charts[sym].candleData = newData;
                    mc.dataCache[sym] = { candles: newData, volume: extractVolume(newData), tf: mc.globalTF };
                    IDB.put(sym, mc.globalTF, newData);
                }
            }
        );
    }
    return true;
}

// Batch load queue — 3-tier cache: memory → IndexedDB → server (SQLite → Binance)
async function processLoadQueue() {
    if (mc.loadingActive) return;
    mc.loadingActive = true;

    while (mc.loadQueue.length > 0) {
        const batch = [];
        while (mc.loadQueue.length > 0 && batch.length < 20) {
            const sym = mc.loadQueue.shift();
            if (!mc.charts[sym]) continue;
            if (mc.loadedData[sym]) continue;
            batch.push(sym);
        }
        if (batch.length === 0) continue;

        // Tier 1: Memory cache (instant, ~0ms)
        const tier1Done = [];
        const tier2Check = [];
        for (const sym of batch) {
            if (mc.dataCache[sym] && mc.dataCache[sym].tf === mc.globalTF) {
                if (renderFromCache(sym)) {
                    tier1Done.push(sym);
                    continue;
                }
            }
            tier2Check.push(sym);
        }

        // Tier 2: Skip IDB — server SQLite cache is fast enough, IDB delta merge caused gaps
        const needFetch = [...tier2Check];

        // Apply densities for all cached charts
        const allCached = [...tier1Done];
        if (allCached.length > 0) {
            applyDensityToBatch(allCached);
            applyOIToBatch(allCached);
        }

        // Tier 3: Server (SQLite cache → Binance fallback)
        if (needFetch.length > 0) {
            try {
                const res = await fetch('/api/klines-batch', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ symbols: needFetch, interval: mc.globalTF, limit: 1500 })
                });
                const allData = await res.json();

                const loadedSyms = [];
                for (const sym of needFetch) {
                    if (!mc.charts[sym]) continue;
                    if (allData[sym] && Array.isArray(allData[sym])) {
                        const parsed = parseKlines(allData[sym]);
                        if (applyDataToChart(sym, parsed, mc.globalTF)) {
                            loadedSyms.push(sym);
                        }
                    }
                }
                if (loadedSyms.length > 0) {
                    applyDensityToBatch(loadedSyms);
                    applyOIToBatch(loadedSyms);
                }
            } catch(e) {
                for (const sym of needFetch) {
                    if (!mc.charts[sym] || mc.loadedData[sym]) continue;
                    await loadChartData(sym, mc.globalTF);
                    await new Promise(r => setTimeout(r, 80));
                }
            }
        }
    }

    mc.loadingActive = false;
}

// NO manual TZ offset — timestamps are pure UTC seconds.
// All time formatting done via Date() which uses browser's local timezone.
const TZ_OFFSET_SEC = 0;

// Local time formatters for LightweightCharts
const localTimeFormatter = (utcSec) => {
    const d = new Date(utcSec * 1000);
    return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
};
const localTickFormatter = (utcSec, tickMarkType, locale) => {
    const d = new Date(utcSec * 1000);
    // tickMarkType: 0=Year, 1=Month, 2=DayOfMonth, 3=Time, 4=TimeWithSeconds
    if (tickMarkType <= 1) return d.toLocaleDateString([], { month: 'short', year: '2-digit' });
    if (tickMarkType === 2) return d.toLocaleDateString([], { day: 'numeric', month: 'short' });
    return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
};
const localChartOptions = {
    localization: { timeFormatter: localTimeFormatter },
};

function parseKlines(json) {
    return json.map(k => ({
        time: k[0] / 1000,
        open: parseFloat(k[1]),
        high: parseFloat(k[2]),
        low: parseFloat(k[3]),
        close: parseFloat(k[4]),
        volume: parseFloat(k[5]),
    }));
}

function extractVolume(data) {
    return data.map(d => ({
        time: d.time,
        value: d.volume,
        color: d.close >= d.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'
    }));
}

// ======================== SIGNAL MARKERS ON CHARTS ========================

const _sigTypeLabel = { oi_cvd: '🔮 OI+CVD', volume_spike: '📊 Vol', big_mover: '🚀 Mover', liq_sweep: '🎯 Sweep', oi_divergence: '🔀 OI Div', oi_funding_squeeze: '⚡ Squeeze', channel: '📐 Channel' };

/**
 * Place signal markers on a chart series (works for both mini-charts and modal).
 * Reads from sigState.signals (populated by signals.js).
 * @param {string} sym — symbol e.g. "BTCUSDT"
 * @param {Object} series — LightweightCharts series instance
 * @param {Array}  candles — [{time, open, high, low, close}, ...] ASC
 * @param {Object} [modalRef] — if provided, stores markers ref on modal obj; else on mc.charts[sym]
 */
function applySignalMarkers(sym, series, candles, modalRef, force) {
    // Check setting (skip check when forced, e.g. from Signals tab modal)
    if (!force) {
        const sp = typeof settingsPanel !== 'undefined' ? settingsPanel : null;
        if (sp && !sp.get('showSignalsOnCharts')) return;
    }
    if (!series || !candles || candles.length === 0) return;

    // Get signals for this symbol, filtered by selected types
    const hasSigState = typeof sigState !== 'undefined' && sigState.signals;
    const activeTypes = (hasSigState && sigState.typeFilter instanceof Set && sigState.typeFilter.size > 0)
        ? sigState.typeFilter : null;
    const signals = hasSigState
        ? sigState.signals.filter(s => s.symbol === sym && (!activeTypes || activeTypes.has(s.type)))
        : [];

    // Also consume pending signal marker (from push notification click)
    const pending = window._pendingSignalMarker;
    if (pending && modalRef) {
        // Inject pending marker as a synthetic signal so it always shows
        signals.push({
            symbol: sym,
            created_at: new Date(pending.time * 1000).toISOString(),
            price: pending.price,
            direction: pending.direction,
            type: pending.type,
            description: pending.description,
        });
        window._pendingSignalMarker = null;
        console.log(`[Signal] Consumed pending marker for ${sym}: ${pending.type} ${pending.direction} @ ${pending.price}`);
    }

    if (signals.length === 0) return;

    const firstTime = candles[0].time;
    const lastTime = candles[candles.length - 1].time;

    const markers = [];
    for (const sig of signals) {
        const sigTime = Math.floor(new Date(ensureUTC(sig.created_at)).getTime() / 1000);
        if (sigTime < firstTime || sigTime > lastTime) continue;

        // Find nearest candle
        let best = candles[candles.length - 1];
        let bestDiff = Infinity;
        for (const c of candles) {
            const diff = Math.abs(c.time - sigTime);
            if (diff < bestDiff) { bestDiff = diff; best = c; }
        }

        const isLong = sig.direction === 'LONG';
        // Build marker text — show extra detail for channel signals
        let markerText = `${_sigTypeLabel[sig.type] || sig.type} ${sig.direction}`;
        if (sig.type === 'channel' && sig.metadata) {
            const m = sig.metadata;
            const sub = (m.subType || '').replace('channel_', '');
            const tf = m.interval || '';
            const stars = m.confluence > 1 ? '★'.repeat(m.confluence) : '';
            markerText = `📐 ${sub}${tf ? ' ' + tf : ''}${stars ? ' ' + stars : ''}`;
        }
        markers.push({
            time: best.time,
            position: isLong ? 'belowBar' : 'aboveBar',
            color: isLong ? '#22c55e' : '#ef4444',
            shape: isLong ? 'arrowUp' : 'arrowDown',
            text: markerText,
        });
    }

    if (markers.length === 0) return;

    // Sort markers by time (LWC requires this)
    markers.sort((a, b) => a.time - b.time);

    // Dedupe same time+direction (keep first)
    const seen = new Set();
    const unique = markers.filter(m => {
        const key = `${m.time}:${m.position}`;
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    });

    try {
        const target = modalRef || mc.charts[sym];
        if (!target) return;
        if (target._sigMarkers) target._sigMarkers.setMarkers([]);
        target._sigMarkers = LightweightCharts.createSeriesMarkers(series, unique);
    } catch (e) {
        // Silently fail — chart may have been disposed
    }
}

/** Refresh signal markers on all visible mini-charts + open modal (called when type filter changes) */
function refreshSignalMarkers() {
    // Mini-charts
    for (const [sym, c] of Object.entries(mc.charts)) {
        if (!c || !c.series || !c.candleData) continue;
        if (c._sigMarkers) { try { c._sigMarkers.setMarkers([]) } catch(_){} c._sigMarkers = null; }
        applySignalMarkers(sym, c.series, c.candleData);
    }
    // Open modal
    if (typeof modal !== 'undefined' && modal.chart && modal.series && modal.candleData && modal.currentSym) {
        if (modal._sigMarkers) { try { modal._sigMarkers.setMarkers([]) } catch(_){} modal._sigMarkers = null; }
        applySignalMarkers(modal.currentSym, modal.series, modal.candleData, modal);
    }
}

// Reuse ensureUTC from signals.js (or define fallback)
if (typeof ensureUTC === 'undefined') {
    var ensureUTC = function(iso) {
        if (!iso) return iso;
        let s = iso.includes('T') ? iso : iso.replace(' ', 'T');
        if (!s.endsWith('Z') && !s.includes('+')) s += 'Z';
        return s;
    };
}

// Apply saved drawings (hlines, fibs) to mini-chart
function applyDrawingsToMiniChart(sym) {
    const c = mc.charts[sym];
    if (!c || !c.series) return;

    // Clear old drawing objects
    if (c.drawObjs) {
        c.drawObjs.forEach(obj => {
            if (obj.priceLine) try { c.series.removePriceLine(obj.priceLine); } catch(e) {}
            if (obj.lineSeries) try { c.chart.removeSeries(obj.lineSeries); } catch(e) {}
        });
    }
    c.drawObjs = [];

    const saved = drawStore.load(sym);
    if (!saved || saved.length === 0) return;

    saved.forEach(s => {
        if (s.type === 'hline' && s.data) {
            const pl = c.series.createPriceLine({
                price: s.data.price,
                color: s.color || '#5b9cf6',
                lineWidth: 1,
                lineStyle: 2, // dashed on mini
                axisLabelVisible: false,
                title: '',
            });
            c.drawObjs.push({ priceLine: pl });
        } else if (s.type === 'fib' && s.data) {
            const rawLevels = s.data.levels || FIB_DEFAULTS_OBJ;
            const diff = s.data.p2 - s.data.p1;
            rawLevels.forEach((item, i) => {
                const lvl = typeof item === 'number' ? item : item.level;
                const clr = (typeof item === 'object' && item.color) ? item.color : (s.color || '#5b9cf6');
                const price = s.data.p1 + diff * lvl;
                const pl = c.series.createPriceLine({
                    price,
                    color: clr,
                    lineWidth: 1,
                    lineStyle: 0,
                    axisLabelVisible: false,
                    title: '',
                });
                c.drawObjs.push({ priceLine: pl });
            });
        } else if ((s.type === 'ray' || s.type === 'trendline') && s.data) {
            const { t1, p1, t2, p2 } = s.data;
            const points = [];
            if (s.type === 'ray') {
                const dt = t2 - t1;
                const dp = p2 - p1;
                const steps = 200;
                const seen = new Set();
                for (let i = 0; i <= steps; i++) {
                    const ratio = i / 5;
                    const t = Math.round(t1 + dt * ratio);
                    if (seen.has(t)) continue;
                    seen.add(t);
                    points.push({ time: t, value: p1 + dp * ratio });
                }
                points.sort((a, b) => a.time - b.time);
            } else {
                points.push({ time: t1, value: p1 });
                points.push({ time: t2, value: p2 });
            }
            const ls = c.chart.addSeries(LightweightCharts.LineSeries, {
                color: s.color || '#5b9cf6',
                lineWidth: 1,
                crosshairMarkerVisible: false,
                lastValueVisible: false,
                priceLineVisible: false,
                pointMarkersVisible: false,
            });
            ls.setData(points);
            c.drawObjs.push({ lineSeries: ls });
        } else if (s.type === 'rect' && s.data) {
            // Rectangle on mini-chart: top + bottom border lines
            const { t1, p1, t2, p2 } = s.data;
            const clr = s.color || '#5b9cf6';
            const topLs = c.chart.addSeries(LightweightCharts.LineSeries, {
                color: clr, lineWidth: 1, crosshairMarkerVisible: false,
                lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
            });
            topLs.setData([{ time: t1, value: Math.max(p1, p2) }, { time: t2, value: Math.max(p1, p2) }]);
            c.drawObjs.push({ lineSeries: topLs });

            const botLs = c.chart.addSeries(LightweightCharts.LineSeries, {
                color: clr, lineWidth: 1, crosshairMarkerVisible: false,
                lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
            });
            botLs.setData([{ time: t1, value: Math.min(p1, p2) }, { time: t2, value: Math.min(p1, p2) }]);
            c.drawObjs.push({ lineSeries: botLs });
        }
    });
}

// Calculate real ATR(14) / close * 100 = NATR from candle data
function calcNATR(candles, period = 14) {
    if (!candles || candles.length < period + 1) return 0;
    const trs = [];
    for (let i = 1; i < candles.length; i++) {
        const h = candles[i].high;
        const l = candles[i].low;
        const pc = candles[i - 1].close;
        trs.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
    }
    // SMA of last `period` TRs
    const recent = trs.slice(-period);
    const atr = recent.reduce((s, v) => s + v, 0) / recent.length;
    const lastClose = candles[candles.length - 1].close;
    return lastClose > 0 ? (atr / lastClose) * 100 : 0;
}

// Update NATR display on mini-chart card after real calc
function updateCardNATR(sym, natr) {
    const card = document.getElementById(`mc-card-${sym}`);
    if (!card) return;
    const natrSpan = card.querySelector('.mc-natr');
    if (natrSpan) natrSpan.textContent = natr.toFixed(1) + '%';
    // Update pair data for sorting
    const pair = mc.allPairs.find(p => p.symbol === sym);
    if (pair) pair.proxyNatr = natr;
}

async function loadChartData(sym, tf) {
    if (!mc.charts[sym]) return;

    // Try cache first
    if (renderFromCache(sym)) {
        applyDensityToMiniChart(sym);
        return;
    }

    try {
        // Load 1500 candles (max Binance allows per request)
        const res1 = await fetch(`/api/klines?symbol=${sym}&interval=${tf}&limit=1500`);
        if (!res1.ok) throw new Error(`klines: ${res1.status}`);
        const json1 = await res1.json();
        if (!Array.isArray(json1) || !mc.charts[sym]) return;

        const data1 = parseKlines(json1);
        mc.charts[sym].candleData = data1;
        mc.charts[sym].series.setData(data1);
        mc.charts[sym].volSeries.setData(extractVolume(data1));
        mc.loadedData[sym] = true;

        const visibleCount = Math.min(100, data1.length);
        mc.charts[sym].chart.timeScale().setVisibleLogicalRange({
            from: data1.length - visibleCount,
            to: data1.length - 1 + 10
        });

        // Save to cache
        mc.dataCache[sym] = { candles: data1, volume: extractVolume(data1), tf };

        const realNatr = calcNATR(data1);
        if (realNatr > 0) updateCardNATR(sym, realNatr);

        applyDrawingsToMiniChart(sym);
        applyDensityToMiniChart(sym);
        wsSubscribe(sym);

        // Auto S/R levels applied via applyDataToChart → applyAutoLevels

    } catch (e) {
        console.error(`Chart load error ${sym}:`, e);
    }
}

// ==========================================
// Auto S/R Levels — Fractal Pivots + ATR Clustering
// ==========================================

function computeAutoLevels(candles) {
    if (!candles || candles.length < 30) return [];

    // 1. Calculate ATR(14) for adaptive clustering
    const trs = [];
    for (let i = 1; i < candles.length; i++) {
        const h = candles[i].high, l = candles[i].low, pc = candles[i - 1].close;
        trs.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
    }
    const atrSlice = trs.slice(-14);
    const atr = atrSlice.reduce((s, v) => s + v, 0) / atrSlice.length;
    const mergeDist = atr * 1.5; // ATR-based merge threshold (wider to merge nearby levels)
    if (mergeDist <= 0) return [];

    const currentPrice = candles[candles.length - 1].close;
    const maxDist = atr * 40; // only levels within 40×ATR of current price

    // 2. Detect fractal pivots (both swing highs and lows)
    const WINDOW = Math.min(10, Math.max(3, Math.floor(candles.length / 30)));
    const pivots = [];
    const totalCandles = candles.length;

    for (let i = WINDOW; i < totalCandles - WINDOW; i++) {
        let isHigh = true, isLow = true;
        for (let j = i - WINDOW; j <= i + WINDOW; j++) {
            if (i === j) continue;
            if (candles[j].high >= candles[i].high) isHigh = false;
            if (candles[j].low <= candles[i].low) isLow = false;
            if (!isHigh && !isLow) break;
        }
        if (isHigh) pivots.push({ price: candles[i].high, idx: i, vol: candles[i].volume || 0 });
        if (isLow) pivots.push({ price: candles[i].low, idx: i, vol: candles[i].volume || 0 });
    }

    if (pivots.length < 2) return [];

    // 3. Cluster pivots using DBSCAN (density-based, no chain-merge problem)
    //    epsilon = mergeDist (ATR×1.5), minPts = 2
    const MIN_PTS = 2;
    const visited = new Set();
    const clustered = new Set();
    const clusters = [];

    for (let i = 0; i < pivots.length; i++) {
        if (visited.has(i)) continue;
        visited.add(i);

        // Find all neighbors within epsilon (mergeDist)
        const neighbors = [];
        for (let j = 0; j < pivots.length; j++) {
            if (Math.abs(pivots[i].price - pivots[j].price) <= mergeDist) {
                neighbors.push(j);
            }
        }

        if (neighbors.length < MIN_PTS) continue; // noise point

        // Expand cluster
        const cluster = [];
        const queue = [...neighbors];
        for (const ni of queue) {
            if (!clustered.has(ni)) {
                clustered.add(ni);
                cluster.push(pivots[ni]);
            }
            if (visited.has(ni)) continue;
            visited.add(ni);
            // Find neighbors of neighbor
            const nn = [];
            for (let j = 0; j < pivots.length; j++) {
                if (Math.abs(pivots[ni].price - pivots[j].price) <= mergeDist) {
                    nn.push(j);
                }
            }
            if (nn.length >= MIN_PTS) {
                for (const nj of nn) {
                    if (!queue.includes(nj)) queue.push(nj);
                }
            }
        }
        if (cluster.length >= MIN_PTS) clusters.push(cluster);
    }

    // 4. Score each cluster
    const levels = [];
    for (const cluster of clusters) {
        // Use median price (more robust than average for skewed clusters)
        const prices = cluster.map(p => p.price).sort((a, b) => a - b);
        const medianPrice = prices[Math.floor(prices.length / 2)];

        // Skip levels too far from current price
        if (Math.abs(medianPrice - currentPrice) > maxDist) continue;

        const touches = cluster.length;

        // Recency score: exponential decay, recent pivots matter more
        const recencyScore = cluster.reduce((s, p) => {
            const age = totalCandles - p.idx;
            return s + Math.exp(-age / (totalCandles * 0.4));
        }, 0) / touches;

        // Volume score: log-scaled average volume at touch points
        const avgVol = cluster.reduce((s, p) => s + p.vol, 0) / touches;
        const volScore = avgVol > 0 ? Math.log10(avgVol + 1) : 0;

        // Type based on position relative to current price (not pivot type)
        const type = medianPrice < currentPrice ? 'support' : 'resistance';

        // Composite score (0-100)
        const score = Math.min(100, Math.round(
            Math.min(touches, 8) * 10 + // touch count capped at 8 (2→20, 5→50, 8→80)
            recencyScore * 15 +          // recency (0-15)
            volScore * 2                 // volume bonus
        ));

        levels.push({ price: medianPrice, type, touches: Math.min(touches, 99), score });
    }

    // 5. Add Volume Profile levels (POC / VAH / VAL)
    const vpLevels = computeVolumeProfile(candles, currentPrice);
    for (const vp of vpLevels) {
        // Avoid duplicates: skip VP level if too close to an existing fractal level
        const tooClose = levels.some(l => Math.abs(l.price - vp.price) <= mergeDist);
        if (!tooClose) levels.push(vp);
    }

    // 6. Break detection — penalize levels that price has recently broken through
    //    Scan last 20% of candles for decisive closes past the level.
    //    A "break" = close clearly past level (by 0.3×ATR). If price later returns, partial recovery.
    const breakScanStart = Math.floor(totalCandles * 0.8);
    const breakThresh = atr * 0.3; // close must be 0.3×ATR past level to count as break

    for (const level of levels) {
        if (level.vpType) continue; // don't penalize VP levels
        let broken = false;
        let returnedAfterBreak = false;

        for (let i = breakScanStart; i < totalCandles; i++) {
            const c = candles[i].close;
            if (level.type === 'support') {
                // Support broken = close below level
                if (c < level.price - breakThresh) broken = true;
                // Returned = close back above level after break
                else if (broken && c > level.price + breakThresh) returnedAfterBreak = true;
            } else {
                // Resistance broken = close above level
                if (c > level.price + breakThresh) broken = true;
                else if (broken && c < level.price - breakThresh) returnedAfterBreak = true;
            }
        }

        if (broken && !returnedAfterBreak) {
            level.score = Math.max(10, Math.round(level.score * 0.5)); // halve score
            level.broken = true;
        } else if (broken && returnedAfterBreak) {
            // Retested after break — slight penalty only (level proved it still matters)
            level.score = Math.max(20, Math.round(level.score * 0.8));
            level.retested = true;
        }
    }

    // 7. Sort by proximity to current price, then score
    const supports = levels.filter(l => l.type === 'support')
        .sort((a, b) => b.price - a.price) // nearest support first
        .slice(0, 3);
    const resists = levels.filter(l => l.type === 'resistance')
        .sort((a, b) => a.price - b.price) // nearest resistance first
        .slice(0, 3);

    return [...supports, ...resists];
}

// ── Volume Profile (POC / VAH / VAL) ─────────────────────────────
// Divides visible price range into bins, distributes volume,
// returns POC (max volume), VAH/VAL (70% value area) as S/R levels.
function computeVolumeProfile(candles, currentPrice) {
    if (!candles || candles.length < 30) return [];

    const NUM_BINS = 100;
    const VALUE_AREA_PCT = 0.70;

    // Price range
    let minP = Infinity, maxP = -Infinity;
    for (const c of candles) {
        if (c.low < minP) minP = c.low;
        if (c.high > maxP) maxP = c.high;
    }
    const range = maxP - minP;
    if (range <= 0) return [];

    const binSize = range / NUM_BINS;
    const bins = new Float64Array(NUM_BINS); // volume per bin

    // Distribute each candle's volume across bins it touches
    for (const c of candles) {
        const vol = c.volume || 0;
        if (vol <= 0) continue;
        const lo = Math.max(0, Math.floor((c.low - minP) / binSize));
        const hi = Math.min(NUM_BINS - 1, Math.floor((c.high - minP) / binSize));
        const span = hi - lo + 1;
        const volPerBin = vol / span;
        for (let b = lo; b <= hi; b++) bins[b] += volPerBin;
    }

    // POC = bin with max volume
    let pocBin = 0, maxVol = 0;
    for (let i = 0; i < NUM_BINS; i++) {
        if (bins[i] > maxVol) { maxVol = bins[i]; pocBin = i; }
    }

    // Value Area: expand from POC until 70% of total volume captured
    let totalVol = 0;
    for (let i = 0; i < NUM_BINS; i++) totalVol += bins[i];
    const targetVol = totalVol * VALUE_AREA_PCT;

    let vaLow = pocBin, vaHigh = pocBin;
    let areaVol = bins[pocBin];

    while (areaVol < targetVol && (vaLow > 0 || vaHigh < NUM_BINS - 1)) {
        const addLow = vaLow > 0 ? bins[vaLow - 1] : 0;
        const addHigh = vaHigh < NUM_BINS - 1 ? bins[vaHigh + 1] : 0;
        if (addLow >= addHigh && vaLow > 0) {
            vaLow--;
            areaVol += bins[vaLow];
        } else if (vaHigh < NUM_BINS - 1) {
            vaHigh++;
            areaVol += bins[vaHigh];
        } else {
            vaLow--;
            areaVol += bins[vaLow];
        }
    }

    const pocPrice = minP + (pocBin + 0.5) * binSize;
    const vahPrice = minP + (vaHigh + 1) * binSize;   // upper edge
    const valPrice = minP + vaLow * binSize;            // lower edge

    const results = [];

    // POC — always significant (score 90, high visibility)
    results.push({
        price: pocPrice,
        type: pocPrice < currentPrice ? 'support' : 'resistance',
        touches: 0,   // VP levels don't have "touches"
        score: 90,
        vpType: 'POC'
    });

    // VAH — value area high (resistance zone edge)
    if (Math.abs(vahPrice - pocPrice) > binSize * 2) {
        results.push({
            price: vahPrice,
            type: vahPrice < currentPrice ? 'support' : 'resistance',
            touches: 0,
            score: 70,
            vpType: 'VAH'
        });
    }

    // VAL — value area low (support zone edge)
    if (Math.abs(valPrice - pocPrice) > binSize * 2) {
        results.push({
            price: valPrice,
            type: valPrice < currentPrice ? 'support' : 'resistance',
            touches: 0,
            score: 70,
            vpType: 'VAL'
        });
    }

    return results;
}

// ── Multi-TF Confluence ───────────────────────────────────────────
// Fetches klines for additional timeframes, computes S/R on each,
// then boosts levels that appear on multiple TFs.
// Returns enriched levels with `confluence` count (1-3 TFs).
const CONFLUENCE_TFS = {
    '1m':  ['5m', '15m'],
    '3m':  ['15m', '1h'],
    '5m':  ['1h', '4h'],
    '15m': ['1h', '4h'],
    '30m': ['4h', '1d'],
    '1h':  ['4h', '1d'],
    '2h':  ['4h', '1d'],
    '4h':  ['1d', '1w'],
    '6h':  ['1d', '1w'],
    '8h':  ['1d', '1w'],
    '12h': ['1d', '1w'],
    '1d':  ['1w', '1M'],
    '1w':  ['1M'],
};

async function computeMultiTFLevels(sym, currentTF, currentCandles) {
    // Start with levels from current TF
    const baseLevels = computeAutoLevels(currentCandles);
    if (baseLevels.length === 0) return baseLevels;

    const extraTFs = CONFLUENCE_TFS[currentTF] || [];
    if (extraTFs.length === 0) return baseLevels;

    const currentPrice = currentCandles[currentCandles.length - 1].close;

    // Fetch klines for additional TFs in parallel
    const extraLevelSets = await Promise.all(extraTFs.map(async tf => {
        try {
            const res = await fetch(`/api/klines?symbol=${sym}&interval=${tf}&limit=500`);
            if (!res.ok) return [];
            const raw = await res.json();
            if (!Array.isArray(raw) || raw.length < 30) return [];
            const candles = parseKlines(raw);
            return computeAutoLevels(candles);
        } catch { return []; }
    }));

    // ATR from current candles for merge distance
    const trs = [];
    for (let i = 1; i < currentCandles.length; i++) {
        const h = currentCandles[i].high, l = currentCandles[i].low, pc = currentCandles[i - 1].close;
        trs.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
    }
    const atr = trs.slice(-14).reduce((s, v) => s + v, 0) / Math.min(14, trs.length);
    const confluenceDist = atr * 2; // levels within 2×ATR are "same level"

    // Enrich base levels with confluence count
    for (const level of baseLevels) {
        let conf = 1; // already on current TF
        for (const extraLevels of extraLevelSets) {
            const match = extraLevels.some(el => Math.abs(el.price - level.price) <= confluenceDist);
            if (match) conf++;
        }
        level.confluence = conf;
        // Boost score for multi-TF levels
        if (conf >= 2) level.score = Math.min(100, level.score + 15);
        if (conf >= 3) level.score = Math.min(100, level.score + 10);
    }

    // Also check if higher TFs have levels not in base set
    for (const extraLevels of extraLevelSets) {
        for (const el of extraLevels) {
            const alreadyClose = baseLevels.some(bl => Math.abs(bl.price - el.price) <= confluenceDist);
            if (!alreadyClose && Math.abs(el.price - currentPrice) <= atr * 40) {
                el.confluence = 1;
                el.score = Math.min(100, el.score + 10); // HTF bonus
                el.htf = true; // mark as higher-TF only level
                baseLevels.push(el);
            }
        }
    }

    // Re-sort and limit: 3 supports + 3 resistances, nearest first
    const supports = baseLevels.filter(l => l.type === 'support')
        .sort((a, b) => b.price - a.price).slice(0, 3);
    const resists = baseLevels.filter(l => l.type === 'resistance')
        .sort((a, b) => a.price - b.price).slice(0, 3);

    return [...supports, ...resists];
}

// ── Auto Trendlines v3 ─────────────────────────────────────────
// ZigZag swing detection + gradient descent slope optimization.
// Key principle: ZERO violations — no candle crosses the line on wrong side.
// Inspired by neurotrader888/TechnicalAnalysisAutomation.
// Returns top 1 support + 1 resistance trendline (closest to price).
// Each: { p1:{time,price}, p2:{time,price}, type, touches, score, slope, broken }

function computeAutoTrendlines(candles) {
    if (!candles || candles.length < 40) return [];

    const totalCandles = candles.length;
    const currentPrice = candles[totalCandles - 1].close;
    const lastTime = candles[totalCandles - 1].time;

    // ATR(14)
    const trs = [];
    for (let i = 1; i < totalCandles; i++) {
        const h = candles[i].high, l = candles[i].low, pc = candles[i - 1].close;
        trs.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
    }
    const atr = trs.slice(-14).reduce((s, v) => s + v, 0) / Math.min(14, trs.length);
    if (atr <= 0) return [];

    const touchTol = atr * 0.3; // tighter than before (0.4 → 0.3)

    // ── 1. ZigZag swing detection (ATR-based threshold) ──
    // Produces 5-15 significant swings instead of 30+ fractal pivots
    const zigzagThreshold = atr * 3; // swing must be ≥ 3×ATR to count
    const swings = [];
    let lastType = candles[0].close > candles[1].close ? 'high' : 'low';
    let lastHigh = candles[0].high, lastLow = candles[0].low;
    let lastHighIdx = 0, lastLowIdx = 0;

    for (let i = 1; i < totalCandles; i++) {
        if (lastType === 'low') {
            if (candles[i].high > lastHigh) { lastHigh = candles[i].high; lastHighIdx = i; }
            if (lastHigh - candles[i].low >= zigzagThreshold) {
                swings.push({ idx: lastHighIdx, price: lastHigh, type: 'high', time: candles[lastHighIdx].time });
                lastType = 'high'; lastLow = candles[i].low; lastLowIdx = i;
            }
        } else {
            if (candles[i].low < lastLow) { lastLow = candles[i].low; lastLowIdx = i; }
            if (candles[i].high - lastLow >= zigzagThreshold) {
                swings.push({ idx: lastLowIdx, price: lastLow, type: 'low', time: candles[lastLowIdx].time });
                lastType = 'low'; lastHigh = candles[i].high; lastHighIdx = i;
            }
        }
    }
    // Add final pending swing
    if (lastType === 'low') swings.push({ idx: lastHighIdx, price: lastHigh, type: 'high', time: candles[lastHighIdx].time });
    else swings.push({ idx: lastLowIdx, price: lastLow, type: 'low', time: candles[lastLowIdx].time });

    const swingHighs = swings.filter(s => s.type === 'high');
    const swingLows = swings.filter(s => s.type === 'low');

    // ── 2. Gradient descent: find optimal trendline through each pivot ──
    // For each pivot, try slope via gradient descent so that:
    //   Support line: ALL candle lows stay ABOVE the line (zero violations)
    //   Resistance line: ALL candle highs stay BELOW the line (zero violations)
    // Then pick the slope that's closest to price (tightest valid line).

    function fitTrendline(pivotIdx, pivotPrice, isSupport) {
        // Initial slope: linear regression slope of closes
        const startIdx = Math.max(0, pivotIdx - Math.floor(totalCandles * 0.3));
        const endIdx = Math.min(totalCandles - 1, pivotIdx + Math.floor(totalCandles * 0.3));
        const scanStart = Math.max(0, pivotIdx - Math.floor(totalCandles * 0.5));
        const scanEnd = totalCandles - 1;

        // Initial slope estimate from regression
        let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, n = 0;
        for (let i = startIdx; i <= endIdx; i++) {
            const x = i - pivotIdx;
            const y = candles[i].close;
            sumX += x; sumY += y; sumXY += x * y; sumXX += x * x; n++;
        }
        let slope = n > 1 ? (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX) : 0;

        // Gradient descent: adjust slope to eliminate violations
        const lr = atr * 0.001; // learning rate
        const maxIter = 50;

        for (let iter = 0; iter < maxIter; iter++) {
            let worstViolation = 0;
            let worstIdx = -1;

            for (let i = scanStart; i <= scanEnd; i++) {
                const linePrice = pivotPrice + slope * (i - pivotIdx);
                const candlePrice = isSupport ? candles[i].low : candles[i].high;
                const violation = isSupport
                    ? linePrice - candlePrice  // support: line above low = bad
                    : candlePrice - linePrice; // resistance: high above line = bad

                if (violation > worstViolation) {
                    worstViolation = violation;
                    worstIdx = i;
                }
            }

            if (worstViolation <= atr * 0.05) break; // close enough to zero

            // Nudge slope to fix the worst violation
            const dx = worstIdx - pivotIdx;
            if (dx === 0) break;
            // Adjust slope so the line moves away from the violation
            slope -= (worstViolation * Math.sign(dx)) / Math.abs(dx) * 0.5;
        }

        // Final verification: zero violations
        let valid = true;
        for (let i = scanStart; i <= scanEnd; i++) {
            const linePrice = pivotPrice + slope * (i - pivotIdx);
            if (isSupport && candles[i].low < linePrice - atr * 0.08) { valid = false; break; }
            if (!isSupport && candles[i].high > linePrice + atr * 0.08) { valid = false; break; }
        }
        if (!valid) return null;

        // Enforce correct slope direction:
        //   Support (lows) must slope UP (rising lows)
        //   Resistance (highs) must slope DOWN (falling highs)
        if (isSupport && slope < 0) return null;   // support must rise
        if (!isSupport && slope > 0) return null;  // resistance must fall

        // Reject near-horizontal (S/R levels territory)
        const totalMove = Math.abs(slope * totalCandles);
        if (totalMove < atr * 0.5) return null;

        // Reject too steep
        if (Math.abs(slope) > atr * 0.5) return null;

        // Count touches (zigzag points near the line)
        let touches = 1; // pivot itself
        const allPivots = isSupport ? swingLows : swingHighs;
        for (const p of allPivots) {
            if (p.idx === pivotIdx) continue;
            const linePrice = pivotPrice + slope * (p.idx - pivotIdx);
            if (Math.abs(p.price - linePrice) <= touchTol) touches++;
        }

        // Also count candle wicks that touch the line
        for (let i = scanStart; i <= scanEnd; i++) {
            const linePrice = pivotPrice + slope * (i - pivotIdx);
            const wickPrice = isSupport ? candles[i].low : candles[i].high;
            const dist = Math.abs(wickPrice - linePrice);
            if (dist <= touchTol && !allPivots.some(p => p.idx === i)) touches++;
        }
        // Cap wick touches to avoid noise
        touches = Math.min(touches, 12);

        // Project to current bar
        const projectedPrice = pivotPrice + slope * (totalCandles - 1 - pivotIdx);
        const distToCurrent = Math.abs(projectedPrice - currentPrice) / currentPrice * 100;

        // Break detection: has price crossed the line after pivot?
        let broken = false, breakIdx = -1;
        for (let i = pivotIdx + 1; i < totalCandles; i++) {
            const linePrice = pivotPrice + slope * (i - pivotIdx);
            if (isSupport && candles[i].close < linePrice - atr * 0.8) { broken = true; breakIdx = i; break; }
            if (!isSupport && candles[i].close > linePrice + atr * 0.8) { broken = true; breakIdx = i; break; }
        }

        // Recency: how recent is the pivot
        const recency = Math.exp(-(totalCandles - pivotIdx) / (totalCandles * 0.4));

        // Score: exponential touch reward + recency + proximity
        const proximityBonus = distToCurrent < 5 ? (5 - distToCurrent) / 5 : 0;
        const brokenPenalty = broken ? 0.3 : 1.0;
        const score = Math.min(100, Math.round(
            (Math.pow(touches, 2.5) * 3 + recency * 25 + proximityBonus * 15) * brokenPenalty
        ));

        return {
            pivotIdx, pivotPrice, slope, touches, score,
            projectedPrice, distToCurrent, broken, breakIdx,
        };
    }

    // ── 3. Run gradient descent on each zigzag pivot ──
    const supportCandidates = [];
    const resistCandidates = [];

    for (const s of swingLows) {
        const result = fitTrendline(s.idx, s.price, true);
        if (result && result.touches >= 2) supportCandidates.push({ ...result, time: s.time });
    }
    for (const s of swingHighs) {
        const result = fitTrendline(s.idx, s.price, false);
        if (result && result.touches >= 2) resistCandidates.push({ ...result, time: s.time });
    }

    supportCandidates.sort((a, b) => b.score - a.score);
    resistCandidates.sort((a, b) => b.score - a.score);

    // ── 4. Dedup & pick best ──
    function dedup(arr) {
        if (arr.length <= 1) return arr;
        const result = [arr[0]];
        for (let i = 1; i < arr.length; i++) {
            const cur = arr[i];
            const isDup = result.some(prev => {
                const projDiff = Math.abs(cur.projectedPrice - prev.projectedPrice);
                const slopeRatio = prev.slope !== 0 ? Math.abs(cur.slope / prev.slope) : 2;
                return projDiff < atr * 2 && slopeRatio > 0.6 && slopeRatio < 1.4;
            });
            if (!isDup) result.push(cur);
        }
        return result;
    }

    const bestSupport = dedup(supportCandidates).slice(0, 1);
    const bestResistance = dedup(resistCandidates).slice(0, 1);

    // ── 5. Format output ──
    const results = [];
    for (const c of [...bestSupport, ...bestResistance]) {
        const isSupport = supportCandidates.includes(c);
        const type = isSupport ? 'support' : 'resistance';

        const p1 = { time: c.time, price: c.pivotPrice, idx: c.pivotIdx };

        let endIdx, endTime, endPrice;
        if (c.broken && c.breakIdx > 0) {
            endIdx = c.breakIdx;
        } else {
            endIdx = totalCandles - 1;
        }
        endTime = candles[endIdx].time;
        endPrice = c.pivotPrice + c.slope * (endIdx - c.pivotIdx);

        // Start line from the very beginning of the chart
        const startIdx = 0;
        const startTime = candles[0].time;
        const startPrice = c.pivotPrice + c.slope * (0 - c.pivotIdx);

        const finalProjected = c.broken ? endPrice : c.projectedPrice;
        const finalDist = Math.abs(finalProjected - currentPrice) / currentPrice * 100;
        if (finalDist > 10) continue;

        results.push({
            p1: { time: startTime, price: startPrice, idx: startIdx },
            p2: { time: endTime, price: endPrice, idx: endIdx },
            type,
            touches: c.touches,
            score: c.score,
            slope: c.slope,
            projectedPrice: finalProjected,
            distToCurrent: finalDist,
            broken: c.broken,
        });
    }

    return results;
}

// Compute nearest auto-trendline distance % for a symbol (for sidebar sorting)
// Stores result on mc.tlData[sym] = { type: 'S'|'R', pct: 0.5, price: 74610, touches }
function computeTLDistance(sym) {
    const chartObj = mc.charts[sym];
    const candles = chartObj?.candleData || mc.dataCache[sym]?.candles;
    if (!candles || candles.length < 40) return;

    const lines = computeAutoTrendlines(candles);
    if (lines.length === 0) { mc.tlData[sym] = null; return; }

    const price = candles[candles.length - 1].close;
    let nearest = null;
    let minPct = Infinity;

    for (const l of lines) {
        const pct = l.distToCurrent;
        if (pct < minPct) {
            minPct = pct;
            nearest = { type: l.type === 'support' ? 'S' : 'R', pct, price: l.projectedPrice, touches: l.touches };
        }
    }
    mc.tlData[sym] = nearest;
}

// Format trendline distance for sidebar display
function formatTL(sym) {
    const tl = mc.tlData[sym];
    if (!tl) return '<span style="color:#555">—</span>';
    const color = tl.type === 'S' ? '#22c55e' : '#ef4444';
    return `<span style="color:${color}">T${tl.type}${tl.pct.toFixed(1)}%</span>`;
}

// Compute nearest S/R distance % for a symbol (for sidebar sorting)
// Stores result on mc.srData[sym] = { type: 'S'|'R', pct: 0.5, price: 74610 }
function computeSRDistance(sym) {
    const chartObj = mc.charts[sym];
    const candles = chartObj?.candleData || mc.dataCache[sym]?.candles;
    if (!candles || candles.length < 30) return;

    const levels = computeAutoLevels(candles);
    if (levels.length === 0) { mc.srData[sym] = null; return; }

    const price = candles[candles.length - 1].close;
    let nearest = null;
    let minPct = Infinity;

    for (const l of levels) {
        const pct = Math.abs(l.price - price) / price * 100;
        if (pct < minPct) {
            minPct = pct;
            nearest = { type: l.type === 'support' ? 'S' : 'R', pct, price: l.price, touches: l.touches };
        }
    }
    mc.srData[sym] = nearest;
}

// Format S/R distance for sidebar display
function formatSR(sym) {
    const sr = mc.srData[sym];
    if (!sr) return '<span style="color:#555">—</span>';
    const color = sr.type === 'S' ? '#22c55e' : '#ef4444';
    return `<span style="color:${color}">${sr.type}${sr.pct.toFixed(1)}%</span>`;
}

// ── Level → PriceLine config helper ──────────────────────────────
// Unified styling for all chart types: mini-charts, modal, multi-chart slots.
// Handles VP levels, fractal S/R, confluence tags, broken/retested states.
function levelToPriceLine(l, showAxisLabel) {
    let color, title, lineStyle, lineWidth;

    if (l.vpType) {
        // Volume Profile: POC = blue bold dotted, VAH/VAL = purple dotted
        color = l.vpType === 'POC' ? 'rgba(59,130,246,0.85)' : 'rgba(168,85,247,0.65)';
        title = l.vpType;
        lineStyle = 2; // dotted
        lineWidth = l.vpType === 'POC' ? 2 : 1;
    } else {
        // Fractal S/R
        const opacity = l.broken ? 0.25 : l.retested ? 0.35 : Math.max(0.4, l.score / 100);
        const baseColor = l.type === 'support' ? [34, 197, 94] : [239, 68, 68];
        color = `rgba(${baseColor.join(',')},${opacity})`;

        const prefix = l.type === 'support' ? 'S' : 'R';
        const confTag = l.confluence >= 2 ? ` ★${l.confluence}TF` : '';
        const breakTag = l.broken ? ' ✗' : l.retested ? ' ↻' : '';
        title = `${prefix}×${l.touches}${confTag}${breakTag}`;

        lineStyle = l.broken ? 2 : 1; // broken = dotted, normal = dashed
        lineWidth = l.confluence >= 3 ? 2 : 1;
    }

    return { price: l.price, color, lineWidth, lineStyle, axisLabelVisible: !!showAxisLabel, title };
}

// Apply auto S/R levels to a mini-chart
function applyAutoLevels(sym) {
    // Always compute SR distance for sidebar (even if levels display is off)
    computeSRDistance(sym);

    if (!spGet('levelsEnabled', true)) return;
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.series || !chartObj.candleData) return;

    removeAutoLevels(sym);

    const levels = computeAutoLevels(chartObj.candleData);
    chartObj.autoLevels = [];

    levels.forEach(l => {
        const line = chartObj.series.createPriceLine(levelToPriceLine(l, false));
        chartObj.autoLevels.push(line);
    });
}

// Remove auto S/R levels from a mini-chart
function removeAutoLevels(sym) {
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.autoLevels) return;
    chartObj.autoLevels.forEach(pl => { try { chartObj.series.removePriceLine(pl); } catch(e) {} });
    chartObj.autoLevels = [];
}

// Apply auto S/R levels to modal chart (with Multi-TF confluence)
async function applyModalAutoLevels() {
    if (!spGet('levelsEnabled', true)) return;
    if (!modal.chart || !modal.series || !modal.candleData) return;

    removeModalAutoLevels();

    // Use multi-TF confluence for modal (richer data, single symbol)
    const sym = modal.currentSym;
    const tf = modal.currentTF || mc.globalTF;
    let levels;
    try {
        levels = await computeMultiTFLevels(sym, tf, modal.candleData);
    } catch {
        levels = computeAutoLevels(modal.candleData); // fallback to single-TF
    }
    modal.autoLevels = [];

    // Guard: modal may have been closed during async fetch
    if (!modal.chart || !modal.series) return;

    levels.forEach(l => {
        const line = modal.series.createPriceLine(levelToPriceLine(l, true));
        modal.autoLevels.push(line);
    });
}

function removeModalAutoLevels() {
    if (!modal.autoLevels) return;
    modal.autoLevels.forEach(pl => { try { modal.series.removePriceLine(pl); } catch(e) {} });
    modal.autoLevels = [];
}

// ── Auto Trendlines — Drawing ──────────────────────────────────

// Apply auto trendlines to a mini-chart (as LineSeries, like manual trendlines)
function applyAutoTrendlines(sym) {
    // Only compute TL distance if sorting by trendlines or trendlines enabled
    if (mc.sortBy === 'tl' || spGet('trendlinesEnabled', false)) {
        computeTLDistance(sym);
    }

    if (!spGet('trendlinesEnabled', false)) return;
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.series || !chartObj.candleData) return;

    removeAutoTrendlines(sym);

    const lines = computeAutoTrendlines(chartObj.candleData);
    chartObj.autoTrendlines = [];

    lines.forEach(tl => {
        const opacity = tl.broken ? 0.25 : Math.max(0.4, tl.score / 100);
        const baseColor = tl.type === 'support' ? [34, 197, 94] : [239, 68, 68];
        const color = `rgba(${baseColor.join(',')},${opacity})`;

        const ls = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
            color,
            lineWidth: 1,
            lineStyle: tl.broken ? 3 : 2, // dotted if broken, dashed if active
            crosshairMarkerVisible: false,
            lastValueVisible: false,
            priceLineVisible: false,
            pointMarkersVisible: false,
        });
        ls.setData([
            { time: tl.p1.time, value: tl.p1.price },
            { time: tl.p2.time, value: tl.p2.price },
        ]);
        chartObj.autoTrendlines.push({ lineSeries: ls });
    });
}

// Remove auto trendlines from a mini-chart
function removeAutoTrendlines(sym) {
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.autoTrendlines) return;
    chartObj.autoTrendlines.forEach(obj => {
        if (obj.lineSeries) try { chartObj.chart.removeSeries(obj.lineSeries); } catch(e) {}
    });
    chartObj.autoTrendlines = [];
}

// Apply auto trendlines to modal chart
function applyModalAutoTrendlines() {
    if (!spGet('trendlinesEnabled', false)) return;
    if (!modal.chart || !modal.series || !modal.candleData) return;

    removeModalAutoTrendlines();

    const lines = computeAutoTrendlines(modal.candleData);
    modal.autoTrendlines = [];

    lines.forEach(tl => {
        const opacity = tl.broken ? 0.25 : Math.max(0.4, tl.score / 100);
        const baseColor = tl.type === 'support' ? [34, 197, 94] : [239, 68, 68];
        const color = `rgba(${baseColor.join(',')},${opacity})`;

        const ls = modal.chart.addSeries(LightweightCharts.LineSeries, {
            color,
            lineWidth: tl.broken ? 1 : 2,
            lineStyle: tl.broken ? 3 : 2, // dotted if broken, dashed if active
            crosshairMarkerVisible: false,
            lastValueVisible: false,
            priceLineVisible: false,
            pointMarkersVisible: false,
        });
        ls.setData([
            { time: tl.p1.time, value: tl.p1.price },
            { time: tl.p2.time, value: tl.p2.price },
        ]);
        modal.autoTrendlines.push({ lineSeries: ls, type: tl.type, touches: tl.touches });
    });
}

function removeModalAutoTrendlines() {
    if (!modal.autoTrendlines) return;
    modal.autoTrendlines.forEach(obj => {
        if (obj.lineSeries) try { modal.chart.removeSeries(obj.lineSeries); } catch(e) {}
    });
    modal.autoTrendlines = [];
}

// ── Linear Regression Channel (auto-period via R²) ───────────────
// Scans lookback 20-200, picks best R²×log(length).
// Center = regression line, bands = ±2σ (standard deviation).

function computeRegressionChannel(candles, mult = 2.0) {
    if (!candles || candles.length < 30) return [];

    const n = candles.length;
    const closes = candles.map(c => c.close);

    // Scan multiple lookback periods, pick best by R² × log(length)
    let bestScore = -Infinity, bestPeriod = 50;
    const periods = [20, 30, 50, 75, 100, 150, 200].filter(p => p <= n);

    for (const period of periods) {
        const start = n - period;
        // Linear regression on [start..n-1]
        let sx = 0, sy = 0, sxy = 0, sxx = 0, syy = 0;
        for (let i = start; i < n; i++) {
            const x = i - start;
            const y = closes[i];
            sx += x; sy += y; sxy += x * y; sxx += x * x; syy += y * y;
        }
        const denom = period * sxx - sx * sx;
        if (denom === 0) continue;
        const slope = (period * sxy - sx * sy) / denom;
        const intercept = (sy - slope * sx) / period;

        // R² = 1 - SS_res / SS_tot
        let ssRes = 0, ssTot = 0;
        const meanY = sy / period;
        for (let i = start; i < n; i++) {
            const x = i - start;
            const predicted = intercept + slope * x;
            ssRes += (closes[i] - predicted) ** 2;
            ssTot += (closes[i] - meanY) ** 2;
        }
        const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
        const score = r2 * Math.log(period);
        if (score > bestScore) { bestScore = score; bestPeriod = period; }
    }

    // Compute regression for best period
    const start = n - bestPeriod;
    let sx = 0, sy = 0, sxy = 0, sxx = 0;
    for (let i = start; i < n; i++) {
        const x = i - start; const y = closes[i];
        sx += x; sy += y; sxy += x * y; sxx += x * x;
    }
    const denom = bestPeriod * sxx - sx * sx;
    if (denom === 0) return [];
    const slope = (bestPeriod * sxy - sx * sy) / denom;
    const intercept = (sy - slope * sx) / bestPeriod;

    // Standard deviation from regression line
    let sumSqDev = 0;
    for (let i = start; i < n; i++) {
        const x = i - start;
        const dev = closes[i] - (intercept + slope * x);
        sumSqDev += dev * dev;
    }
    const sigma = Math.sqrt(sumSqDev / bestPeriod);

    // Build data
    const result = [];
    for (let i = start; i < n; i++) {
        const x = i - start;
        const mid = intercept + slope * x;
        result.push({
            time: candles[i].time,
            mid,
            upper: mid + sigma * mult,
            lower: mid - sigma * mult,
        });
    }
    return result;
}

// Apply Regression Channel to mini-chart
function applyRegressionChannel(sym) {
    if (!spGet('ch_regression', false)) return;
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.series || !chartObj.candleData) return;
    removeRegressionChannel(sym);
    const data = computeRegressionChannel(chartObj.candleData);
    if (data.length < 2) return;
    chartObj.regressionCh = [];
    const addLine = (field, clr, dash) => {
        const ls = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
            color: clr, lineWidth: 1, lineStyle: dash ? 2 : 0,
            crosshairMarkerVisible: false, lastValueVisible: false,
            priceLineVisible: false, pointMarkersVisible: false,
        });
        ls.setData(data.map(d => ({ time: d.time, value: d[field] })));
        chartObj.regressionCh.push(ls);
    };
    addLine('upper', 'rgba(56,189,248,0.5)', false);
    addLine('mid',   'rgba(56,189,248,0.35)', true);
    addLine('lower', 'rgba(56,189,248,0.5)', false);
}

function removeRegressionChannel(sym) {
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.regressionCh) return;
    chartObj.regressionCh.forEach(ls => { try { chartObj.chart.removeSeries(ls); } catch(e) {} });
    chartObj.regressionCh = [];
}

function applyModalRegressionChannel() {
    if (!spGet('ch_regression', false)) return;
    if (!modal.chart || !modal.series || !modal.candleData) return;
    removeModalRegressionChannel();
    const data = computeRegressionChannel(modal.candleData);
    if (data.length < 2) return;
    modal.regressionCh = [];
    const addLine = (field, clr, dash) => {
        const ls = modal.chart.addSeries(LightweightCharts.LineSeries, {
            color: clr, lineWidth: dash ? 1 : 2, lineStyle: dash ? 2 : 0,
            crosshairMarkerVisible: false, lastValueVisible: false,
            priceLineVisible: false, pointMarkersVisible: false,
        });
        ls.setData(data.map(d => ({ time: d.time, value: d[field] })));
        modal.regressionCh.push(ls);
    };
    addLine('upper', 'rgba(56,189,248,0.6)', false);
    addLine('mid',   'rgba(56,189,248,0.4)', true);
    addLine('lower', 'rgba(56,189,248,0.6)', false);
}

function removeModalRegressionChannel() {
    if (!modal.regressionCh) return;
    modal.regressionCh.forEach(ls => { try { modal.chart.removeSeries(ls); } catch(e) {} });
    modal.regressionCh = [];
}

// ── Keltner Channel (EMA + ATR bands) ─────────────────────────────
// EMA(20) center line ± ATR(10) × multiplier = upper/lower bands
// Mean-reversion channel: price at edge = potential bounce

function computeKeltnerChannel(candles, emaPeriod = 20, atrPeriod = 10, mult = 1.5) {
    if (!candles || candles.length < Math.max(emaPeriod, atrPeriod) + 1) return [];

    // 1. EMA of close prices
    const closes = candles.map(c => c.close);
    const emaK = 2 / (emaPeriod + 1);
    const ema = new Array(candles.length);
    // Seed EMA with SMA of first emaPeriod candles
    let sum = 0;
    for (let i = 0; i < emaPeriod; i++) sum += closes[i];
    ema[emaPeriod - 1] = sum / emaPeriod;
    for (let i = emaPeriod; i < candles.length; i++) {
        ema[i] = closes[i] * emaK + ema[i - 1] * (1 - emaK);
    }

    // 2. ATR (True Range → SMA)
    const trs = [0]; // first candle has no prev close
    for (let i = 1; i < candles.length; i++) {
        const h = candles[i].high, l = candles[i].low, pc = candles[i - 1].close;
        trs.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
    }
    // Rolling SMA of TR
    const atr = new Array(candles.length);
    const startIdx = Math.max(emaPeriod - 1, atrPeriod);
    for (let i = startIdx; i < candles.length; i++) {
        let s = 0;
        for (let j = i - atrPeriod + 1; j <= i; j++) s += trs[j];
        atr[i] = s / atrPeriod;
    }

    // 3. Build data points (time, mid, upper, lower)
    const result = [];
    for (let i = startIdx; i < candles.length; i++) {
        if (ema[i] == null || atr[i] == null) continue;
        result.push({
            time: candles[i].time,
            mid: ema[i],
            upper: ema[i] + atr[i] * mult,
            lower: ema[i] - atr[i] * mult,
        });
    }
    return result;
}

// Apply Keltner Channel to a mini-chart
function applyKeltnerChannel(sym) {
    if (!spGet('ch_keltner', false)) return;
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.series || !chartObj.candleData) return;

    removeKeltnerChannel(sym);

    const data = computeKeltnerChannel(chartObj.candleData);
    if (data.length < 2) return;

    chartObj.keltner = [];

    // Upper band — orange, thin
    const upperLs = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.5)', lineWidth: 1, lineStyle: 0,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    upperLs.setData(data.map(d => ({ time: d.time, value: d.upper })));
    chartObj.keltner.push(upperLs);

    // Mid EMA — orange, dashed
    const midLs = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.35)', lineWidth: 1, lineStyle: 2,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    midLs.setData(data.map(d => ({ time: d.time, value: d.mid })));
    chartObj.keltner.push(midLs);

    // Lower band — orange, thin
    const lowerLs = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.5)', lineWidth: 1, lineStyle: 0,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    lowerLs.setData(data.map(d => ({ time: d.time, value: d.lower })));
    chartObj.keltner.push(lowerLs);
}

// Remove Keltner Channel from a mini-chart
function removeKeltnerChannel(sym) {
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.keltner) return;
    chartObj.keltner.forEach(ls => { try { chartObj.chart.removeSeries(ls); } catch(e) {} });
    chartObj.keltner = [];
}

// Apply Keltner Channel to modal chart
function applyModalKeltnerChannel() {
    if (!spGet('ch_keltner', false)) return;
    if (!modal.chart || !modal.series || !modal.candleData) return;

    removeModalKeltnerChannel();

    const data = computeKeltnerChannel(modal.candleData);
    if (data.length < 2) return;

    modal.keltner = [];

    const upperLs = modal.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.6)', lineWidth: 1, lineStyle: 0,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    upperLs.setData(data.map(d => ({ time: d.time, value: d.upper })));
    modal.keltner.push(upperLs);

    const midLs = modal.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.4)', lineWidth: 1, lineStyle: 2,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    midLs.setData(data.map(d => ({ time: d.time, value: d.mid })));
    modal.keltner.push(midLs);

    const lowerLs = modal.chart.addSeries(LightweightCharts.LineSeries, {
        color: 'rgba(251,146,60,0.6)', lineWidth: 1, lineStyle: 0,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    lowerLs.setData(data.map(d => ({ time: d.time, value: d.lower })));
    modal.keltner.push(lowerLs);
}

function removeModalKeltnerChannel() {
    if (!modal.keltner) return;
    modal.keltner.forEach(ls => { try { modal.chart.removeSeries(ls); } catch(e) {} });
    modal.keltner = [];
}

// ==========================================
// Live WebSocket — Binance kline stream
// ==========================================
const BINANCE_WS = 'wss://fstream.binance.com/market/stream';

function updateWsIndicator(state, extra) {
    let el = document.getElementById('wsStatusBadge');
    if (!el) {
        el = document.createElement('span');
        el.id = 'wsStatusBadge';
        el.style.cssText = 'font-size:10px;margin-left:6px;padding:1px 5px;border-radius:3px;vertical-align:middle;color:#fff;font-family:monospace;';
        const status = document.getElementById('mcStatus');
        if (status) status.parentElement.appendChild(el);
    }
    const colors = { open: '#22c55e', connecting: '#eab308', closed: '#ef4444', error: '#ef4444' };
    el.style.background = colors[state] || '#666';
    el.textContent = extra || state;
}

function wsConnect() {
    if (mc.ws && mc.ws.readyState <= 1) return; // CONNECTING or OPEN

    updateWsIndicator('connecting');
    console.log('[MC-WS] Connecting to:', BINANCE_WS, 'TF:', mc.globalTF);
    mc.ws = new WebSocket(BINANCE_WS);
    mc.ws.onopen = () => {
        console.log('[MC-WS] Connected, subscribing', mc.wsStreams.size, 'streams');
        updateWsIndicator('open', 'WS:0');
        // Subscribe any pending streams
        if (mc.wsStreams.size > 0) {
            const params = [...mc.wsStreams];
            mc.ws.send(JSON.stringify({ method: 'SUBSCRIBE', params, id: 1 }));
            console.log('[MC-WS] SUBSCRIBE sent:', params.length, 'streams:', params.slice(0, 3).join(', '), '...');
        }
        // Auto-resubscribe safety net: if no ticks after 5s, re-send SUBSCRIBE
        setTimeout(() => {
            if (mc._wsMsgCount === 0 && mc.ws && mc.ws.readyState === WebSocket.OPEN && mc.wsStreams.size > 0) {
                console.warn('[MC-WS] No ticks after 5s! Re-subscribing', mc.wsStreams.size, 'streams on TF=' + mc.globalTF);
                updateWsIndicator('open', 'RE-SUB');
                // Re-build streams from current charts (in case globalTF changed)
                const freshStreams = Object.keys(mc.charts).map(s => `${s.toLowerCase()}@kline_${mc.globalTF}`);
                mc.wsStreams = new Set(freshStreams);
                mc.ws.send(JSON.stringify({ method: 'SUBSCRIBE', params: freshStreams, id: Date.now() }));
                document.title = `RE-SUB ${freshStreams.length} ${mc.globalTF}`;
            }
        }, 5000);
    };
    mc._wsMsgCount = 0;
    mc._wsLastTick = 0;
    mc.ws.onmessage = (evt) => {
        try {
            const msg = JSON.parse(evt.data);
            // Log ALL raw messages for first 5 to debug format
            if (mc._wsMsgCount < 3) {
                console.log('[MC-WS] raw msg:', JSON.stringify(msg).substring(0, 200));
            }
            if (!msg.data || msg.data.e !== 'kline') return;
            const k = msg.data.k;
            const sym = k.s;
            const candle = {
                time: Math.floor(k.t / 1000),
                open: parseFloat(k.o),
                high: parseFloat(k.h),
                low: parseFloat(k.l),
                close: parseFloat(k.c),
            };

            const vol = parseFloat(k.v);

            // Count WS ticks and show on badge
            mc._wsMsgCount++;
            mc._wsLastTick = Date.now();
            const incomingInterval = k.i; // actual interval from Binance: "5m", "4h", etc.
            if (mc._wsMsgCount <= 5 || mc._wsMsgCount % 100 === 0) {
                updateWsIndicator('open', `WS:${mc._wsMsgCount}`);
            }
            // TEMP DEBUG: show tick info on first 5 ticks
            if (mc._wsMsgCount <= 5) {
                console.log(`[MC-WS] tick#${mc._wsMsgCount} sym=${sym} i=${incomingInterval} globalTF=${mc.globalTF} hasChart=${!!mc.charts[sym]} streams=${mc.wsStreams.size}`);
                document.title = `T${mc._wsMsgCount} ${incomingInterval} ${sym.slice(0,4)} streams=${mc.wsStreams.size}`;
            }

            // Check price alerts (custom drawings + dedicated + library DM drawings)
            checkPriceAlerts(sym, candle.close);
            checkDedicatedPriceAlerts(sym, candle.close);
            if (typeof DM !== 'undefined' && DM && DM.checkAlerts) DM.checkAlerts(sym, candle.close);

            // Update mini-chart (throttled: max 4 updates/sec per symbol)
            if (mc.charts[sym]) {
                const now = Date.now();
                const lastUpdate = mc.charts[sym]._lastWsUpdate || 0;
                if (now - lastUpdate >= 250) {
                    mc.charts[sym]._lastWsUpdate = now;
                    // Detect new bar (candle boundary) for smooth scroll
                    const cd = mc.charts[sym].candleData;
                    const isNewBar = cd && cd.length > 0 && candle.time > cd[cd.length - 1].time;
                    mc.charts[sym].series.update(candle);
                    mc.charts[sym].volSeries.update({
                        time: candle.time,
                        value: vol,
                        color: candle.close >= candle.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'
                    });
                    // New bar scroll handled by shiftVisibleRangeOnNewBar: true
                    // Keep dataCache in sync with live candles
                    if (cd) {
                        if (cd.length > 0 && cd[cd.length - 1].time === candle.time) {
                            cd[cd.length - 1] = { ...candle, volume: vol };
                        } else if (cd.length === 0 || candle.time > cd[cd.length - 1].time) {
                            cd.push({ ...candle, volume: vol });
                        }
                    }
                }
            } else if (mc.dataCache[sym] && mc.dataCache[sym].tf === mc.globalTF) {
                // Chart destroyed but cache exists — keep cache updated to prevent gaps on return
                const cd = mc.dataCache[sym].candles;
                if (cd && cd.length > 0) {
                    if (cd[cd.length - 1].time === candle.time) {
                        cd[cd.length - 1] = { ...candle, volume: vol };
                    } else if (candle.time > cd[cd.length - 1].time) {
                        cd.push({ ...candle, volume: vol });
                        // Also update volume cache
                        if (mc.dataCache[sym].volume) {
                            mc.dataCache[sym].volume.push({
                                time: candle.time, value: vol,
                                color: candle.close >= candle.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'
                            });
                        }
                        // Cap cache size to prevent unbounded growth
                        if (cd.length > 2000) { cd.splice(0, cd.length - 1500); }
                        if (mc.dataCache[sym].volume?.length > 2000) { mc.dataCache[sym].volume.splice(0, mc.dataCache[sym].volume.length - 1500); }
                    }
                }
            }

            // Update multi-chart slots
            for (const slot of mch.slots) {
                if (slot.sym === sym && slot.chart && slot.series) {
                    const slotStream = `${sym.toLowerCase()}@kline_${slot.tf}`;
                    const incomingStream = msg.stream || '';
                    if (incomingStream === slotStream) {
                        // Detect new bar for smooth scroll
                        const isSlotNewBar = slot._lastTime && candle.time > slot._lastTime;
                        slot.series.update(candle);
                        if (slot.volSeries) slot.volSeries.update({
                            time: candle.time,
                            value: vol,
                            color: candle.close >= candle.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'
                        });
                        slot._lastTime = candle.time;
                        // New bar scroll handled by shiftVisibleRangeOnNewBar: true
                        // Update header price/change
                        const pair = mc.allPairs.find(p => p.symbol === sym);
                        if (pair) {
                            const prec = getPricePrecision(candle.close);
                            const priceEl = slot.el.querySelector('.mch-slot-price');
                            if (priceEl) priceEl.textContent = '$' + candle.close.toFixed(prec);
                        }
                    }
                }
            }

            // Update modal chart if same symbol & TF
            if (modal.chart && modal.currentSym === sym) {
                const modalStream = `${sym.toLowerCase()}@kline_${modal.currentTF}`;
                const incomingStream = msg.stream || '';
                if (incomingStream === modalStream || modal.wsStream === `${sym.toLowerCase()}@kline_${k.i}`) {
                    // Detect new bar for smooth scroll
                    const mcd = modal.candleData;
                    const isNewBar = mcd && mcd.length > 0 && candle.time > mcd[mcd.length - 1].time;
                    modal.series.update(candle);
                    if (modal.volSeries) modal.volSeries.update({
                        time: candle.time,
                        value: vol,
                        color: candle.close >= candle.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'
                    });
                    // Keep modal.candleData in sync with live candles
                    if (mcd) {
                        if (mcd.length > 0 && mcd[mcd.length - 1].time === candle.time) {
                            mcd[mcd.length - 1] = { ...candle, volume: vol };
                        } else if (mcd.length === 0 || candle.time > mcd[mcd.length - 1].time) {
                            mcd.push({ ...candle, volume: vol });
                        }
                    }
                    // New bar scroll handled by shiftVisibleRangeOnNewBar: true
                }
            }
        } catch (e) { console.error('[MC-WS] onmessage error:', e.message, e.stack?.split('\n')[1]); }
    };
    mc.ws.onclose = (evt) => {
        updateWsIndicator('closed');
        if (mc._wsClosing) return; // Don't reconnect on intentional close
        console.warn('[MC-WS] Disconnected (code:', evt.code, 'reason:', evt.reason || 'none', '), reconnecting in 3s...');
        setTimeout(wsConnect, 3000);
    };
    mc.ws.onerror = (e) => {
        updateWsIndicator('error');
        console.error('[MC-WS] Error:', e.message || e.type || 'unknown');
    };
}

// Clean up WebSocket on page unload to prevent dangling connections
window.addEventListener('beforeunload', () => {
    mc._wsClosing = true;
    if (mc.ws) {
        mc.ws.close();
        mc.ws = null;
    }
});

// Debounced WS subscribe — Binance limits 10 msgs/sec per WS connection
// Collect individual subscribes for 200ms, then send as one batch
let _wsPendingSub = new Set();
let _wsSubTimer = null;

function _flushWsSubscribe() {
    _wsSubTimer = null;
    if (_wsPendingSub.size === 0) { console.warn('[MC-WS] _flushWsSubscribe: nothing pending'); return; }
    if (!mc.ws || mc.ws.readyState !== WebSocket.OPEN) { console.error('[MC-WS] _flushWsSubscribe: WS not open! state=', mc.ws?.readyState); return; }
    const params = [..._wsPendingSub];
    _wsPendingSub.clear();
    mc.ws.send(JSON.stringify({ method: 'SUBSCRIBE', params, id: Date.now() }));
    console.log('[MC-WS] Batch subscribed', params.length, 'streams, TF=' + mc.globalTF, 'sample:', params.slice(0, 3));
}

function wsSubscribe(sym) {
    const stream = `${sym.toLowerCase()}@kline_${mc.globalTF}`;
    if (mc.wsStreams.has(stream)) return;
    mc.wsStreams.add(stream);

    if (!mc.ws || mc.ws.readyState !== WebSocket.OPEN) {
        console.log(`[MC-WS] wsSubscribe ${stream}: WS not open (state=${mc.ws?.readyState}), calling wsConnect`);
        wsConnect();
        return; // onopen will subscribe all pending
    }
    // Debounce: collect for 200ms, send as one batch
    _wsPendingSub.add(stream);
    if (!_wsSubTimer) {
        _wsSubTimer = setTimeout(_flushWsSubscribe, 200);
    }
}

function wsUnsubscribeAll() {
    console.log('[MC-WS] wsUnsubscribeAll: killing WS, had', mc.wsStreams.size, 'streams');
    // Kill the entire WS connection — cleaner than unsub/resub
    // New connection will be created by wsSubscribe() when charts reload
    mc._wsClosing = true;
    if (mc.ws) {
        mc.ws.onclose = null; // prevent async zombie reconnect
        try { mc.ws.close(); } catch(_) {}
        mc.ws = null;
    }
    mc._wsClosing = false;
    mc.wsStreams.clear();
    _wsPendingSub.clear();
    if (_wsSubTimer) { clearTimeout(_wsSubTimer); _wsSubTimer = null; }
}

// ==========================================
// Shift+Drag Ruler (like TradingView)
// ==========================================
function attachRuler(chartEl, chart, series) {
    // Cleanup previous ruler listeners on this element to prevent leaks
    if (chartEl._rulerCleanup) chartEl._rulerCleanup();

    let rulerActive = false;
    let startX = 0, startY = 0;
    let startPrice = 0, startTime = 0;
    let line = null, label = null;

    function createOverlay() {
        // SVG line
        line = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        line.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100;';
        line.innerHTML = '<line x1="0" y1="0" x2="0" y2="0" stroke="#5b9cf6" stroke-width="1.5" stroke-dasharray="4,3"/>';
        chartEl.appendChild(line);

        // Label
        label = document.createElement('div');
        label.style.cssText = 'position:absolute;z-index:101;pointer-events:none;background:rgba(59,130,246,0.9);color:#fff;font-size:11px;font-weight:600;padding:3px 8px;border-radius:4px;white-space:nowrap;display:none;';
        chartEl.appendChild(label);
    }

    function removeOverlay() {
        if (line) { line.remove(); line = null; }
        if (label) { label.remove(); label = null; }
    }

    const onMouseDown = (e) => {
        if (!e.shiftKey) return;
        e.preventDefault();
        e.stopPropagation();

        const rect = chartEl.getBoundingClientRect();
        startX = e.clientX - rect.left;
        startY = e.clientY - rect.top;

        // Convert pixel to price/time
        startPrice = series.coordinateToPrice(startY);
        startTime = chart.timeScale().coordinateToTime(startX);
        if (startPrice === null) return;

        rulerActive = true;
        removeOverlay();
        createOverlay();

        // Temporarily disable chart interaction
        chart.applyOptions({ handleScroll: false, handleScale: false });
    };
    chartEl.addEventListener('mousedown', onMouseDown);

    const onMouseMove = (e) => {
        if (!rulerActive || !line || !label) return;

        const rect = chartEl.getBoundingClientRect();
        const curX = e.clientX - rect.left;
        const curY = e.clientY - rect.top;
        const curPrice = series.coordinateToPrice(curY);
        if (curPrice === null) return;

        // Update SVG line
        const svgLine = line.querySelector('line');
        svgLine.setAttribute('x1', startX);
        svgLine.setAttribute('y1', startY);
        svgLine.setAttribute('x2', curX);
        svgLine.setAttribute('y2', curY);

        // Calculate diff
        const priceDiff = curPrice - startPrice;
        const pctDiff = startPrice !== 0 ? (priceDiff / startPrice * 100) : 0;
        const prec = getPricePrecision(Math.abs(startPrice));
        const sign = priceDiff >= 0 ? '+' : '';

        // Calculate time duration
        const curTime = chart.timeScale().coordinateToTime(curX);
        let timeStr = '';
        if (startTime && curTime) {
            const timeDiffSec = Math.abs(curTime - startTime);
            if (timeDiffSec < 3600) {
                timeStr = Math.round(timeDiffSec / 60) + 'm';
            } else if (timeDiffSec < 86400) {
                timeStr = (timeDiffSec / 3600).toFixed(1) + 'h';
            } else {
                timeStr = (timeDiffSec / 86400).toFixed(1) + 'd';
            }
        }

        // Position label at midpoint
        const midX = (startX + curX) / 2;
        const midY = Math.min(startY, curY) - 8;
        label.style.left = midX + 'px';
        label.style.top = Math.max(2, midY) + 'px';
        label.style.transform = 'translateX(-50%)';
        label.style.display = 'block';
        label.style.background = priceDiff >= 0 ? 'rgba(34,197,94,0.92)' : 'rgba(239,68,68,0.92)';
        const timeInfo = timeStr ? ` | ${timeStr}` : '';
        label.textContent = `${sign}${priceDiff.toFixed(prec)}  (${sign}${pctDiff.toFixed(2)}%)${timeInfo}`;
    };
    chartEl.addEventListener('mousemove', onMouseMove);

    const endRuler = () => {
        if (!rulerActive) return;
        rulerActive = false;
        chart.applyOptions({
            handleScroll: { mouseWheel: true, pressedMouseMove: true, vertTouchDrag: true, horzTouchDrag: true },
            handleScale: { mouseWheel: true, pinch: true, axisPressedMouseMove: { price: true, time: true }, axisDoubleClickReset: { price: true, time: true } }
        });
        // Remove after 3 seconds
        setTimeout(removeOverlay, 3000);
    };

    chartEl.addEventListener('mouseup', endRuler);
    chartEl.addEventListener('mouseleave', endRuler);

    // Store cleanup function to prevent listener leaks on re-attach
    chartEl._rulerCleanup = () => {
        chartEl.removeEventListener('mousedown', onMouseDown);
        chartEl.removeEventListener('mousemove', onMouseMove);
        chartEl.removeEventListener('mouseup', endRuler);
        chartEl.removeEventListener('mouseleave', endRuler);
        removeOverlay();
        chartEl._rulerCleanup = null;
    };
}

// ==========================================
// Coin Detail Modal
// ==========================================
const modal = {
    chart: null,
    series: null,
    lines: [],
    currentSym: null,
    currentTF: '15m',
    wsStream: null
};

function openCoinModal(sym) {
    const pair = mc.allPairs.find(p => p.symbol === sym);
    if (!pair) return;

    modal.currentSym = sym;
    modal.currentTF = mc.globalTF;

    const ticker = sym.replace('USDT', '');
    const prec = getPricePrecision(pair.lastPrice);
    const chg = pair.priceChange;
    const chgClass = chg >= 0 ? 'mc-metric-green' : 'mc-metric-red';
    const chgSign = chg >= 0 ? '+' : '';

    // Header
    el('cmSymbol').textContent = ticker + '/USDT';
    const cmCopyBtn = el('cmCopyBtn');
    if (cmCopyBtn) {
        cmCopyBtn.dataset.ticker = sym.toLowerCase();
        cmCopyBtn.title = `Copy ${sym.toLowerCase()}`;
        cmCopyBtn.onclick = (e) => {
            e.stopPropagation();
            navigator.clipboard.writeText(sym.toLowerCase()).then(() => {
                cmCopyBtn.classList.add('mc-copy-ok');
                setTimeout(() => cmCopyBtn.classList.remove('mc-copy-ok'), 800);
            });
        };
    }
    el('cmPrice').textContent = '$' + pair.lastPrice.toFixed(prec);
    const cmChange = el('cmChange');
    cmChange.textContent = chgSign + chg.toFixed(2) + '%';
    cmChange.className = 'mc-modal-change ' + chgClass;

    // Metrics in header (icons, no text labels)
    const vol = pair.quoteVol >= 1e9 ? (pair.quoteVol / 1e9).toFixed(2) + 'B' : (pair.quoteVol / 1e6).toFixed(1) + 'M';
    const tradesStr = pair.tradesCount >= 1e6 ? (pair.tradesCount / 1e6).toFixed(1) + 'M'
        : pair.tradesCount >= 1e3 ? (pair.tradesCount / 1e3).toFixed(1) + 'K'
        : pair.tradesCount.toString();
    el('cmStats').innerHTML = `
        <span class="cm-metric" title="24h Volume"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><rect x="1" y="5" width="2" height="5" fill="currentColor" opacity="0.5"/><rect x="4" y="2" width="2" height="8" fill="currentColor" opacity="0.7"/><rect x="7" y="0" width="2" height="10" fill="currentColor"/></svg>$${vol}</span>
        <span class="cm-metric" title="NATR Volatility"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M0 7L3 3L5 6L7 1L10 5" stroke="currentColor" stroke-width="1.3" fill="none"/></svg><span id="cmNatr">${pair.proxyNatr.toFixed(1)}%</span></span>
        <span class="cm-metric" title="24h Trades"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M1 3h3M6 3h3M1 7h3M6 7h3" stroke="currentColor" stroke-width="1.2"/></svg>${tradesStr}</span>
        <span class="cm-metric" title="24h High"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M5 1L8 5H2L5 1Z" fill="#22c55e" opacity="0.8"/></svg>${parseFloat(pair.highPrice).toFixed(prec)}</span>
        <span class="cm-metric" title="24h Low"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M5 9L2 5H8L5 9Z" fill="#ef4444" opacity="0.8"/></svg>${parseFloat(pair.lowPrice).toFixed(prec)}</span>
        <span class="cm-metric" id="cmVpin" title="VPIN — Order Flow Toxicity (0=balanced, 1=informed)"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><circle cx="5" cy="5" r="4" stroke="currentColor" stroke-width="1.2" fill="none"/><path d="M5 3v4M5 3l2 2M5 3l-2 2" stroke="currentColor" stroke-width="1"/></svg>VPIN: —</span>
        <span class="cm-metric" id="cmFillKill" title="Fill:Kill — Wall authenticity (>0.5=genuine, <0.3=spoof)"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><rect x="1" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1" fill="none"/><path d="M3 5h4" stroke="currentColor" stroke-width="1.2"/></svg>F:K —</span>
        <span class="cm-metric" id="cmResilience" title="Book Stability — depth consistency near price (1=stable, 0=fragile)"><svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M1 8L3 4L5 6L7 3L9 5" stroke="currentColor" stroke-width="1.2" fill="none"/><line x1="1" y1="9" x2="9" y2="9" stroke="currentColor" stroke-width="0.8"/></svg>Stab —</span>
    `;

    // Fetch metrics with AbortController (cancelled on modal close)
    if (modal._metricsController) modal._metricsController.abort();
    modal._metricsController = new AbortController();
    const metricSignal = modal._metricsController.signal;

    fetch('/api/vpin?symbol=' + sym, { signal: metricSignal }).then(r => r.json()).then(j => {
        const vpinEl = document.getElementById('cmVpin');
        if (!vpinEl || !j.success || !j.data || j.data.vpin == null) return;
        const v = j.data.vpin;
        const color = v >= 0.6 ? '#ef4444' : v >= 0.4 ? '#fb923c' : '#22c55e';
        vpinEl.innerHTML = `<svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><circle cx="5" cy="5" r="4" stroke="${color}" stroke-width="1.2" fill="none"/><path d="M5 3v4M5 3l2 2M5 3l-2 2" stroke="${color}" stroke-width="1"/></svg><span style="color:${color}">VPIN: ${v.toFixed(3)}</span>`;
    }).catch(() => {});

    fetch('/api/fill-kill?symbol=' + sym, { signal: metricSignal }).then(r => r.json()).then(j => {
        const fkEl = document.getElementById('cmFillKill');
        if (!fkEl || !j.success || !j.data || j.data.fillKillRatio == null) return;
        const r = j.data.fillKillRatio;
        const color = r < 0.3 ? '#ef4444' : r < 0.5 ? '#fb923c' : '#22c55e';
        fkEl.innerHTML = `<svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><rect x="1" y="1" width="8" height="8" rx="1" stroke="${color}" stroke-width="1" fill="none"/><path d="M3 5h4" stroke="${color}" stroke-width="1.2"/></svg><span style="color:${color}">F:K ${r.toFixed(2)}</span> <span style="color:#64748b;font-size:10px">(${j.data.filled}/${j.data.total})</span>`;
    }).catch(() => {});

    fetch('/api/resilience?symbol=' + sym, { signal: metricSignal }).then(r => r.json()).then(j => {
        const stEl = document.getElementById('cmResilience');
        if (!stEl || !j.success || !j.data || j.data.stability == null) return;
        const s = j.data.stability;
        const color = s < 0.5 ? '#ef4444' : s < 0.75 ? '#fb923c' : '#22c55e';
        const depthStr = j.data.depthScore >= 1e6 ? (j.data.depthScore / 1e6).toFixed(1) + 'M' : j.data.depthScore >= 1e3 ? (j.data.depthScore / 1e3).toFixed(0) + 'K' : j.data.depthScore;
        stEl.innerHTML = `<svg width="11" height="11" viewBox="0 0 10 10" style="vertical-align:-1px;margin-right:2px"><path d="M1 8L3 4L5 6L7 3L9 5" stroke="${color}" stroke-width="1.2" fill="none"/><line x1="1" y1="9" x2="9" y2="9" stroke="${color}" stroke-width="0.8"/></svg><span style="color:${color}">Stab ${(s * 100).toFixed(0)}%</span> <span style="color:#64748b;font-size:10px">$${depthStr}</span>`;
    }).catch(() => {});

    // TF buttons — set active
    const tfBtns = el('cmTFButtons');
    tfBtns.querySelectorAll('.mc-tf-btn').forEach(b => {
        b.classList.toggle('active', b.dataset.tf === modal.currentTF);
    });

    // Show modal (CSS: visibility:hidden→visible, NOT display:none)
    el('coinModal').classList.remove('hidden');

    // Create or recreate chart
    if (modal._infiniteUnsub) { try { modal._infiniteUnsub(); } catch(e) {} modal._infiniteUnsub = null; }
    if (modal.chart) {
        modal.chart.remove();
        modal.chart = null;
    }

    const chartEl = el('cmChartBody');
    // Clean up stale DOM children (legend, ruler, toolbar) left from previous chart
    chartEl.innerHTML = '';

    // Re-create loader overlay (innerHTML cleared it)
    const loader = document.createElement('div');
    loader.id = 'cmChartLoader';
    loader.className = 'cm-chart-loader';
    loader.innerHTML = '<div class="cm-spinner"></div><span>Loading chart…</span>';
    chartEl.appendChild(loader);

    const cw = chartEl.clientWidth, ch = chartEl.clientHeight;
    console.log('[modal] chartEl dimensions:', cw, 'x', ch, '| children cleared');

    const minMove = parseFloat((1 / Math.pow(10, prec)).toFixed(prec));

    const wmText = spGet('showWatermark', true) ? sym.replace('USDT', '/USDT') : '';
    // Standard chart (same as mini-charts — no gapless, crypto is 24/7)
    modal._gapless = null;
    modal.chart = LightweightCharts.createChart(chartEl, {
        autoSize: true,
        layout: { background: { type: 'solid', color: 'transparent' }, textColor: '#94a3b8' },
        grid: getGridOpts(),
        crosshair: { mode: 0 },
        rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)', scaleMargins: { top: 0.05, bottom: 0.05 }, minimumWidth: 50, mode: getPriceScaleMode() },
        timeScale: { borderColor: 'rgba(255,255,255,0.08)', timeVisible: true, secondsVisible: false, rightOffset: 50, shiftVisibleRangeOnNewBar: true, lockVisibleTimeRangeOnResize: true, tickMarkFormatter: localTickFormatter },
        handleScroll: { mouseWheel: true, pressedMouseMove: true, vertTouchDrag: true, horzTouchDrag: true },
        handleScale: { mouseWheel: true, pinch: true, axisPressedMouseMove: { price: true, time: true }, axisDoubleClickReset: { price: true, time: true } },
    });

    modal.series = addMainSeries(modal.chart, prec, minMove);

    if (wmText) {
        try {
            LightweightCharts.createTextWatermark(modal.chart, { lines: [{ text: wmText, color: 'rgba(255,255,255,0.04)', fontSize: 48 }] });
        } catch (e) {
            console.warn('[modal] createTextWatermark failed (LWC v5 compat):', e.message);
        }
    }

    modal.volSeries = modal.chart.addSeries(LightweightCharts.HistogramSeries, {
        priceFormat: { type: 'volume' },
        priceScaleId: 'vol',
        color: 'rgba(100,116,139,0.3)',
    });
    modal.chart.priceScale('vol').applyOptions({
        scaleMargins: { top: getVolScaleTop(), bottom: 0 },
        drawTicks: false,
        borderVisible: false,
    });

    // autoSize: true handles resize via its own internal ResizeObserver
    if (modal._resizeObserver) { modal._resizeObserver.disconnect(); modal._resizeObserver = null; }

    modal.lines = [];
    modal.drawings = [];

    // Attach ruler to modal chart
    attachRuler(chartEl, modal.chart, modal.series);

    // OHLCV legend on crosshair move
    const legend = document.createElement('div');
    legend.className = 'mc-ohlcv-legend';
    chartEl.appendChild(legend);
    modal.legend = legend;

    modal.chart.subscribeCrosshairMove(param => {
        if (!param || !param.time || !modal.legend) {
            if (modal.legend) modal.legend.style.display = 'none';
            return;
        }
        const data = param.seriesData.get(modal.series);
        if (!data) { modal.legend.style.display = 'none'; return; }
        const p = getPricePrecision(data.close || data.open || 1);
        const o = (data.open || 0).toFixed(p);
        const h = (data.high || 0).toFixed(p);
        const l = (data.low || 0).toFixed(p);
        const c = (data.close || 0).toFixed(p);
        const volData = param.seriesData.get(modal.volSeries);
        const v = volData ? (volData.value >= 1e6 ? (volData.value/1e6).toFixed(1)+'M' : (volData.value >= 1e3 ? (volData.value/1e3).toFixed(0)+'K' : volData.value.toFixed(0))) : '—';
        const chg = data.close >= data.open;
        const color = chg ? '#22c55e' : '#ef4444';
        modal.legend.style.display = 'flex';
        modal.legend.innerHTML = `<span style="color:${color}">O <b>${o}</b></span><span style="color:${color}">H <b>${h}</b></span><span style="color:${color}">L <b>${l}</b></span><span style="color:${color}">C <b>${c}</b></span><span style="color:var(--text-muted)">V <b>${v}</b></span>`;
    });

    // Reset drawing tool to cursor on every modal open (prevents locked interaction)
    draw.activeTool = 'cursor';
    draw.clickCount = 0;
    setDrawCtxModal();
    renderDrawToolbar();
    // Only attach drawing handlers once per DOM element (they use global state via DC()/DS())
    if (!chartEl._drawHandlersAttached) {
        setupDrawingHandlers();
        chartEl._drawHandlersAttached = true;
    }
    updateModalCursor();

    // ---- Price Alert: bell button + right-click on price scale ----
    const alertBtn = document.createElement('button');
    alertBtn.className = 'chart-alert-btn';
    alertBtn.innerHTML = '🔔';
    alertBtn.title = 'Price Alerts';
    alertBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleAlertPanel(sym, chartEl);
    });
    chartEl.appendChild(alertBtn);

    // Right-click on price scale → add alert at that price (attach once)
    if (!chartEl._contextmenuAttached) {
        chartEl._contextmenuAttached = true;
        chartEl.addEventListener('contextmenu', (e) => {
            if (!modal.series || !modal.currentSym) return;
            const rect = chartEl.getBoundingClientRect();
            const xInChart = e.clientX - rect.left;
            // Right ~60px is the price scale area
            if (xInChart < rect.width - 65) return;
            e.preventDefault();
            const yInChart = e.clientY - rect.top;
            try {
                const price = modal.series.coordinateToPrice(yInChart);
                if (!price || isNaN(price) || price <= 0) return;
                showAlertContextMenu(e.clientX, e.clientY, price, modal.currentSym);
            } catch {}
        });
    }

    // Load and render alert lines after chart data is loaded
    priceAlertStore.loadFromServer().then(() => {
        applyAlertLinesToChart(sym, modal.series);
    }).catch(() => {
        applyAlertLinesToChart(sym, modal.series);
    });

    // Attach library DrawingManager (if loaded)
    if (typeof DM !== 'undefined' && DM && DM.attach) {
        try {
            DM.attach(modal.chart, modal.series, chartEl, sym, () => {
                // Callback: library finished drawing → reset our UI to cursor
                draw.activeTool = 'cursor';
                draw.clickCount = 0;
                renderDrawToolbar();
                updateModalCursor();
            });
        } catch(e) { console.warn('[DM] attach failed:', e.message); }
    }

    loadModalChart(sym, modal.currentTF);
    startCountdown();
}

// ---- Candle Countdown Timer ----

function startCountdown() {
    if (modal._countdownTimer) clearInterval(modal._countdownTimer);
    updateCountdown();
    modal._countdownTimer = setInterval(updateCountdown, 1000);
}

function updateCountdown() {
    const cdEl = document.getElementById('cmChartCountdown');
    if (!cdEl || !modal.currentTF) return;
    const ms = TF_MS[modal.currentTF];
    if (!ms) { cdEl.textContent = ''; return; }
    const now = Date.now();
    const remaining = ms - (now % ms);
    const totalSec = Math.floor(remaining / 1000);
    if (ms <= 3600000) {
        const m = Math.floor(totalSec / 60);
        const s = totalSec % 60;
        cdEl.textContent = `${m}:${s.toString().padStart(2, '0')}`;
    } else {
        const h = Math.floor(totalSec / 3600);
        const m = Math.floor((totalSec % 3600) / 60);
        const s = totalSec % 60;
        cdEl.textContent = `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
    }
}

let _modalLoadController = null; // AbortController for current modal chart load

async function loadModalChart(sym, tf) {
    // Abort previous load if still in progress
    if (_modalLoadController) _modalLoadController.abort();
    _modalLoadController = new AbortController();
    const signal = _modalLoadController.signal;

    // Loader helpers
    const loaderEl = document.getElementById('cmChartLoader');
    const showLoader = () => { if (loaderEl) { loaderEl.innerHTML = '<div class="cm-spinner"></div><span>Loading chart…</span>'; loaderEl.classList.remove('hidden'); } };
    const hideLoader = () => { if (loaderEl) loaderEl.classList.add('hidden'); };
    const showError = (msg) => {
        if (loaderEl) {
            loaderEl.innerHTML = `<div class="cm-chart-error">
                <div style="margin-bottom:8px">⚠ ${msg}</div>
                <button onclick="loadModalChart('${sym}','${tf}')">Retry</button>
            </div>`;
            loaderEl.classList.remove('hidden');
        }
    };
    showLoader();

    // Unsubscribe previous modal WS stream
    if (modal.wsStream) {
        if (mc.ws && mc.ws.readyState === WebSocket.OPEN) {
            mc.ws.send(JSON.stringify({ method: 'UNSUBSCRIBE', params: [modal.wsStream], id: Date.now() }));
        }
        mc.wsStreams.delete(modal.wsStream);
        modal.wsStream = null;
    }

    try {
        // Phase 1: fast — 1000 candles (pre-warmed in server cache)
        const res1 = await fetch(`/api/klines?symbol=${sym}&interval=${tf}&limit=1000`, { signal });
        if (!res1.ok) throw new Error(`HTTP ${res1.status}`);
        const json1 = await res1.json();
        if (signal.aborted || !Array.isArray(json1) || !modal.chart) return;
        if (json1.length === 0) { showError('No data for ' + sym); return; }

        hideLoader();
        const data1 = parseKlines(json1);
        modal.candleData = data1;
        if (drawCtx.source === 'modal') drawCtx.candleData = data1;

        console.log('[modal] setData:', data1.length, 'candles, chart size:', modal.chart.timeScale().width(), 'x', el('cmChartBody').clientHeight);
        modal.series.setData(data1);
        if (modal.volSeries) modal.volSeries.setData(extractVolume(data1));

        // Show last ~100 candles in viewport
        const visFrom = Math.max(0, data1.length - 100);
        const visTo = data1.length - 1 + 10;
        modal.chart.timeScale().setVisibleLogicalRange({ from: visFrom, to: visTo });
        console.log('[modal] visibleRange set:', visFrom, '-', visTo);

        // Safety net: if chart internal width was 0 when range was set,
        // re-apply range after layout settles (fixes LWC v5 first-open bug)
        const tsWidth = modal.chart.timeScale().width();
        if (tsWidth <= 0) {
            console.log('[modal] timeScale width=0, scheduling fitContent fallback');
            setTimeout(() => {
                if (!modal.chart) return;
                const w2 = modal.chart.timeScale().width();
                console.log('[modal] fallback: timeScale width now=', w2);
                if (w2 > 0 && modal.candleData) {
                    const vf = Math.max(0, modal.candleData.length - 100);
                    modal.chart.timeScale().setVisibleLogicalRange({ from: vf, to: modal.candleData.length - 1 + 10 });
                } else {
                    modal.chart.timeScale().fitContent();
                }
            }, 150);
        }

        // Update modal NATR with real value
        const modalNatr = calcNATR(data1);
        const cmNatrEl = document.getElementById('cmNatr');
        if (cmNatrEl && modalNatr > 0) cmNatrEl.textContent = modalNatr.toFixed(1) + '%';

        // Restore saved drawings for this symbol
        restoreDrawings();

        // Signal markers on modal chart
        // Show all signals for this symbol (not just pending one)
        const isSignalsTab = document.getElementById('tab-signals')?.style.display !== 'none';
        applySignalMarkers(sym, modal.series, data1, modal, isSignalsTab || !!window._pendingSignalMarker);

        // Apply density walls to modal
        applyDensityToModal();

        // Apply auto S/R levels + trendlines + channels to modal
        applyModalAutoLevels();
        applyModalAutoTrendlines();
        applyModalKeltnerChannel();
        applyModalRegressionChannel();

        // Apply OI overlay if enabled
        applyOIOverlay(modal.chart, sym);

        // Attach depth heatmap overlay (Bookmap-style)
        if (typeof depthHeatmapUI !== 'undefined') {
            try { depthHeatmapUI.attach(modal); } catch(e) { console.warn('[modal] heatmap attach failed:', e.message); }
        }

        // Subscribe modal to live WS
        const stream = `${sym.toLowerCase()}@kline_${tf}`;
        modal.wsStream = stream;
        mc.wsStreams.add(stream);
        if (mc.ws && mc.ws.readyState === WebSocket.OPEN) {
            mc.ws.send(JSON.stringify({ method: 'SUBSCRIBE', params: [stream], id: Date.now() }));
        } else {
            wsConnect();
        }

        // Phase 2: background prepend history in chunks (official TradingView pattern)
        // Prepending via setData does NOT cause jumps (fixed in LWC PR #555)
        if (modal._infiniteUnsub) { try { modal._infiniteUnsub(); } catch(e) {} }
        modal._infiniteUnsub = setupInfiniteScroll(
            modal.chart, modal.series, modal.volSeries, sym, tf,
            () => modal.candleData,
            (newData) => {
                modal.candleData = newData;
                if (drawCtx.source === 'modal') drawCtx.candleData = newData;
            },
            null // no gapless — standard time scale like mini-charts
        );
        // History loads on-demand via infinite scroll (up to 20k candles)
        // No auto-prepend — avoids conflicts with overlays and gapless scale
        console.log(`[Modal] ${sym} loaded ${data1.length} candles, infinite scroll ready`);
    } catch (e) {
        if (e.name === 'AbortError') return; // Cancelled by new TF click — expected
        console.error('Modal chart error:', e);
        showError('Failed to load chart');
    }
}

// ---- OI Indicator (bottom pane, like TradingView) ----
const OI_PANE_HEIGHT = 0.10; // 10% of chart height for OI pane
const OI_TF_MAP = { '1m': '5m', '3m': '5m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1h', '2h': '2h', '4h': '4h', '6h': '6h', '8h': '4h', '12h': '1d', '1d': '1d' };
const OI_FMT = (v) => v >= 1e9 ? (v/1e9).toFixed(1) + 'B' : v >= 1e6 ? (v/1e6).toFixed(0) + 'M' : v.toFixed(0);

// Adjust candle + volume margins when OI pane is toggled
function adjustChartMargins(chartObj, hasOI) {
    // chartObj = { chart, series, volSeries } — works for modal, mc.charts[sym], mch.slots[i]
    if (!chartObj || !chartObj.chart) return;
    const bot = hasOI ? OI_PANE_HEIGHT + 0.02 : 0.05;
    chartObj.chart.priceScale('right').applyOptions({
        scaleMargins: { top: 0.05, bottom: bot },
    });
    if (chartObj.volSeries) {
        const volH = spGet('volumeHeight', 15) / 100;
        const volTop = hasOI ? (1 - OI_PANE_HEIGHT - 0.02 - volH) : (1 - volH);
        const volBot = hasOI ? (OI_PANE_HEIGHT + 0.02) : 0;
        try {
            chartObj.chart.priceScale('vol').applyOptions({
                scaleMargins: { top: volTop, bottom: volBot },
            });
        } catch(e) {}
    }
}

// Generic: add OI line to any chart object. chartObj must have { chart, oiSeries? }
async function applyOI(chartObj, sym, tf) {
    if (!chartObj || !chartObj.chart) return;

    // Remove existing
    if (chartObj.oiSeries) {
        try { chartObj.chart.removeSeries(chartObj.oiSeries); } catch(e) {}
        chartObj.oiSeries = null;
    }

    const enabled = spGet('indicatorOI', false);
    if (!enabled || !sym) {
        adjustChartMargins(chartObj, false);
        return;
    }

    const period = OI_TF_MAP[tf || '5m'] || '5m';

    try {
        const res = await fetch(`/api/oi-history?symbol=${sym}&period=${period}&limit=500`);
        if (!res.ok) throw new Error(`oi-history: ${res.status}`);
        const data = await res.json();
        if (!Array.isArray(data) || data.length === 0) {
            adjustChartMargins(chartObj, false);
            return;
        }

        const color = spGet('indicatorOIColor', '#eab308');

        chartObj.oiSeries = chartObj.chart.addSeries(LightweightCharts.LineSeries, {
            color: color,
            lineWidth: 1.5,
            priceScaleId: 'oi',
            priceFormat: { type: 'custom', formatter: OI_FMT },
            title: 'OI',
            lastValueVisible: true,
            priceLineVisible: false,
            crosshairMarkerVisible: true,
            crosshairMarkerRadius: 3,
        });
        chartObj.chart.priceScale('oi').applyOptions({
            scaleMargins: { top: 1 - OI_PANE_HEIGHT, bottom: 0 },
            drawTicks: false,
            borderVisible: false,
            entireTextOnly: true,
        });

        const oiData = data.map(d => ({
            time: Math.floor(d.timestamp / 1000),
            value: parseFloat(d.sumOpenInterestValue || d.sumOpenInterest || 0),
        }));
        chartObj.oiSeries.setData(oiData);
        adjustChartMargins(chartObj, true);

    } catch (e) {
        console.error('[OI] Error:', sym, e);
        adjustChartMargins(chartObj, false);
    }
}

// Wrapper for modal (backward compat)
async function applyOIOverlay(chart, sym) {
    await applyOI(modal, sym, modal.currentTF);
}

// Batch OI for mini-charts (throttled, non-blocking)
async function applyOIToBatch(symbols) {
    if (!spGet('indicatorOI', false)) return;
    for (const sym of symbols) {
        const c = mc.charts[sym];
        if (!c || !c.chart) continue;
        applyOI(c, sym, mc.globalTF); // fire-and-forget, no await to avoid blocking
        await new Promise(r => setTimeout(r, 50)); // 50ms throttle between fetches
    }
}

// drawModalLevels replaced by applyModalAutoLevels() above


// ==========================================
// Drawing Tools — Modal only
// ==========================================
const DRAW_TOOLS = [
    { id: 'cursor', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M6 3v15.5l4-4.5 3.5 6.5 2-1-3.5-6.5H18L6 3z" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>', title: 'Cursor (Esc)', key: 'Escape' },
    { id: 'alert', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="currentColor" fill-opacity="0.08"/><path d="M13.73 21a2 2 0 01-3.46 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>', title: 'Price Alert (A)', key: 'a' },
    { id: 'hline', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2"/><circle cx="6" cy="12" r="2.5" stroke="currentColor" stroke-width="1.5" fill="#1e222d"/><circle cx="18" cy="12" r="2.5" stroke="currentColor" stroke-width="1.5" fill="#1e222d"/></svg>', title: 'Horizontal Line (H)', key: 'h' },
    { id: 'ray', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="4" cy="17" r="2.5" stroke="currentColor" stroke-width="1.5" fill="#1e222d"/><line x1="6" y1="16" x2="22" y2="8" stroke="currentColor" stroke-width="2"/><path d="M19 6l3 2.5-3.5 2" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" fill="none"/></svg>', title: 'Ray (R)', key: 'r' },
    { id: 'trendline', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><line x1="4" y1="18" x2="20" y2="6" stroke="currentColor" stroke-width="2"/><circle cx="4" cy="18" r="2.5" stroke="currentColor" stroke-width="1.5" fill="#1e222d"/><circle cx="20" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5" fill="#1e222d"/></svg>', title: 'Trend Line (T)', key: 't' },
    { id: 'fib', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><line x1="3" y1="4" x2="21" y2="4" stroke="currentColor" stroke-width="1.8"/><line x1="3" y1="9.5" x2="21" y2="9.5" stroke="currentColor" stroke-width="1" opacity="0.5" stroke-dasharray="3 2"/><line x1="3" y1="14.5" x2="21" y2="14.5" stroke="currentColor" stroke-width="1" opacity="0.5" stroke-dasharray="3 2"/><line x1="3" y1="20" x2="21" y2="20" stroke="currentColor" stroke-width="1.8"/><text x="2" y="8" font-size="6" font-weight="600" fill="currentColor" opacity="0.6" font-family="sans-serif">0</text><text x="2" y="19" font-size="6" font-weight="600" fill="currentColor" opacity="0.6" font-family="sans-serif">1</text></svg>', title: 'Fibonacci (F)', key: 'f' },
    { id: 'rect', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="4" y="5" width="16" height="14" rx="1" stroke="currentColor" stroke-width="1.8" fill="currentColor" fill-opacity="0.06"/><circle cx="4" cy="5" r="2" stroke="currentColor" stroke-width="1.3" fill="#1e222d"/><circle cx="20" cy="5" r="2" stroke="currentColor" stroke-width="1.3" fill="#1e222d"/><circle cx="4" cy="19" r="2" stroke="currentColor" stroke-width="1.3" fill="#1e222d"/><circle cx="20" cy="19" r="2" stroke="currentColor" stroke-width="1.3" fill="#1e222d"/></svg>', title: 'Rectangle (B)', key: 'b' },
    { id: 'ruler', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M4 20L20 4" stroke="currentColor" stroke-width="2"/><path d="M4 20v-7" stroke="currentColor" stroke-width="1.2" opacity="0.4" stroke-dasharray="2 2"/><path d="M4 13h4" stroke="currentColor" stroke-width="1" opacity="0.35"/><path d="M4 16h3" stroke="currentColor" stroke-width="1" opacity="0.35"/><path d="M20 4h-7" stroke="currentColor" stroke-width="1.2" opacity="0.4" stroke-dasharray="2 2"/><path d="M13 4v4" stroke="currentColor" stroke-width="1" opacity="0.35"/><path d="M16 4v3" stroke="currentColor" stroke-width="1" opacity="0.35"/></svg>', title: 'Ruler / Measure (M)', key: 'm' },
    { id: 'trash', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M6 7h12l-1.2 12.5a1 1 0 01-1 .9H8.2a1 1 0 01-1-.9L6 7z" stroke="currentColor" stroke-width="1.5" fill="currentColor" fill-opacity="0.04"/><path d="M4 7h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="10" x2="10" y2="17" stroke="currentColor" stroke-width="1.2" opacity="0.4"/><line x1="14" y1="10" x2="14" y2="17" stroke="currentColor" stroke-width="1.2" opacity="0.4"/></svg>', title: 'Clear All', key: 'Delete' },
];

const DRAW_COLORS = ['#5b9cf6', '#ef4444', '#f97316', '#eab308', '#22c55e', '#a855f7', '#ec4899', '#ffffff'];
let drawIdCounter = 0;

// Persistent drawing storage (localStorage)
const drawStore = (() => {
    const KEY = 'mc_drawings';
    let _cache = null; // in-memory cache to avoid localStorage parse on every WS tick
    function loadAll() {
        if (_cache) return _cache;
        try { _cache = JSON.parse(localStorage.getItem(KEY) || '{}'); } catch(e) { _cache = {}; }
        return _cache;
    }
    function saveAll(store) {
        _cache = store;
        lsSet(KEY, JSON.stringify(store));
    }
    return {
        save(sym, drawings) {
            const store = loadAll();
            store[sym] = drawings.map(d => ({
                type: d.type, color: d.color, locked: d.locked,
                data: d.data, alert: d.alert || false
            }));
            saveAll(store);
        },
        load(sym) {
            const store = loadAll();
            return store[sym] || [];
        },
        loadAll,
        invalidateCache() { _cache = null; },
        remove(sym) {
            const store = loadAll();
            delete store[sym];
            saveAll(store);
        }
    };
})();

// ==========================================
// Price Alert System
// ==========================================
const alertState = {
    // Track last known price per symbol to detect crossings
    lastPrices: {},
    // Cooldown per alert to avoid spam (key: "sym:price", value: timestamp)
    cooldowns: {},
    COOLDOWN_MS: 60000, // 1 min cooldown per alert
};

function checkPriceAlerts(sym, currentPrice) {
    const allDrawings = drawStore.loadAll();
    const symDrawings = allDrawings[sym];
    if (!symDrawings || symDrawings.length === 0) return;

    const lastPrice = alertState.lastPrices[sym];
    alertState.lastPrices[sym] = currentPrice;
    if (lastPrice === undefined) return; // first tick, no crossing possible

    symDrawings.forEach(d => {
        if (!d.alert) return;
        if (d.type !== 'hline' && d.type !== 'ray') return;
        const alertPrice = d.data.price;
        if (!alertPrice) return;

        // Check crossing: last was below, now above (or vice versa)
        const crossedUp = lastPrice < alertPrice && currentPrice >= alertPrice;
        const crossedDown = lastPrice > alertPrice && currentPrice <= alertPrice;
        if (!crossedUp && !crossedDown) return;

        // Cooldown check
        const cooldownKey = `${sym}:${alertPrice.toFixed(8)}`;
        const now = Date.now();
        if (alertState.cooldowns[cooldownKey] && now - alertState.cooldowns[cooldownKey] < alertState.COOLDOWN_MS) return;
        alertState.cooldowns[cooldownKey] = now;

        const direction = crossedUp ? '▲ Crossed Above' : '▼ Crossed Below';
        const ticker = sym.replace('USDT', '');
        showAlertToast(sym, ticker, currentPrice, alertPrice, direction, d.color);
    });
}

function showAlertToast(sym, ticker, currentPrice, alertPrice, direction, color) {
    // Remove old toasts if too many
    const existing = document.querySelectorAll('.alert-toast');
    if (existing.length >= 5) existing[0].remove();

    const prec = getPricePrecision(currentPrice);
    const toast = document.createElement('div');
    toast.className = 'alert-toast';
    toast.style.borderLeftColor = color || '#5b9cf6';
    toast.innerHTML = `
        <div class="alert-toast-header">
            <span class="alert-toast-icon">🔔</span>
            <span class="alert-toast-sym">${ticker}/USDT</span>
            <button class="alert-toast-close">&times;</button>
        </div>
        <div class="alert-toast-body">
            <div class="alert-toast-dir" style="color:${direction.includes('Above') ? '#22c55e' : '#ef4444'}">${direction}</div>
            <div class="alert-toast-price">Level: <b>$${alertPrice.toFixed(prec)}</b></div>
            <div class="alert-toast-current">Price: <b>$${currentPrice.toFixed(prec)}</b></div>
        </div>
    `;

    // Click toast → open modal
    toast.addEventListener('click', (e) => {
        if (e.target.closest('.alert-toast-close')) {
            toast.remove();
            return;
        }
        toast.remove();
        openCoinModal(sym);
    });

    // Close button
    toast.querySelector('.alert-toast-close').addEventListener('click', (e) => {
        e.stopPropagation();
        toast.remove();
    });

    document.body.appendChild(toast);

    // Auto-remove after 10s
    setTimeout(() => { if (toast.parentNode) toast.remove(); }, 10000);

    // Also try browser Notification API
    if (Notification.permission === 'granted') {
        try {
            new Notification(`${ticker}/USDT — ${direction}`, {
                body: `Level: $${alertPrice.toFixed(prec)} | Price: $${currentPrice.toFixed(prec)}`,
                icon: '🔔',
                tag: `alert-${sym}-${alertPrice}`,
            });
        } catch(e) {}
    }
}

// ==========================================
// Price Alert Store + UI (TradingView-style)
// ==========================================
const priceAlertStore = (() => {
    const KEY = 'fs_price_alerts';
    let _cache = null;
    let _serverAlerts = []; // from GET /api/alerts

    function loadLocal() {
        if (_cache) return _cache;
        try { _cache = JSON.parse(localStorage.getItem(KEY) || '[]'); } catch { _cache = []; }
        return _cache;
    }
    function saveLocal(alerts) {
        _cache = alerts;
        lsSet(KEY, JSON.stringify(alerts));
    }
    function nextLocalId() {
        return 'local_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
    }

    return {
        getAll() {
            const local = loadLocal();
            // Merge: server alerts + local-only alerts (no server id)
            const serverIds = new Set(_serverAlerts.map(a => a.id));
            const localOnly = local.filter(a => !a.serverId || !serverIds.has(a.serverId));
            return [..._serverAlerts.map(a => ({ ...a, source: 'server' })), ...localOnly.map(a => ({ ...a, source: 'local' }))];
        },
        getBySymbol(sym) {
            return this.getAll().filter(a => a.symbol === sym);
        },
        async add(sym, price, direction = 'crosses') {
            const alert = { id: nextLocalId(), symbol: sym, price, direction, enabled: true, createdAt: Date.now() };

            // Try save to server first
            if (typeof authUI !== 'undefined' && authUI.isLoggedIn()) {
                try {
                    const res = await authUI.authFetch('/api/alerts', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ type: 'price', symbol: sym, condition: { price, direction }, cooldown_sec: 300 })
                    });
                    const data = await res.json();
                    if (data.success && data.id) {
                        alert.serverId = data.id;
                        alert.source = 'server';
                    }
                } catch (e) { console.warn('[PriceAlerts] Server save failed:', e.message); }
            }

            // Always save locally too
            const local = loadLocal();
            local.push(alert);
            saveLocal(local);
            return alert;
        },
        async remove(alertId) {
            // Find and remove
            const all = this.getAll();
            const alert = all.find(a => a.id === alertId || a.serverId === alertId);
            if (!alert) return;

            // Remove from server
            const sId = alert.serverId || alert.id;
            if (alert.source === 'server' && typeof authUI !== 'undefined' && authUI.isLoggedIn()) {
                try {
                    await authUI.authFetch(`/api/alerts/${sId}`, { method: 'DELETE' });
                } catch (e) { console.warn('[PriceAlerts] Server delete failed:', e.message); }
            }

            // Remove from local
            const local = loadLocal();
            const idx = local.findIndex(a => a.id === alert.id || a.serverId === sId);
            if (idx !== -1) { local.splice(idx, 1); saveLocal(local); }

            // Remove from server cache
            const sIdx = _serverAlerts.findIndex(a => a.id === sId);
            if (sIdx !== -1) _serverAlerts.splice(sIdx, 1);
        },
        setServerAlerts(alerts) {
            _serverAlerts = alerts.filter(a => a.type === 'price').map(a => ({
                id: a.id, serverId: a.id, symbol: a.symbol,
                price: a.condition?.price, direction: a.condition?.direction || 'crosses',
                enabled: a.enabled, cooldown_sec: a.cooldown_sec
            }));
        },
        async loadFromServer() {
            if (typeof authUI === 'undefined' || !authUI.isLoggedIn()) return;
            try {
                const res = await authUI.authFetch('/api/alerts');
                const data = await res.json();
                if (data.success && data.alerts) {
                    this.setServerAlerts(data.alerts);
                    // Cleanup stale local alerts: if local has serverId not on server → remove
                    const serverIds = new Set(_serverAlerts.map(a => a.id));
                    const local = loadLocal();
                    const cleaned = local.filter(a => !a.serverId || serverIds.has(a.serverId));
                    if (cleaned.length !== local.length) {
                        console.log(`[PriceAlerts] Cleaned ${local.length - cleaned.length} stale local alert(s)`);
                        saveLocal(cleaned);
                    }
                }
            } catch (e) { console.warn('[PriceAlerts] Load from server failed:', e.message); }
        },
        invalidateCache() { _cache = null; }
    };
})();

// Render alert price lines on a chart
const _alertPriceLines = new Map(); // key: "sym:alertId" → priceLine object

function applyAlertLinesToChart(sym, series) {
    if (!series) return;
    // Remove existing alert lines for this symbol
    for (const [key, line] of _alertPriceLines) {
        if (key.startsWith(sym + ':')) {
            try { series.removePriceLine(line); } catch {}
            _alertPriceLines.delete(key);
        }
    }
    // Add current alerts
    const alerts = priceAlertStore.getBySymbol(sym);
    for (const a of alerts) {
        if (!a.price || !a.enabled) continue;
        try {
            const pl = series.createPriceLine({
                price: a.price,
                color: '#eab308',
                lineWidth: 1,
                lineStyle: 2, // dashed
                axisLabelVisible: false,
                title: '',
            });
            _alertPriceLines.set(`${sym}:${a.id || a.serverId}`, pl);
        } catch {}
    }
}

function removeAlertLineFromChart(sym, alertId, series) {
    const key = `${sym}:${alertId}`;
    const line = _alertPriceLines.get(key);
    if (line && series) {
        try { series.removePriceLine(line); } catch {}
        _alertPriceLines.delete(key);
    }
}

// Context menu for adding alerts
let _alertCtxEl = null;

function showAlertContextMenu(x, y, price, sym) {
    closeAlertContextMenu();
    const prec = getPricePrecision(price);

    const menu = document.createElement('div');
    menu.className = 'alert-ctx-menu';
    menu.style.left = Math.min(x, window.innerWidth - 240) + 'px';
    menu.style.top = Math.min(y, window.innerHeight - 200) + 'px';
    menu.innerHTML = `
        <label>Price Alert — ${sym.replace('USDT', '/USDT')}</label>
        <input type="number" step="any" value="${price.toFixed(prec)}" id="alertPriceInput" />
        <select id="alertDirSelect">
            <option value="crosses">↕ Crosses</option>
            <option value="crosses_above">▲ Crosses Above</option>
            <option value="crosses_below">▼ Crosses Below</option>
        </select>
        <div class="alert-ctx-actions">
            <button class="alert-ctx-add">🔔 Add Alert</button>
            <button class="alert-ctx-cancel">Cancel</button>
        </div>
    `;

    // Auto-select direction based on current price
    const currentPrice = alertState.lastPrices[sym];
    if (currentPrice) {
        const dir = price > currentPrice ? 'crosses_above' : 'crosses_below';
        menu.querySelector('#alertDirSelect').value = dir;
    }

    menu.querySelector('.alert-ctx-add').onclick = async () => {
        const inputPrice = parseFloat(menu.querySelector('#alertPriceInput').value);
        const direction = menu.querySelector('#alertDirSelect').value;
        if (!inputPrice || isNaN(inputPrice)) return;

        const alert = await priceAlertStore.add(sym, inputPrice, direction);
        // Apply visual line
        const series = modal.series || (mc.charts[sym] && mc.charts[sym].series);
        if (series) applyAlertLinesToChart(sym, series);

        closeAlertContextMenu();
        // Show confirmation toast
        showAlertToast(sym, sym.replace('USDT', ''), inputPrice, inputPrice,
            '🔔 Alert Set: ' + (direction === 'crosses' ? '↕ Crosses' : direction === 'crosses_above' ? '▲ Above' : '▼ Below'),
            '#eab308');
    };
    menu.querySelector('.alert-ctx-cancel').onclick = () => closeAlertContextMenu();

    // Close on click outside
    menu.addEventListener('mousedown', e => e.stopPropagation());
    menu.addEventListener('click', e => e.stopPropagation());
    document.addEventListener('mousedown', closeAlertContextMenu, { once: true });

    document.body.appendChild(menu);
    _alertCtxEl = menu;
    menu.querySelector('#alertPriceInput').focus();
    menu.querySelector('#alertPriceInput').select();
}

function closeAlertContextMenu() {
    if (_alertCtxEl && _alertCtxEl.parentNode) _alertCtxEl.remove();
    _alertCtxEl = null;
}

// Alert panel (list of alerts for current symbol)
let _alertPanelEl = null;

function toggleAlertPanel(sym, chartEl) {
    if (_alertPanelEl) { _alertPanelEl.remove(); _alertPanelEl = null; return; }

    const panel = document.createElement('div');
    panel.className = 'alert-panel';

    function renderPanel() {
        const alerts = priceAlertStore.getBySymbol(sym);
        if (!alerts.length) {
            panel.innerHTML = `<div class="alert-panel-title">🔔 Alerts — ${sym.replace('USDT', '')}</div>
                <div class="alert-panel-empty">No alerts set.<br>Right-click price scale to add.</div>`;
            return;
        }
        const prec = getPricePrecision(alerts[0].price || 1);
        panel.innerHTML = `<div class="alert-panel-title">🔔 Alerts — ${sym.replace('USDT', '')} (${alerts.length})</div>` +
            alerts.map(a => {
                const dirIcon = a.direction === 'crosses_above' ? '▲' : a.direction === 'crosses_below' ? '▼' : '↕';
                return `<div class="alert-panel-item" data-id="${a.id || a.serverId}">
                    <span class="ap-bell">🔔</span>
                    <span class="ap-price">$${(a.price || 0).toFixed(prec)}</span>
                    <span class="ap-dir">${dirIcon}</span>
                    <span class="ap-del" title="Delete alert">✕</span>
                </div>`;
            }).join('');

        panel.querySelectorAll('.ap-del').forEach(btn => {
            btn.onclick = async (e) => {
                e.stopPropagation();
                const id = btn.closest('.alert-panel-item').dataset.id;
                await priceAlertStore.remove(id.startsWith('local_') ? id : Number(id));
                const series = modal.series || (mc.charts[sym] && mc.charts[sym].series);
                if (series) applyAlertLinesToChart(sym, series);
                renderPanel();
            };
        });
    }

    renderPanel();
    panel.addEventListener('mousedown', e => e.stopPropagation());
    chartEl.appendChild(panel);
    _alertPanelEl = panel;

    // Close on next click outside
    setTimeout(() => {
        document.addEventListener('mousedown', function handler(e) {
            if (_alertPanelEl && !_alertPanelEl.contains(e.target)) {
                _alertPanelEl.remove();
                _alertPanelEl = null;
                document.removeEventListener('mousedown', handler);
            }
        });
    }, 100);
}

// Check dedicated price alerts (extends existing checkPriceAlerts)
function checkDedicatedPriceAlerts(sym, currentPrice) {
    const alerts = priceAlertStore.getBySymbol(sym);
    if (!alerts.length) return;

    const lastPrice = alertState.lastPrices[sym];
    if (lastPrice === undefined) return;

    for (const a of alerts) {
        if (!a.price || !a.enabled) continue;

        const crossedUp = lastPrice < a.price && currentPrice >= a.price;
        const crossedDown = lastPrice > a.price && currentPrice <= a.price;
        if (!crossedUp && !crossedDown) continue;

        const matchesDir =
            a.direction === 'crosses' ||
            (a.direction === 'crosses_above' && crossedUp) ||
            (a.direction === 'crosses_below' && crossedDown);
        if (!matchesDir) continue;

        // Cooldown
        const cKey = `pa:${a.id}`;
        const now = Date.now();
        if (alertState.cooldowns[cKey] && now - alertState.cooldowns[cKey] < (a.cooldown_sec || 60) * 1000) continue;
        alertState.cooldowns[cKey] = now;

        const direction = crossedUp ? '▲ Crossed Above' : '▼ Crossed Below';
        const ticker = sym.replace('USDT', '');
        showAlertToast(sym, ticker, currentPrice, a.price, direction, '#eab308');
    }
}

// Fibonacci levels config (customizable, persisted in localStorage)
// Format: [{level: 0, color: '#4caf50'}, ...] — each level has its own color
const FIB_DEFAULT_COLORS = ['#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3', '#9c27b0', '#787b86', '#e91e63', '#00bcd4', '#8bc34a'];
const FIB_DEFAULTS_OBJ = [
    { level: 0, color: '#4caf50' },
    { level: 0.236, color: '#2196f3' },
    { level: 0.382, color: '#ff9800' },
    { level: 0.5, color: '#f59e0b' },
    { level: 0.618, color: '#ff9800' },
    { level: 0.786, color: '#2196f3' },
    { level: 1, color: '#4caf50' },
];
const fibConfig = (() => {
    const KEY = 'mc_fib_levels';
    function load() {
        try {
            const saved = JSON.parse(localStorage.getItem(KEY));
            if (Array.isArray(saved) && saved.length > 0) {
                // Migration: old format was plain numbers → convert to {level, color}
                if (typeof saved[0] === 'number') {
                    return saved.map((lvl, i) => ({ level: lvl, color: FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length] }));
                }
                return saved;
            }
        } catch(e) {}
        return FIB_DEFAULTS_OBJ.map(o => ({ ...o }));
    }
    function save(levels) {
        lsSet(KEY, JSON.stringify(levels));
    }
    // Helper: extract plain level numbers from config
    function levels(cfg) { return (cfg || load()).map(o => typeof o === 'number' ? o : o.level); }
    function colorAt(cfg, i) { const c = cfg || load(); const item = c[i]; return (item && item.color) || FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length]; }
    return { load, save, levels, colorAt };
})();

let drawMagnet = localStorage.getItem('fs_magnet') !== 'false'; // default ON

const draw = {
    activeTool: 'cursor',
    clickCount: 0,
    startPrice: 0,
    startTime: 0,
    tempLine: null,
    drawings: [],    // { id, type, color, locked, priceLine/lineSeries/fibLines, data }
    overlay: null,   // canvas overlay for live preview
    selected: null,  // selected drawing id
    dragging: false, // drag in progress
    dragMode: 'move', // 'move' | 'resize'
    dragAnchorIdx: -1, // which anchor point is being resized
    justDragged: false, // suppress click after drag
    dragStartY: 0,
    dragStartPrice: 0,
    dragStartTime: 0,
    dragOrigData: null,  // snapshot of d.data at drag start
};

// Drawing Context — points to the active chart for drawing tools
// All drawing functions use drawCtx instead of hardcoded modal.*
// Switched when user activates a multi-chart slot or opens modal
const drawCtx = {
    chart: null,     // LightweightCharts instance
    series: null,    // main candle series
    candleData: null,// array of OHLCV
    chartEl: null,   // DOM element containing the chart
    sym: null,       // current symbol
    tf: null,        // current timeframe
    source: 'modal', // 'modal' | 'slot:0' | 'slot:1' etc
};

function setDrawCtx(source, chart, series, candleData, chartEl, sym, tf) {
    drawCtx.chart = chart;
    drawCtx.series = series;
    drawCtx.candleData = candleData;
    drawCtx.chartEl = chartEl;
    drawCtx.sym = sym;
    drawCtx.tf = tf;
    drawCtx.source = source;
}

function setDrawCtxModal() {
    setDrawCtx('modal', modal.chart, modal.series, modal.candleData, el('cmChartBody'), modal.currentSym, modal.currentTF);
}

function setDrawCtxSlot(slotIndex) {
    const slot = mch.slots[slotIndex];
    if (!slot || !slot.chart) return;
    const chartEl = el('mch-chart-' + slotIndex);
    setDrawCtx('slot:' + slotIndex, slot.chart, slot.series, slot.candleData || null, chartEl, slot.sym, slot.tf);
}

function setDrawCtxMini(sym) {
    const c = mc.charts[sym];
    if (!c || !c.chart) return;
    const chartEl = el('mc-body-' + sym);
    setDrawCtx('mini:' + sym, c.chart, c.series, c.candleData || null, chartEl, sym, mc.globalTF);
}

function renderDrawToolbar(targetEl) {
    const chartEl = targetEl || drawCtx.chartEl || el('cmChartBody');
    if (!chartEl) return;

    // Remove old toolbar if exists
    const old = chartEl.querySelector('.dt-tools');
    if (old) old.remove();

    const container = document.createElement('div');
    container.className = 'dt-tools';
    container.innerHTML = DRAW_TOOLS.map(t => {
        const active = draw.activeTool === t.id ? ' dt-active' : '';
        return `<button class="dt-btn${active}" data-tool="${t.id}" title="${t.title}">${t.icon}</button>`;
    }).join('') + `<div class="dt-divider"></div><button class="dt-btn dt-magnet${drawMagnet ? ' dt-active' : ''}" data-tool="magnet" title="Magnet (snap to OHLC)"><svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M5 4h4v8a3 3 0 006 0V4h4v8a7 7 0 01-14 0V4z" stroke="currentColor" stroke-width="1.6" fill="currentColor" fill-opacity="0.08"/><rect x="4.5" y="2" width="5" height="3.5" rx="0.8" fill="currentColor" opacity="0.25"/><rect x="14.5" y="2" width="5" height="3.5" rx="0.8" fill="currentColor" opacity="0.25"/></svg></button>`;

    chartEl.appendChild(container);

    // Magnet toggle
    const magnetBtn = container.querySelector('.dt-magnet');
    if (magnetBtn) {
        magnetBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            drawMagnet = !drawMagnet;
            lsSet('fs_magnet', drawMagnet);
            magnetBtn.classList.toggle('dt-active', drawMagnet);
        });
    }

    container.querySelectorAll('.dt-btn:not(.dt-magnet)').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const tool = btn.dataset.tool;
            if (tool === 'trash') {
                // Library drawings + custom drawings
                if (typeof DM !== 'undefined' && DM && DM.isActive()) DM.clearAll();
                clearAllDrawings();
                return;
            }
            draw.activeTool = tool;
            draw.clickCount = 0;
            removePreviewOverlay();
            if (tool !== 'ruler') removeRulerMeasurement();
            // Route to library DrawingManager if available (hline/ray/trendline/fib/rect)
            if (typeof DM !== 'undefined' && DM && DM.isActive() && tool !== 'ruler' && tool !== 'alert') {
                DM.setTool(tool);
            }
            renderDrawToolbar();
            updateModalCursor();
        });
    });
}

function updateModalCursor() {
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    if (!chartEl) return;
    const chart = drawCtx.chart || modal.chart;
    if (draw.activeTool === 'cursor') {
        chartEl.style.cursor = '';
        chartEl.style.touchAction = '';
        if (chart) {
            chart.applyOptions({
                handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: true },
                handleScale: { mouseWheel: true, pinch: true, axisPressedMouseMove: { price: true, time: true }, axisDoubleClickReset: { price: true, time: true } },
            });
        }
    } else {
        chartEl.style.cursor = 'crosshair';
        chartEl.style.touchAction = 'none';
        if (chart) {
            chart.applyOptions({
                handleScroll: { mouseWheel: false, pressedMouseMove: false, vertTouchDrag: false, horzTouchDrag: false },
                handleScale: { mouseWheel: false, pinch: false, axisPressedMouseMove: false },
            });
        }
    }
}

// Keyboard shortcuts for tools
document.addEventListener('keydown', (e) => {
    if (!drawCtx.chart && !modal.chart) return;
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
    const tool = DRAW_TOOLS.find(t => t.key && t.key.toLowerCase() === e.key.toLowerCase());
    if (tool) {
        if (tool.id === 'Escape') {
            draw.activeTool = 'cursor';
        } else {
            draw.activeTool = tool.id;
        }
        draw.clickCount = 0;
        removePreviewOverlay();
        // Route to library DrawingManager
        if (typeof DM !== 'undefined' && DM && DM.isActive() && draw.activeTool !== 'ruler') {
            DM.setTool(draw.activeTool);
        }
        renderDrawToolbar();
        updateModalCursor();
    }
    if (e.key === 'Delete' && (drawCtx.chart || modal.chart)) {
        // Try library first, then custom
        if (typeof DM !== 'undefined' && DM && DM.isActive()) DM.deleteSelected();
        if (draw.selected !== null) {
            deleteDrawing(draw.selected);
        } else {
            clearAllDrawings();
        }
    }
});

function clearAllDrawings() {
    draw.drawings.forEach(d => removeDrawingFromChart(d));
    draw.drawings = [];
    draw.clickCount = 0;
    draw.selected = null;
    hideDrawingPanel();
    removePreviewOverlay();
    removeRulerMeasurement();
    persistDrawings();
}

function deleteDrawing(id) {
    const idx = draw.drawings.findIndex(d => d.id === id);
    if (idx === -1) return;
    const d = draw.drawings[idx];
    removeDrawingFromChart(d);
    draw.drawings.splice(idx, 1);
    if (draw.selected === id) {
        draw.selected = null;
        hideDrawingPanel();
    }
    persistDrawings();
}

function removeDrawingFromChart(d) {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (d.priceLine && _s) {
        try { _s.removePriceLine(d.priceLine); } catch(e) {}
    }
    if (d.lineSeries && _c) {
        try { _c.removeSeries(d.lineSeries); } catch(e) {}
    }
    if (d.fibLines && _s) {
        d.fibLines.forEach(fl => {
            try { _s.removePriceLine(fl); } catch(e) {}
        });
    }
    if (d.rectLines && _c) {
        d.rectLines.forEach(ls => {
            try { _c.removeSeries(ls); } catch(e) {}
        });
    }
    if (d.fillSeries && _c) {
        try { _c.removeSeries(d.fillSeries); } catch(e) {}
    }
    if (d.bottomPriceLine && _s) {
        try { _s.removePriceLine(d.bottomPriceLine); } catch(e) {}
    }
}

// ---- Selection overlay (anchor handles + highlight) ----
let _selOverlay = null;   // canvas element
let _selUnsub = null;     // unsubscribe from chart range changes

function renderSelectionOverlay() {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    if (!_selOverlay || !_s || !_c || !chartEl) return;
    const d = draw.drawings.find(dd => dd.id === draw.selected);
    if (!d || !d.data) { removeSelectionOverlay(); return; }

    const w = chartEl.clientWidth, h = chartEl.clientHeight;
    _selOverlay.width = w;
    _selOverlay.height = h;
    const ctx = _selOverlay.getContext('2d');
    ctx.clearRect(0, 0, w, h);

    const clr = d.color || '#5b9cf6';
    const anchors = []; // [{x, y}] — collect anchor points for circles

    if (d.type === 'hline') {
        const ly = _s.priceToCoordinate(d.data.price);
        if (ly === null) return;
        // Highlight line
        ctx.strokeStyle = clr; ctx.lineWidth = 3; ctx.globalAlpha = 0.4;
        ctx.beginPath(); ctx.moveTo(0, ly); ctx.lineTo(w, ly); ctx.stroke();
        ctx.globalAlpha = 1;
        anchors.push({ x: 40, y: ly }, { x: w - 40, y: ly });
    }
    if (d.type === 'ray') {
        const ly = _s.priceToCoordinate(d.data.price);
        const sx = _c.timeScale().timeToCoordinate(d.data.startTime);
        if (ly === null) return;
        const startX = sx !== null ? sx : 0;
        ctx.strokeStyle = clr; ctx.lineWidth = 3; ctx.globalAlpha = 0.4;
        ctx.beginPath(); ctx.moveTo(startX, ly); ctx.lineTo(w, ly); ctx.stroke();
        ctx.globalAlpha = 1;
        anchors.push({ x: startX, y: ly }, { x: w - 20, y: ly });
    }
    if (d.type === 'trendline') {
        const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
        const y1 = _s.priceToCoordinate(d.data.p1);
        const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
        const y2 = _s.priceToCoordinate(d.data.p2);
        if (x1 === null || y1 === null || x2 === null || y2 === null) return;
        ctx.strokeStyle = clr; ctx.lineWidth = 3; ctx.globalAlpha = 0.4;
        ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
        ctx.globalAlpha = 1;
        anchors.push({ x: x1, y: y1 }, { x: x2, y: y2 });
    }
    if (d.type === 'rect') {
        const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
        const y1 = _s.priceToCoordinate(d.data.p1);
        const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
        const y2 = _s.priceToCoordinate(d.data.p2);
        if (x1 === null || y1 === null || x2 === null || y2 === null) return;
        const xMin = Math.min(x1, x2), xMax = Math.max(x1, x2);
        const yMin = Math.min(y1, y2), yMax = Math.max(y1, y2);
        ctx.strokeStyle = clr; ctx.lineWidth = 2.5; ctx.globalAlpha = 0.4;
        ctx.strokeRect(xMin, yMin, xMax - xMin, yMax - yMin);
        ctx.globalAlpha = 1;
        anchors.push({ x: xMin, y: yMin }, { x: xMax, y: yMin }, { x: xMin, y: yMax }, { x: xMax, y: yMax });
    }
    if (d.type === 'fib') {
        const diff = d.data.p2 - d.data.p1;
        const rawLevels = d.data.levels || fibConfig.load();
        for (const item of rawLevels) {
            const lvl = typeof item === 'number' ? item : item.level;
            const fibPrice = d.data.p1 + diff * lvl;
            const ly = _s.priceToCoordinate(fibPrice);
            if (ly === null) continue;
            ctx.strokeStyle = clr; ctx.lineWidth = 2; ctx.globalAlpha = 0.3;
            ctx.beginPath(); ctx.moveTo(0, ly); ctx.lineTo(w, ly); ctx.stroke();
            ctx.globalAlpha = 1;
        }
        const y1 = _s.priceToCoordinate(d.data.p1);
        const y2 = _s.priceToCoordinate(d.data.p2);
        if (y1 !== null) anchors.push({ x: 40, y: y1 });
        if (y2 !== null) anchors.push({ x: 40, y: y2 });
    }

    // Draw anchor circles
    for (const a of anchors) {
        ctx.fillStyle = '#fff'; ctx.strokeStyle = clr; ctx.lineWidth = 2;
        ctx.beginPath(); ctx.arc(a.x, a.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
    }
}

function showSelectionOverlay() {
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    const _c = drawCtx.chart || modal.chart;
    if (!chartEl || !_c) return;
    removeSelectionOverlay();
    const canvas = document.createElement('canvas');
    canvas.className = 'sel-overlay';
    canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:55;';
    chartEl.appendChild(canvas);
    _selOverlay = canvas;
    renderSelectionOverlay();
    // Re-render on zoom/scroll
    try {
        const unsub = _c.timeScale().subscribeVisibleLogicalRangeChange(() => {
            if (_selOverlay) renderSelectionOverlay();
        });
        _selUnsub = unsub;
    } catch (_) {}
}

function removeSelectionOverlay() {
    if (_selUnsub) { try { _selUnsub(); } catch (_) {} _selUnsub = null; }
    if (_selOverlay) { _selOverlay.remove(); _selOverlay = null; }
}

function selectDrawing(id) {
    draw.selected = id;
    const d = draw.drawings.find(dd => dd.id === id);
    if (!d) return;
    showDrawingPanel(d);
    showSelectionOverlay();
}

function deselectDrawing() {
    draw.selected = null;
    hideDrawingPanel();
    removeSelectionOverlay();
}

function showDrawingPanel(d) {
    hideDrawingPanel();
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    if (!chartEl) return;

    const panel = document.createElement('div');
    panel.id = 'drawPanel';
    panel.className = 'draw-panel';

    // Stop ALL events on the panel from reaching chart handlers
    panel.addEventListener('click', (e) => e.stopPropagation());
    panel.addEventListener('mousedown', (e) => e.stopPropagation());
    panel.addEventListener('mouseup', (e) => e.stopPropagation());
    panel.addEventListener('touchstart', (e) => e.stopPropagation());
    panel.addEventListener('touchend', (e) => e.stopPropagation());

    // Color dots
    const colorsHtml = DRAW_COLORS.map(c => {
        const sel = c === d.color ? ' draw-color-active' : '';
        return `<div class="draw-color-dot${sel}" data-color="${c}" style="background:${c};"></div>`;
    }).join('');

    const lockIcon = d.locked ? '🔒' : '🔓';
    const fibBtn = d.type === 'fib' ? `<button class="draw-panel-btn" data-action="fib-settings" title="Fib Levels">&#9881;</button>` : '';
    const alertBtn = (d.type === 'hline' || d.type === 'ray')
        ? `<button class="draw-panel-btn${d.alert ? ' draw-alert-active' : ''}" data-action="alert" title="${d.alert ? 'Disable Alert' : 'Enable Alert'}">🔔</button>`
        : '';
    panel.innerHTML = `
        <div class="draw-panel-colors">${colorsHtml}</div>
        ${alertBtn}
        ${fibBtn}
        <button class="draw-panel-btn" data-action="lock" title="${d.locked ? 'Unlock' : 'Lock'}">${lockIcon}</button>
        <button class="draw-panel-btn draw-panel-delete" data-action="delete" title="Delete">&#10005;</button>
    `;

    chartEl.appendChild(panel);

    // Color click
    panel.querySelectorAll('.draw-color-dot').forEach(dot => {
        dot.addEventListener('click', () => changeDrawingColor(d.id, dot.dataset.color));
    });

    // Lock
    panel.querySelector('[data-action="lock"]').addEventListener('click', () => {
        d.locked = !d.locked;
        persistDrawings();
        showDrawingPanel(d);
    });

    // Delete
    panel.querySelector('[data-action="delete"]').addEventListener('click', () => deleteDrawing(d.id));

    // Alert toggle
    const alertToggle = panel.querySelector('[data-action="alert"]');
    if (alertToggle) {
        alertToggle.addEventListener('click', () => {
            d.alert = !d.alert;
            persistDrawings();
            showDrawingPanel(d); // refresh
            // Request browser notification permission
            if (d.alert && Notification.permission === 'default') {
                Notification.requestPermission();
            }
        });
    }

    // Fib settings
    const fibSettingsBtn = panel.querySelector('[data-action="fib-settings"]');
    if (fibSettingsBtn) {
        fibSettingsBtn.addEventListener('click', () => showFibLevelsPopup(d));
    }
}

function showFibColorPicker(dotEl) {
    // Remove old picker if exists
    const oldPicker = document.getElementById('fibColorPicker');
    if (oldPicker) oldPicker.remove();

    const colors = ['#4caf50', '#2196f3', '#ff9800', '#f59e0b', '#f44336', '#9c27b0', '#e91e63', '#00bcd4', '#8bc34a', '#787b86', '#ffffff', '#5b9cf6'];
    const picker = document.createElement('div');
    picker.id = 'fibColorPicker';
    picker.className = 'fib-color-picker';

    // Position relative to dot
    const dotRect = dotEl.getBoundingClientRect();
    const popupEl = dotEl.closest('.fib-levels-popup');
    const popupRect = popupEl.getBoundingClientRect();
    picker.style.top = (dotRect.top - popupRect.top + dotRect.height + 4) + 'px';
    picker.style.left = (dotRect.left - popupRect.left) + 'px';

    picker.innerHTML = colors.map(c =>
        `<span class="fib-color-opt${c === dotEl.dataset.color ? ' active' : ''}" data-c="${c}" style="background:${c}"></span>`
    ).join('');

    picker.addEventListener('click', (e) => {
        e.stopPropagation();
        const opt = e.target.closest('.fib-color-opt');
        if (!opt) return;
        const newColor = opt.dataset.c;
        dotEl.style.background = newColor;
        dotEl.dataset.color = newColor;
        picker.remove();
    });
    picker.addEventListener('mousedown', (e) => e.stopPropagation());

    popupEl.appendChild(picker);

    // Close on outside click
    const closeHandler = (ev) => {
        if (!picker.contains(ev.target) && ev.target !== dotEl) {
            picker.remove();
            document.removeEventListener('click', closeHandler, true);
        }
    };
    setTimeout(() => document.addEventListener('click', closeHandler, true), 10);
}

function showFibLevelsPopup(d) {
    // Remove old popup if exists
    const oldPopup = document.getElementById('fibLevelsPopup');
    if (oldPopup) oldPopup.remove();

    const chartEl = el('cmChartBody');
    if (!chartEl) return;

    const rawLevels = d.data.levels || fibConfig.load();
    // Normalize: support both old format (number[]) and new ({level,color}[])
    const levels = rawLevels.map((item, i) => {
        if (typeof item === 'number') return { level: item, color: FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length] };
        return { level: item.level, color: item.color || FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length] };
    });

    const popup = document.createElement('div');
    popup.id = 'fibLevelsPopup';
    popup.className = 'fib-levels-popup';
    popup.innerHTML = `
        <div class="fib-popup-title">Fibonacci Levels</div>
        <div class="fib-popup-list" id="fibLevelsList">
            ${levels.map((item, i) => `
                <div class="fib-level-row" data-idx="${i}">
                    <span class="fib-level-color" style="background:${item.color}" data-color="${item.color}" title="Change color"></span>
                    <input type="number" class="fib-level-input" value="${(item.level * 100).toFixed(1)}" step="0.1" />
                    <span class="fib-level-pct">%</span>
                    <button class="fib-level-remove" title="Remove">×</button>
                </div>
            `).join('')}
        </div>
        <div class="fib-popup-actions">
            <button class="fib-popup-btn fib-add-btn" id="fibAddLevel">+ Add</button>
            <button class="fib-popup-btn fib-reset-btn" id="fibResetLevels">Reset</button>
            <button class="fib-popup-btn fib-apply-btn" id="fibApplyLevels">Apply</button>
        </div>
    `;
    // Block all events from reaching chart handlers
    popup.addEventListener('click', (e) => e.stopPropagation());
    popup.addEventListener('mousedown', (e) => e.stopPropagation());
    popup.addEventListener('mouseup', (e) => e.stopPropagation());
    popup.addEventListener('touchstart', (e) => e.stopPropagation());
    popup.addEventListener('touchend', (e) => e.stopPropagation());

    chartEl.appendChild(popup);

    // Add level
    popup.querySelector('#fibAddLevel').addEventListener('click', (e) => {
        e.stopPropagation();
        const list = popup.querySelector('#fibLevelsList');
        const idx = list.children.length;
        const defColor = FIB_DEFAULT_COLORS[idx % FIB_DEFAULT_COLORS.length];
        const row = document.createElement('div');
        row.className = 'fib-level-row';
        row.dataset.idx = idx;
        row.innerHTML = `
            <span class="fib-level-color" style="background:${defColor}" data-color="${defColor}" title="Change color"></span>
            <input type="number" class="fib-level-input" value="50.0" step="0.1" />
            <span class="fib-level-pct">%</span>
            <button class="fib-level-remove" title="Remove">×</button>
        `;
        list.appendChild(row);
        row.querySelector('.fib-level-remove').addEventListener('click', (ev) => {
            ev.stopPropagation();
            row.remove();
        });
        row.querySelector('.fib-level-color').addEventListener('click', (ev) => {
            ev.stopPropagation();
            showFibColorPicker(ev.target);
        });
    });

    // Remove level buttons
    popup.querySelectorAll('.fib-level-remove').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            btn.closest('.fib-level-row').remove();
        });
    });

    // Color picker on each dot
    popup.querySelectorAll('.fib-level-color').forEach(dot => {
        dot.addEventListener('click', (e) => {
            e.stopPropagation();
            showFibColorPicker(dot);
        });
    });

    // Reset
    popup.querySelector('#fibResetLevels').addEventListener('click', (e) => {
        e.stopPropagation();
        const list = popup.querySelector('#fibLevelsList');
        list.innerHTML = FIB_DEFAULTS_OBJ.map((item, i) => `
            <div class="fib-level-row" data-idx="${i}">
                <span class="fib-level-color" style="background:${item.color}" data-color="${item.color}" title="Change color"></span>
                <input type="number" class="fib-level-input" value="${(item.level * 100).toFixed(1)}" step="0.1" />
                <span class="fib-level-pct">%</span>
                <button class="fib-level-remove" title="Remove">×</button>
            </div>
        `).join('');
        list.querySelectorAll('.fib-level-remove').forEach(btn => {
            btn.addEventListener('click', (ev) => {
                ev.stopPropagation();
                btn.closest('.fib-level-row').remove();
            });
        });
        list.querySelectorAll('.fib-level-color').forEach(dot => {
            dot.addEventListener('click', (ev) => {
                ev.stopPropagation();
                showFibColorPicker(dot);
            });
        });
    });

    // Apply
    popup.querySelector('#fibApplyLevels').addEventListener('click', (e) => {
        e.stopPropagation();
        const rows = popup.querySelectorAll('.fib-level-row');
        const newLevels = [];
        rows.forEach(row => {
            const inp = row.querySelector('.fib-level-input');
            const colorDot = row.querySelector('.fib-level-color');
            const val = parseFloat(inp.value);
            if (!isNaN(val)) {
                newLevels.push({
                    level: val / 100,
                    color: colorDot ? colorDot.dataset.color : FIB_DEFAULT_COLORS[newLevels.length % FIB_DEFAULT_COLORS.length]
                });
            }
        });
        newLevels.sort((a, b) => a.level - b.level);

        // Save globally
        fibConfig.save(newLevels);

        // Update this drawing
        const _s2 = drawCtx.series || modal.series;
        if (d.fibLines && _s2) {
            d.fibLines.forEach(fl => {
                try { _s2.removePriceLine(fl); } catch(ex) {}
            });
        }
        d.data.levels = newLevels;
        const diff = d.data.p2 - d.data.p1;
        d.fibLines = newLevels.map((item, i) => {
            const price = d.data.p1 + diff * item.level;
            return _s2.createPriceLine({
                price,
                color: item.color,
                lineWidth: 1.5,
                lineStyle: 0,
                axisLabelVisible: true,
                title: `${(item.level * 100).toFixed(1)}%`,
            });
        });
        persistDrawings();
        popup.remove();
    });

}

function hideDrawingPanel() {
    const p = document.getElementById('drawPanel');
    if (p) p.remove();
    const fp = document.getElementById('fibLevelsPopup');
    if (fp) fp.remove();
}

function changeDrawingColor(id, color) {
    const d = draw.drawings.find(dd => dd.id === id);
    if (!d) return;
    d.color = color;
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;

    // Recreate with new color
    if (d.type === 'hline' && d.priceLine && _s) {
        const price = d.data.price;
        try { _s.removePriceLine(d.priceLine); } catch(e) {}
        d.priceLine = _s.createPriceLine({
            price, color, lineWidth: 2, lineStyle: 0,
            axisLabelVisible: true, title: '',
        });
    } else if ((d.type === 'ray' || d.type === 'trendline') && d.lineSeries) {
        d.lineSeries.applyOptions({ color });
    } else if (d.type === 'fib' && d.fibLines && _s) {
        d.fibLines.forEach(fl => {
            try { _s.removePriceLine(fl); } catch(e) {}
        });
        const rawLevels = d.data.levels || fibConfig.load();
        const diff = d.data.p2 - d.data.p1;
        d.fibLines = rawLevels.map((item, i) => {
            const lvl = typeof item === 'number' ? item : item.level;
            const price = d.data.p1 + diff * lvl;
            return _s.createPriceLine({
                price, color, lineWidth: 1, lineStyle: 0,
                axisLabelVisible: true, title: `${(lvl * 100).toFixed(1)}%`,
            });
        });
        if (Array.isArray(d.data.levels)) {
            d.data.levels = d.data.levels.map(item => {
                if (typeof item === 'number') return { level: item, color };
                return { ...item, color };
            });
        }
    } else if (d.type === 'rect' && d.rectLines && _c) {
        d.rectLines.forEach(ls => ls.applyOptions({ color }));
        if (d.fillSeries) {
            const r = parseInt(color.slice(1, 3), 16);
            const g = parseInt(color.slice(3, 5), 16);
            const b = parseInt(color.slice(5, 7), 16);
            d.fillSeries.applyOptions({
                topColor: `rgba(${r}, ${g}, ${b}, 0.08)`,
                lineColor: 'transparent',
            });
        }
    }
    persistDrawings();
    showDrawingPanel(d); // refresh panel
}

// Point-to-segment distance in pixels
function ptSegDist(px, py, x1, y1, x2, y2) {
    const dx = x2 - x1, dy = y2 - y1;
    const lenSq = dx * dx + dy * dy;
    if (lenSq === 0) return Math.hypot(px - x1, py - y1);
    const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
    return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
}

// 2D hit detection — find drawing at pixel coordinates (relative to chartEl)
function findDrawingAtPoint(px, py) {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (!_s || !_c) return null;
    const HIT = 8; // px tolerance

    // Reverse order: last drawn = on top → gets priority
    for (let i = draw.drawings.length - 1; i >= 0; i--) {
        const d = draw.drawings[i];
        if (!d.data) continue;

        if (d.type === 'hline') {
            const ly = _s.priceToCoordinate(d.data.price);
            if (ly !== null && Math.abs(py - ly) < HIT) return d;
        }
        if (d.type === 'ray') {
            const ly = _s.priceToCoordinate(d.data.price);
            const sx = _c.timeScale().timeToCoordinate(d.data.startTime);
            if (ly !== null && Math.abs(py - ly) < HIT) {
                if (sx === null || px >= sx - HIT) return d;
            }
        }
        if (d.type === 'trendline') {
            const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
            const y1 = _s.priceToCoordinate(d.data.p1);
            const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
            const y2 = _s.priceToCoordinate(d.data.p2);
            if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
                if (ptSegDist(px, py, x1, y1, x2, y2) < HIT) return d;
            }
        }
        if (d.type === 'rect') {
            const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
            const y1 = _s.priceToCoordinate(d.data.p1);
            const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
            const y2 = _s.priceToCoordinate(d.data.p2);
            if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
                const xMin = Math.min(x1, x2), xMax = Math.max(x1, x2);
                const yMin = Math.min(y1, y2), yMax = Math.max(y1, y2);
                // Near border or inside
                if (px >= xMin - HIT && px <= xMax + HIT && py >= yMin - HIT && py <= yMax + HIT) return d;
            }
        }
        if (d.type === 'fib') {
            const diff = d.data.p2 - d.data.p1;
            const rawLevels = d.data.levels || fibConfig.load();
            for (const item of rawLevels) {
                const lvl = typeof item === 'number' ? item : item.level;
                const fibPrice = d.data.p1 + diff * lvl;
                const ly = _s.priceToCoordinate(fibPrice);
                if (ly !== null && Math.abs(py - ly) < HIT) return d;
            }
        }
    }
    return null;
}

// Legacy wrapper for backward compat (price-only)
function findDrawingNearPrice(price) {
    const _s = drawCtx.series || modal.series;
    if (!_s) return null;
    const py = _s.priceToCoordinate(price);
    if (py === null) return null;
    // Use center-x of chart as px (full-width hit for price-only calls)
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    const cx = chartEl ? chartEl.clientWidth / 2 : 400;
    return findDrawingAtPoint(cx, py);
}

// Get pixel positions of anchor points for a drawing
function getDrawingAnchors(d) {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (!_s || !_c || !d.data) return [];
    if (d.type === 'trendline') {
        const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
        const y1 = _s.priceToCoordinate(d.data.p1);
        const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
        const y2 = _s.priceToCoordinate(d.data.p2);
        if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
            return [{ x: x1, y: y1 }, { x: x2, y: y2 }];
        }
    }
    if (d.type === 'rect') {
        const x1 = _c.timeScale().timeToCoordinate(d.data.t1);
        const y1 = _s.priceToCoordinate(d.data.p1);
        const x2 = _c.timeScale().timeToCoordinate(d.data.t2);
        const y2 = _s.priceToCoordinate(d.data.p2);
        if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
            return [{ x: x1, y: y1 }, { x: x2, y: y1 }, { x: x1, y: y2 }, { x: x2, y: y2 }];
        }
    }
    if (d.type === 'fib') {
        const y1 = _s.priceToCoordinate(d.data.p1);
        const y2 = _s.priceToCoordinate(d.data.p2);
        const anchors = [];
        if (y1 !== null) anchors.push({ x: 40, y: y1 });
        if (y2 !== null) anchors.push({ x: 40, y: y2 });
        return anchors;
    }
    return [];
}

// Find anchor index at pixel point, returns -1 if none
function findAnchorAtPoint(px, py, d) {
    const anchors = getDrawingAnchors(d);
    for (let i = 0; i < anchors.length; i++) {
        if (Math.hypot(px - anchors[i].x, py - anchors[i].y) < 12) return i;
    }
    return -1;
}

// Save current drawings to localStorage
function persistDrawings() {
    const sym = drawCtx.sym || modal.currentSym;
    if (sym) {
        drawStore.save(sym, draw.drawings);
    }
}

// Restore drawings from localStorage for current symbol
function restoreDrawings() {
    const _sym = drawCtx.sym || modal.currentSym;
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (!_sym || !_s || !_c) return;
    const saved = drawStore.load(_sym);
    saved.forEach(s => {
        if (s.type === 'hline') {
            drawHorizontalLine(s.data.price, s.color);
            const d = draw.drawings[draw.drawings.length - 1];
            d.locked = s.locked;
        } else if (s.type === 'ray') {
            drawHorizontalRay(s.data.price, s.data.startTime, s.color);
            const d = draw.drawings[draw.drawings.length - 1];
            d.locked = s.locked;
        } else if (s.type === 'trendline') {
            drawTwoPointLine(s.type, s.data.t1, s.data.p1, s.data.t2, s.data.p2, s.color);
            const d = draw.drawings[draw.drawings.length - 1];
            d.locked = s.locked;
        } else if (s.type === 'fib') {
            drawFibonacci(s.data.p1, s.data.p2, null, s.data.levels);
            const d = draw.drawings[draw.drawings.length - 1];
            d.locked = s.locked;
        } else if (s.type === 'rect') {
            drawRectangle(s.data.t1, s.data.p1, s.data.t2, s.data.p2, s.color);
            const d = draw.drawings[draw.drawings.length - 1];
            d.locked = s.locked;
        }
    });
}

function removePreviewOverlay() {
    if (draw.overlay) {
        draw.overlay.remove();
        draw.overlay = null;
    }
}

function getPreviewCanvas() {
    if (draw.overlay) return draw.overlay;
    const chartEl = drawCtx.chartEl || el('cmChartBody');
    if (!chartEl) return null;
    const canvas = document.createElement('canvas');
    canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:50;';
    canvas.width = chartEl.clientWidth;
    canvas.height = chartEl.clientHeight;
    chartEl.appendChild(canvas);
    draw.overlay = canvas;
    return canvas;
}

// ============================================
// Drawing — click handlers (works on any chart via drawCtx)
// ============================================
function setupDrawingHandlers(targetEl) {
    const chartEl = targetEl || el('cmChartBody');
    if (!chartEl) return;

    // Remove old listeners by replacing element reference approach — use data attribute
    if (chartEl.dataset.drawInit) return;
    chartEl.dataset.drawInit = '1';

    // Getter helpers — always resolve to active drawCtx (or modal fallback)
    const DC = () => drawCtx.chart || modal.chart;
    const DS = () => drawCtx.series || modal.series;
    const DD = () => drawCtx.candleData || modal.candleData;
    const DTF = () => drawCtx.tf || modal.currentTF;

    // Get time from pixel X — extrapolates for future area (beyond last candle)
    function getTimeFromX(x) {
        const _c = DC();
        if (!_c) return null;
        const data = DD();
        if (!data || data.length < 2) {
            return _c.timeScale().coordinateToTime(x);
        }
        const last = data[data.length - 1];
        const prev = data[data.length - 2];
        const lastX = _c.timeScale().timeToCoordinate(last.time);
        const prevX = _c.timeScale().timeToCoordinate(prev.time);
        if (lastX === null || prevX === null) {
            return _c.timeScale().coordinateToTime(x);
        }
        // If x is beyond the last candle — ALWAYS extrapolate (coordinateToTime snaps to last bar)
        if (x > lastX + 2) {
            const pxPerBar = lastX - prevX;
            if (pxPerBar <= 0) return null;
            const step = last.time - prev.time;
            const barsAhead = (x - lastX) / pxPerBar;
            return Math.round(last.time + barsAhead * step);
        }
        // Within data range — use LWC's coordinate mapping
        const t = _c.timeScale().coordinateToTime(x);
        return t !== null ? t : null;
    }

    // Snap price to nearest OHLC of closest candle (magnet mode)
    function snapToCandle(time, price) {
        const data = DD();
        if (!data || data.length === 0) return { time, price };

        // Don't snap time if it's beyond the last candle (future area)
        const lastTime = data[data.length - 1].time;
        if (time > lastTime) return { time, price };

        // Binary search for closest candle by time
        let lo = 0, hi = data.length - 1;
        while (lo < hi) {
            const mid = (lo + hi) >> 1;
            if (data[mid].time < time) lo = mid + 1;
            else hi = mid;
        }
        // Check neighbors for closest (up to 2 in each direction)
        let bestIdx = lo;
        let bestDist = Math.abs(data[lo].time - time);
        for (let i = Math.max(0, lo - 2); i <= Math.min(data.length - 1, lo + 2); i++) {
            const d = Math.abs(data[i].time - time);
            if (d < bestDist) { bestDist = d; bestIdx = i; }
        }

        const c = data[bestIdx];
        const ohlc = [c.open, c.high, c.low, c.close];
        // Find nearest OHLC value to cursor price
        let nearestVal = ohlc[0];
        let nearestDist = Math.abs(ohlc[0] - price);
        for (const v of ohlc) {
            const d = Math.abs(v - price);
            if (d < nearestDist) { nearestDist = d; nearestVal = v; }
        }
        // Snap threshold: within 40% of candle range (tight but usable)
        const range = c.high - c.low || Math.abs(c.close) * 0.005;
        if (nearestDist > range * 0.4) return { time: c.time, price };
        return { time: c.time, price: nearestVal };
    }

    // Unified handler for both click and touch
    function handleDrawClick(clientX, clientY, fromTouch) {
        const _c = drawCtx.chart || modal.chart;
        const _s = drawCtx.series || modal.series;
        if (!_c || !_s) return;
        if (draw.activeTool === 'cursor') return;
        // Desktop: DM handles drawing via chart.subscribeClick, skip custom handler to avoid duplicates
        // Touch: always use custom handler (LWC touch is blocked, DM can't receive touch events)
        if (!fromTouch && typeof DM !== 'undefined' && DM && DM.isActive() && draw.activeTool !== 'ruler' && draw.activeTool !== 'alert') return;

        const rect = chartEl.getBoundingClientRect();
        const x = clientX - rect.left;
        const y = clientY - rect.top;
        let price = _s.coordinateToPrice(y);
        let time = getTimeFromX(x);
        if (price === null || time === null) return;

        // Magnet snap to nearest OHLC
        if (drawMagnet) {
            const snapped = snapToCandle(time, price);
            price = snapped.price;
            time = snapped.time;
        }

        if (draw.activeTool === 'alert') {
            drawAlertLine(price);
            draw.activeTool = 'cursor';
            renderDrawToolbar();
            updateModalCursor();
        } else if (draw.activeTool === 'hline') {
            drawHorizontalLine(price);
            draw.activeTool = 'cursor';
            renderDrawToolbar();
            updateModalCursor();
        } else if (draw.activeTool === 'ray') {
            drawHorizontalRay(price, time);
            draw.activeTool = 'cursor';
            renderDrawToolbar();
            updateModalCursor();
        } else if (draw.activeTool === 'trendline') {
            if (draw.clickCount === 0) {
                draw.startPrice = price;
                draw.startTime = time;
                draw.clickCount = 1;
            } else {
                drawTwoPointLine(draw.activeTool, draw.startTime, draw.startPrice, time, price);
                draw.clickCount = 0;
                removePreviewOverlay();
                draw.activeTool = 'cursor';
                renderDrawToolbar();
                updateModalCursor();
            }
        } else if (draw.activeTool === 'fib') {
            if (draw.clickCount === 0) {
                draw.startPrice = price;
                draw.startTime = time;
                draw.clickCount = 1;
            } else {
                drawFibonacci(draw.startPrice, price);
                draw.clickCount = 0;
                removePreviewOverlay();
                draw.activeTool = 'cursor';
                renderDrawToolbar();
                updateModalCursor();
            }
        } else if (draw.activeTool === 'rect') {
            if (draw.clickCount === 0) {
                draw.startPrice = price;
                draw.startTime = time;
                draw.clickCount = 1;
            } else {
                drawRectangle(draw.startTime, draw.startPrice, time, price);
                draw.clickCount = 0;
                removePreviewOverlay();
                draw.activeTool = 'cursor';
                renderDrawToolbar();
                updateModalCursor();
            }
        } else if (draw.activeTool === 'ruler') {
            if (draw.clickCount === 0) {
                draw.startPrice = price;
                draw.startTime = time;
                draw.clickCount = 1;
                // Remove old ruler measurement if exists
                removeRulerMeasurement();
            } else {
                showRulerMeasurement(draw.startTime, draw.startPrice, time, price);
                draw.clickCount = 0;
                removePreviewOverlay();
                draw.activeTool = 'cursor';
                renderDrawToolbar();
                updateModalCursor();
            }
        }
    }

    // Clear ruler on any click/tap in cursor mode
    chartEl.addEventListener('mousedown', () => {
        if (draw.activeTool === 'cursor' && rulerOverlay) removeRulerMeasurement();
    }, true);
    chartEl.addEventListener('touchstart', () => {
        if (draw.activeTool === 'cursor' && rulerOverlay) removeRulerMeasurement();
    }, { capture: true, passive: true });

    // Desktop click
    chartEl.addEventListener('click', (e) => {
        // Suppress click right after drag ended
        if (draw.justDragged) {
            draw.justDragged = false;
            return;
        }
        if (draw.activeTool === 'cursor') {
            // Shift+click starts ruler (like TradingView)
            const _sc = drawCtx.series || modal.series;
            const _cc = drawCtx.chart || modal.chart;
            if (e.shiftKey && _sc && _cc) {
                const rect = chartEl.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                let price = _sc.coordinateToPrice(y);
                let time = getTimeFromX(x);
                if (price !== null && time !== null) {
                    if (drawMagnet) { const snapped = snapToCandle(time, price); price = snapped.price; time = snapped.time; }
                    removeRulerMeasurement();
                    draw.activeTool = 'ruler';
                    draw.startPrice = price;
                    draw.startTime = time;
                    draw.clickCount = 1;
                    renderDrawToolbar();
                    updateModalCursor();
                }
                return;
            }
            // Select/deselect drawing (2D hit detection)
            const rect2 = chartEl.getBoundingClientRect();
            const px2 = e.clientX - rect2.left;
            const py2 = e.clientY - rect2.top;
            const found = findDrawingAtPoint(px2, py2);
            if (found) {
                selectDrawing(found.id);
            } else {
                deselectDrawing();
            }
            return;
        }
        handleDrawClick(e.clientX, e.clientY);
    });

    // Mobile touch — cursor mode only (drawing/drag handled in capture handlers above)
    chartEl.addEventListener('touchend', (e) => {
        if (draw.dragging) { endDrag(); return; }
        if (draw.activeTool === 'cursor') {
            const touch = e.changedTouches[0];
            if (!touch) return;
            const tRect = chartEl.getBoundingClientRect();
            const tPx = touch.clientX - tRect.left;
            const tPy = touch.clientY - tRect.top;
            const found = findDrawingAtPoint(tPx, tPy);
            if (found) { selectDrawing(found.id); } else { deselectDrawing(); }
        }
    }, { passive: false });

    // Touch→mouse proxy for library DrawingManager drag (library only listens to mouse events)
    chartEl.addEventListener('touchstart', (e) => {
        if (typeof DM === 'undefined' || !DM || !DM.isActive()) return;
        if (draw.activeTool !== 'cursor') return;
        const t = e.touches[0]; if (!t) return;
        chartEl.dispatchEvent(new MouseEvent('mousedown', { clientX: t.clientX, clientY: t.clientY, bubbles: true }));
    }, { passive: true });
    chartEl.addEventListener('touchmove', (e) => {
        if (typeof DM === 'undefined' || !DM || !DM.isDragging || !DM.isDragging()) return;
        const t = e.touches[0]; if (!t) return;
        chartEl.dispatchEvent(new MouseEvent('mousemove', { clientX: t.clientX, clientY: t.clientY, bubbles: true }));
    }, { passive: true });
    chartEl.addEventListener('touchend', (e) => {
        if (typeof DM === 'undefined' || !DM || !DM.isDragging || !DM.isDragging()) return;
        const t = e.changedTouches[0]; if (!t) return;
        chartEl.dispatchEvent(new MouseEvent('mouseup', { clientX: t.clientX, clientY: t.clientY, bubbles: true }));
    }, { passive: true });

    // Freeze/unfreeze chart scroll during drawing drag
    const _freezeChart = () => {
        const ce = drawCtx.chartEl || el('cmChartBody');
        if (ce) ce.style.touchAction = 'none';
        if (DC()) DC().applyOptions({
            handleScroll: { mouseWheel: false, pressedMouseMove: false, vertTouchDrag: false, horzTouchDrag: false },
            handleScale: { mouseWheel: false, pinch: false, axisPressedMouseMove: false },
        });
    };
    const _unfreezeChart = () => {
        const ce = drawCtx.chartEl || el('cmChartBody');
        if (ce) ce.style.touchAction = '';
        if (DC()) DC().applyOptions({
            handleScroll: { mouseWheel: true, pressedMouseMove: true, vertTouchDrag: true, horzTouchDrag: true },
            handleScale: { mouseWheel: true, pinch: true, axisPressedMouseMove: { price: true, time: true }, axisDoubleClickReset: { price: true, time: true } },
        });
    };

    // Drag support for ALL drawing types — mousedown
    function initDrag(px, py, e) {
        if (draw.activeTool !== 'cursor' || draw.selected === null) return false;
        const d = draw.drawings.find(dd => dd.id === draw.selected);
        if (!d || d.locked) return false;

        // Check anchor hit first (resize mode)
        const anchorIdx = findAnchorAtPoint(px, py, d);
        if (anchorIdx >= 0) {
            if (e) e.preventDefault();
            draw.dragging = true;
            draw.dragMode = 'resize';
            draw.dragAnchorIdx = anchorIdx;
            draw.dragOrigData = JSON.parse(JSON.stringify(d.data));
            _freezeChart();
            return true;
        }

        // Check body hit (move mode)
        const hit = findDrawingAtPoint(px, py);
        if (!hit || hit.id !== d.id) return false;

        if (e) e.preventDefault();
        draw.dragging = true;
        draw.dragMode = 'move';
        draw.dragAnchorIdx = -1;
        draw.dragStartPrice = DS() ? DS().coordinateToPrice(py) : 0;
        draw.dragStartTime = getTimeFromX(px) || 0;
        draw.dragOrigData = JSON.parse(JSON.stringify(d.data));
        _freezeChart();
        return true;
    }

    chartEl.addEventListener('mousedown', (e) => {
        const rect = chartEl.getBoundingClientRect();
        initDrag(e.clientX - rect.left, e.clientY - rect.top, e);
    });

    // Drag — mousemove (move + resize)
    function handleDragMove(px, py) {
        if (!draw.dragging || draw.selected === null) return;
        const d = draw.drawings.find(dd => dd.id === draw.selected);
        if (!d) return;
        const newPrice = DS() ? DS().coordinateToPrice(py) : null;
        const newTime = getTimeFromX(px);
        if (newPrice === null) return;
        const orig = draw.dragOrigData;

        if (draw.dragMode === 'resize') {
            // Resize: move only the dragged anchor point
            const idx = draw.dragAnchorIdx;
            if (d.type === 'trendline') {
                if (idx === 0) { d.data.t1 = newTime || orig.t1; d.data.p1 = newPrice; }
                else { d.data.t2 = newTime || orig.t2; d.data.p2 = newPrice; }
            } else if (d.type === 'rect') {
                // anchors: 0=(t1,p1) 1=(t2,p1) 2=(t1,p2) 3=(t2,p2)
                const nt = newTime || (idx < 2 ? orig.t1 : orig.t2);
                if (idx === 0)      { d.data.t1 = nt; d.data.p1 = newPrice; }
                else if (idx === 1) { d.data.t2 = nt; d.data.p1 = newPrice; }
                else if (idx === 2) { d.data.t1 = nt; d.data.p2 = newPrice; }
                else                { d.data.t2 = nt; d.data.p2 = newPrice; }
            } else if (d.type === 'fib') {
                if (idx === 0) d.data.p1 = newPrice;
                else d.data.p2 = newPrice;
            }
        } else {
            // Move: shift all points by delta
            const dp = newPrice - draw.dragStartPrice;
            const dt = (newTime && draw.dragStartTime) ? newTime - draw.dragStartTime : 0;
            if (d.type === 'hline') {
                d.data.price = orig.price + dp;
            } else if (d.type === 'ray') {
                d.data.price = orig.price + dp;
                d.data.startTime = orig.startTime + dt;
            } else if (d.type === 'trendline') {
                d.data.t1 = orig.t1 + dt; d.data.p1 = orig.p1 + dp;
                d.data.t2 = orig.t2 + dt; d.data.p2 = orig.p2 + dp;
            } else if (d.type === 'rect') {
                d.data.t1 = orig.t1 + dt; d.data.p1 = orig.p1 + dp;
                d.data.t2 = orig.t2 + dt; d.data.p2 = orig.p2 + dp;
            } else if (d.type === 'fib') {
                d.data.p1 = orig.p1 + dp;
                d.data.p2 = orig.p2 + dp;
            } else { return; }
        }

        removeDrawingVisuals(d);
        recreateDrawingVisuals(d);
        renderSelectionOverlay();
    }

    chartEl.addEventListener('mousemove', (e) => {
        if (!draw.dragging) return;
        const rect = chartEl.getBoundingClientRect();
        handleDragMove(e.clientX - rect.left, e.clientY - rect.top);
    });

    // Drag — mouseup
    function endDrag() {
        if (!draw.dragging) return;
        draw.dragging = false;
        draw.justDragged = true;
        draw.dragOrigData = null;
        draw.dragMode = 'move';
        draw.dragAnchorIdx = -1;
        persistDrawings();
        updateModalCursor();
        _unfreezeChart();
    }

    chartEl.addEventListener('mouseup', endDrag);

    // Touch: capture phase on chartEl fires BEFORE LWC's canvas handlers.
    // stopPropagation prevents LWC from seeing touch → no chart panning.
    // ALL drawing/drag touch logic runs HERE (bubble handlers won't fire after stopPropagation).
    // Cursor mode: no blocking → events flow to LWC + bubble handlers normally.

    chartEl.addEventListener('touchstart', (e) => {
        const touch = e.touches[0];
        if (!touch) return;
        const rect = chartEl.getBoundingClientRect();
        const px = touch.clientX - rect.left;
        const py = touch.clientY - rect.top;
        if (initDrag(px, py, e)) {
            e.preventDefault();
            e.stopPropagation();
            return;
        }
        if (draw.activeTool !== 'cursor') {
            e.preventDefault();
            e.stopPropagation();
        }
    }, { capture: true, passive: false });

    chartEl.addEventListener('touchmove', (e) => {
        if (draw.dragging && draw.selected !== null) {
            e.preventDefault();
            e.stopPropagation();
            const touch = e.touches[0];
            const rect = chartEl.getBoundingClientRect();
            handleDragMove(touch.clientX - rect.left, touch.clientY - rect.top);
            return;
        }
        if (draw.activeTool !== 'cursor') {
            e.preventDefault();
            e.stopPropagation();
            if (draw.clickCount >= 1) {
                const touch = e.touches[0];
                if (touch) {
                    const rect = chartEl.getBoundingClientRect();
                    renderDrawPreview(touch.clientX - rect.left, touch.clientY - rect.top);
                }
            }
        }
    }, { capture: true, passive: false });

    chartEl.addEventListener('touchend', (e) => {
        if (draw.dragging) {
            e.stopPropagation();
            endDrag();
            return;
        }
        if (draw.activeTool !== 'cursor') {
            e.stopPropagation();
            const touch = e.changedTouches[0];
            if (touch) handleDrawClick(touch.clientX, touch.clientY, true);
            return;
        }
    }, { capture: true, passive: false });

    // Live preview for 2-click tools (shared by mouse + touch)
    function renderDrawPreview(x, y) {
        if (!DC() || !DS()) return;
        if (draw.clickCount !== 1) return;
        if (draw.activeTool !== 'trendline' && draw.activeTool !== 'fib' && draw.activeTool !== 'rect' && draw.activeTool !== 'ruler') return;

        let price = DS().coordinateToPrice(y);
        if (price === null) return;

        // Snap to nearest candle OHLC (if magnet enabled)
        if (drawMagnet) {
            const curTime = getTimeFromX(x);
            if (curTime !== null) {
                const snapped = snapToCandle(curTime, price);
                price = snapped.price;
                const snapY = DS().priceToCoordinate(price);
                if (snapY !== null) y = snapY;
            }
        }

        const canvas = getPreviewCanvas();
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        canvas.width = chartEl.clientWidth;
        canvas.height = chartEl.clientHeight;
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        const startY2 = DS().priceToCoordinate(draw.startPrice);
        const startX2 = DC().timeScale().timeToCoordinate(draw.startTime);
        if (startY2 === null || startX2 === null) return;

        ctx.strokeStyle = '#5b9cf6';
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 3]);

        if (draw.activeTool === 'fib') {
            // Preview fib levels
            const fibCfg = fibConfig.load();
            const diff = price - draw.startPrice;
            fibCfg.forEach((item, i) => {
                const lvl = typeof item === 'number' ? item : item.level;
                const clr = (typeof item === 'object' && item.color) ? item.color : FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length];
                const fibPrice = draw.startPrice + diff * lvl;
                const fibY = DS().priceToCoordinate(fibPrice);
                if (fibY === null) return;
                ctx.strokeStyle = clr;
                ctx.setLineDash([3, 3]);
                ctx.beginPath();
                ctx.moveTo(0, fibY);
                ctx.lineTo(canvas.width, fibY);
                ctx.stroke();
                ctx.fillStyle = clr;
                ctx.font = '10px Inter, sans-serif';
                ctx.fillText(`${(lvl * 100).toFixed(1)}%  ${fibPrice.toFixed(getPricePrecision(fibPrice))}`, 5, fibY - 3);
            });
        } else if (draw.activeTool === 'rect') {
            // Preview rectangle
            const w = x - startX2;
            const h = y - startY2;
            ctx.strokeStyle = '#5b9cf6';
            ctx.lineWidth = 1.5;
            ctx.setLineDash([5, 3]);
            ctx.strokeRect(startX2, startY2, w, h);
            ctx.fillStyle = 'rgba(91, 156, 246, 0.08)';
            ctx.fillRect(startX2, startY2, w, h);
        } else if (draw.activeTool === 'ruler') {
            // Preview ruler measurement
            const curTime = getTimeFromX(x);
            const priceDiff = price - draw.startPrice;
            const pctDiff = draw.startPrice !== 0 ? (priceDiff / draw.startPrice * 100) : 0;
            const isUp = priceDiff >= 0;
            const color = isUp ? 'rgba(34,197,94,0.9)' : 'rgba(239,68,68,0.9)';
            const colorFill = isUp ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)';

            // Dashed lines (L-shape: vertical + horizontal)
            ctx.strokeStyle = color;
            ctx.lineWidth = 1;
            ctx.setLineDash([4, 3]);
            // Vertical from start
            ctx.beginPath(); ctx.moveTo(startX2, startY2); ctx.lineTo(startX2, y); ctx.stroke();
            // Horizontal to end
            ctx.beginPath(); ctx.moveTo(startX2, y); ctx.lineTo(x, y); ctx.stroke();

            // Main diagonal line
            ctx.lineWidth = 1.5;
            ctx.setLineDash([]);
            ctx.beginPath(); ctx.moveTo(startX2, startY2); ctx.lineTo(x, y); ctx.stroke();

            // Fill area
            ctx.fillStyle = colorFill;
            ctx.beginPath(); ctx.moveTo(startX2, startY2); ctx.lineTo(startX2, y); ctx.lineTo(x, y); ctx.closePath(); ctx.fill();

            // Dots at start/end
            ctx.fillStyle = color;
            ctx.beginPath(); ctx.arc(startX2, startY2, 3, 0, Math.PI * 2); ctx.fill();
            ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill();

            // Label
            const prec = getPricePrecision(Math.abs(draw.startPrice));
            const sign = isUp ? '+' : '';
            let timeStr = '';
            let barsStr = '';
            if (draw.startTime && curTime) {
                const timeDiffSec = Math.abs(curTime - draw.startTime);
                if (timeDiffSec < 60) timeStr = Math.round(timeDiffSec) + 's';
                else if (timeDiffSec < 3600) timeStr = Math.round(timeDiffSec / 60) + 'm';
                else if (timeDiffSec < 86400) timeStr = (timeDiffSec / 3600).toFixed(1) + 'h';
                else timeStr = (timeDiffSec / 86400).toFixed(1) + 'd';
                // Bars count based on modal TF
                const tfSec = { '1m': 60, '5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400 }[DTF()] || 300;
                barsStr = Math.round(timeDiffSec / tfSec) + ' bars';
            }

            const labelText = `${sign}${priceDiff.toFixed(prec)}  (${sign}${pctDiff.toFixed(2)}%)`;
            const labelText2 = timeStr ? `${timeStr}  •  ${barsStr}` : '';

            // Background box
            const midX = (startX2 + x) / 2;
            const labelY = Math.min(startY2, y) - 12;
            ctx.font = '600 12px Inter, sans-serif';
            const w1 = ctx.measureText(labelText).width;
            ctx.font = '11px Inter, sans-serif';
            const w2 = labelText2 ? ctx.measureText(labelText2).width : 0;
            const boxW = Math.max(w1, w2) + 16;
            const boxH = labelText2 ? 38 : 22;
            const boxX = midX - boxW / 2;
            const boxY = Math.max(2, labelY - boxH);

            ctx.fillStyle = isUp ? 'rgba(34,197,94,0.92)' : 'rgba(239,68,68,0.92)';
            ctx.beginPath();
            const r = 5;
            ctx.moveTo(boxX + r, boxY); ctx.lineTo(boxX + boxW - r, boxY);
            ctx.quadraticCurveTo(boxX + boxW, boxY, boxX + boxW, boxY + r);
            ctx.lineTo(boxX + boxW, boxY + boxH - r);
            ctx.quadraticCurveTo(boxX + boxW, boxY + boxH, boxX + boxW - r, boxY + boxH);
            ctx.lineTo(boxX + r, boxY + boxH);
            ctx.quadraticCurveTo(boxX, boxY + boxH, boxX, boxY + boxH - r);
            ctx.lineTo(boxX, boxY + r);
            ctx.quadraticCurveTo(boxX, boxY, boxX + r, boxY);
            ctx.fill();

            // Text
            ctx.fillStyle = '#fff';
            ctx.font = '600 12px Inter, sans-serif';
            ctx.textAlign = 'center';
            ctx.fillText(labelText, midX, boxY + 15);
            if (labelText2) {
                ctx.font = '11px Inter, sans-serif';
                ctx.fillStyle = 'rgba(255,255,255,0.8)';
                ctx.fillText(labelText2, midX, boxY + 30);
            }
            ctx.textAlign = 'start';
        } else {
            // Preview trendline
            ctx.beginPath();
            ctx.moveTo(startX2, startY2);
            ctx.lineTo(x, y);
            ctx.stroke();
        }
    }

    chartEl.addEventListener('mousemove', (e) => {
        if (draw.clickCount !== 1) return;
        const rect = chartEl.getBoundingClientRect();
        renderDrawPreview(e.clientX - rect.left, e.clientY - rect.top);
    });

    chartEl.addEventListener('touchmove', (e) => {
        if (draw.clickCount !== 1) return;
        if (draw.dragging) return; // drag has its own handler
        const touch = e.touches[0];
        if (!touch) return;
        const rect = chartEl.getBoundingClientRect();
        renderDrawPreview(touch.clientX - rect.left, touch.clientY - rect.top);
    }, { passive: true });
}

// ============================================
// Drawing visual helpers (remove + recreate for drag/move)
// ============================================
function removeDrawingVisuals(d) {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (d.priceLine && _s) { try { _s.removePriceLine(d.priceLine); } catch (_) {} d.priceLine = null; }
    if (d.lineSeries && _c) { try { _c.removeSeries(d.lineSeries); } catch (_) {} d.lineSeries = null; }
    if (d.rectLines && _c) { d.rectLines.forEach(ls => { try { _c.removeSeries(ls); } catch (_) {} }); d.rectLines = null; }
    if (d.fillSeries && _c) { try { _c.removeSeries(d.fillSeries); } catch (_) {} d.fillSeries = null; }
    if (d.bottomPriceLine && _s) { try { _s.removePriceLine(d.bottomPriceLine); } catch (_) {} d.bottomPriceLine = null; }
    if (d.fibLines && _s) { d.fibLines.forEach(pl => { try { _s.removePriceLine(pl); } catch (_) {} }); d.fibLines = null; }
}

function recreateDrawingVisuals(d) {
    const _s = drawCtx.series || modal.series;
    const _c = drawCtx.chart || modal.chart;
    if (!_s || !_c) return;
    const c = d.color || '#5b9cf6';
    const lineOpts = { color: c, lineWidth: 2, crosshairMarkerVisible: false, lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false };

    if (d.type === 'hline') {
        d.priceLine = _s.createPriceLine({ price: d.data.price, color: c, lineWidth: 2, lineStyle: 0, axisLabelVisible: true, title: '' });
    }
    if (d.type === 'ray') {
        const farTime = d.data.startTime + 365 * 24 * 3600;
        d.lineSeries = _c.addSeries(LightweightCharts.LineSeries, lineOpts);
        d.lineSeries.setData([{ time: d.data.startTime, value: d.data.price }, { time: farTime, value: d.data.price }]);
    }
    if (d.type === 'trendline') {
        d.lineSeries = _c.addSeries(LightweightCharts.LineSeries, lineOpts);
        const pts = [{ time: d.data.t1, value: d.data.p1 }, { time: d.data.t2, value: d.data.p2 }].sort((a, b) => a.time - b.time);
        // Dedup times
        const seen = new Set();
        const unique = pts.filter(p => { if (seen.has(p.time)) return false; seen.add(p.time); return true; });
        d.lineSeries.setData(unique);
    }
    if (d.type === 'rect') {
        const { t1, p1, t2, p2 } = d.data;
        const tMin = Math.min(t1, t2), tMax = Math.max(t1, t2);
        const pMin = Math.min(p1, p2), pMax = Math.max(p1, p2);
        const mkLine = () => _c.addSeries(LightweightCharts.LineSeries, { ...lineOpts, lineWidth: 1.5 });
        const topLine = mkLine(); topLine.setData([{ time: tMin, value: pMax }, { time: tMax, value: pMax }]);
        const bottomLine = mkLine(); bottomLine.setData([{ time: tMin, value: pMin }, { time: tMax, value: pMin }]);
        const leftLine = mkLine(); leftLine.setData([{ time: tMin, value: pMin }, { time: tMin + 1, value: pMax }]);
        const rightLine = mkLine(); rightLine.setData([{ time: tMax, value: pMin }, { time: tMax + 1, value: pMax }]);
        d.rectLines = [topLine, bottomLine, leftLine, rightLine];
        // Fill
        const hexToFill = (hex) => { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r},${g},${b},0.08)`; };
        d.fillSeries = _c.addSeries(LightweightCharts.AreaSeries, {
            topColor: hexToFill(c), bottomColor: 'transparent', lineColor: 'transparent', lineWidth: 0,
            crosshairMarkerVisible: false, lastValueVisible: false, priceLineVisible: false,
        });
        const fillPts = []; const steps = 20;
        for (let i = 0; i <= steps; i++) fillPts.push({ time: Math.round(tMin + (tMax - tMin) * i / steps), value: pMax });
        const seenT = new Set(); d.fillSeries.setData(fillPts.filter(p => { if (seenT.has(p.time)) return false; seenT.add(p.time); return true; }));
        d.bottomPriceLine = _s.createPriceLine({ price: pMin, color: 'transparent', lineWidth: 0, lineStyle: 2, axisLabelVisible: false, title: '' });
    }
    if (d.type === 'fib') {
        const rawLevels = d.data.levels || fibConfig.load();
        const diff = d.data.p2 - d.data.p1;
        d.fibLines = [];
        rawLevels.forEach((item, i) => {
            const lvl = typeof item === 'number' ? item : item.level;
            const clr = (typeof item === 'object' && item.color) ? item.color : c;
            const price = d.data.p1 + diff * lvl;
            d.fibLines.push(_s.createPriceLine({ price, color: clr, lineWidth: 1.5, lineStyle: 0, axisLabelVisible: true, title: `${(lvl * 100).toFixed(1)}%` }));
        });
    }
}

// ============================================
// Drawing implementations
// ============================================
function drawHorizontalLine(price, color) {
    const _s = drawCtx.series || modal.series;
    if (!_s) return;
    const c = color || '#5b9cf6';
    const priceLine = _s.createPriceLine({
        price: price,
        color: c,
        lineWidth: 2,
        lineStyle: 0,
        axisLabelVisible: true,
        title: '',
    });
    draw.drawings.push({ id: ++drawIdCounter, type: 'hline', color: c, locked: false, priceLine, data: { price } });
    persistDrawings();
}

function drawAlertLine(price) {
    const _s = drawCtx.series || modal.series;
    if (!_s) return;
    const sym = drawCtx.sym || modal.currentSym;
    if (!sym) return;

    // Auto-detect direction: above current price = crosses_above, below = crosses_below
    const currentPrice = alertState.lastPrices[sym];
    const direction = currentPrice ? (price > currentPrice ? 'crosses_above' : 'crosses_below') : 'crosses';

    // Visual: white dashed line (clean, no emoji on price axis)
    const pl = _s.createPriceLine({
        price, color: '#ffffff', lineWidth: 1,
        lineStyle: 2, axisLabelVisible: false, title: '',
    });
    _alertPriceLines.set(`${sym}:local_${Date.now()}`, pl);

    // Save alert to server/local
    priceAlertStore.add(sym, price, direction).then(alert => {
        // Re-key with proper ID
        if (alert && (alert.serverId || alert.id)) {
            applyAlertLinesToChart(sym, _s);
        }
    });

    // Toast
    const dirLabel = direction === 'crosses_above' ? '▲ Above' : direction === 'crosses_below' ? '▼ Below' : '↕ Crosses';
    showAlertToast(sym, sym.replace('USDT', ''), price, price, '🔔 Alert: ' + dirLabel, '#ffffff');
}

function drawHorizontalRay(price, startTime, color) {
    const _c = drawCtx.chart || modal.chart;
    if (!_c) return;
    const c = color || '#5b9cf6';
    const farTime = startTime + 365 * 24 * 3600; // 1 year forward

    const lineSeries = _c.addSeries(LightweightCharts.LineSeries, {
        color: c, lineWidth: 2,
        crosshairMarkerVisible: false, lastValueVisible: false,
        priceLineVisible: false, pointMarkersVisible: false,
    });
    lineSeries.setData([
        { time: startTime, value: price },
        { time: farTime, value: price },
    ]);
    draw.drawings.push({
        id: ++drawIdCounter, type: 'ray', color: c, locked: false,
        lineSeries, data: { price, startTime }
    });
    persistDrawings();
}

function drawTwoPointLine(type, t1, p1, t2, p2, color) {
    const _c = drawCtx.chart || modal.chart;
    if (!_c) return;
    const c = color || '#5b9cf6';
    const points = [];

    points.push({ time: t1, value: p1 });
    points.push({ time: t2, value: p2 });

    const seen = new Set();
    const uniquePoints = points.filter(p => {
        if (seen.has(p.time)) return false;
        seen.add(p.time);
        return true;
    }).sort((a, b) => a.time - b.time);

    const lineSeries = _c.addSeries(LightweightCharts.LineSeries, {
        color: c,
        lineWidth: 2,
        lineStyle: type === 'ray' ? 0 : 0,
        crosshairMarkerVisible: false,
        lastValueVisible: false,
        priceLineVisible: false,
        pointMarkersVisible: false,
    });
    lineSeries.setData(uniquePoints);
    draw.drawings.push({ id: ++drawIdCounter, type, color: c, locked: false, lineSeries, data: { t1, p1, t2, p2 } });
    persistDrawings();
}

function drawRectangle(t1, p1, t2, p2, color) {
    const _c = drawCtx.chart || modal.chart;
    const _s = drawCtx.series || modal.series;
    if (!_c || !_s) return;
    const c = color || '#5b9cf6';

    const tMin = Math.min(t1, t2);
    const tMax = Math.max(t1, t2);
    const pMin = Math.min(p1, p2);
    const pMax = Math.max(p1, p2);

    const topLine = _c.addSeries(LightweightCharts.LineSeries, {
        color: c, lineWidth: 1.5, crosshairMarkerVisible: false,
        lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
    });
    topLine.setData([{ time: tMin, value: pMax }, { time: tMax, value: pMax }]);

    const bottomLine = _c.addSeries(LightweightCharts.LineSeries, {
        color: c, lineWidth: 1.5, crosshairMarkerVisible: false,
        lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
    });
    bottomLine.setData([{ time: tMin, value: pMin }, { time: tMax, value: pMin }]);

    const leftLine = _c.addSeries(LightweightCharts.LineSeries, {
        color: c, lineWidth: 1.5, crosshairMarkerVisible: false,
        lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
    });
    leftLine.setData([{ time: tMin, value: pMin }, { time: tMin + 1, value: pMax }]);

    const rightLine = _c.addSeries(LightweightCharts.LineSeries, {
        color: c, lineWidth: 1.5, crosshairMarkerVisible: false,
        lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
    });
    rightLine.setData([{ time: tMax, value: pMin }, { time: tMax + 1, value: pMax }]);

    // Fill area
    const hexToFill = (hex) => {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return `rgba(${r}, ${g}, ${b}, 0.08)`;
    };
    const fillSeries = _c.addSeries(LightweightCharts.AreaSeries, {
        topColor: hexToFill(c),
        bottomColor: 'transparent',
        lineColor: 'transparent',
        lineWidth: 0,
        crosshairMarkerVisible: false,
        lastValueVisible: false,
        priceLineVisible: false,
    });
    // Generate fill data points
    const fillPoints = [];
    const steps = 20;
    for (let i = 0; i <= steps; i++) {
        const t = Math.round(tMin + (tMax - tMin) * i / steps);
        fillPoints.push({ time: t, value: pMax });
    }
    // Deduplicate times
    const seenTimes = new Set();
    const uniqueFill = fillPoints.filter(p => {
        if (seenTimes.has(p.time)) return false;
        seenTimes.add(p.time);
        return true;
    });
    fillSeries.setData(uniqueFill);

    // Bottom price line for visual boundary
    const bottomPriceLine = _s.createPriceLine({
        price: pMin, color: 'transparent', lineWidth: 0, lineStyle: 2,
        axisLabelVisible: false, title: '',
    });

    draw.drawings.push({
        id: ++drawIdCounter, type: 'rect', color: c, locked: false,
        rectLines: [topLine, bottomLine, leftLine, rightLine],
        fillSeries, bottomPriceLine,
        data: { t1: tMin, p1: pMin, t2: tMax, p2: pMax }
    });
    persistDrawings();
}

// ---- Ruler Measurement ----
let rulerOverlay = null;

function removeRulerMeasurement() {
    if (rulerOverlay) {
        if (rulerOverlay._unsub) rulerOverlay._unsub();
        rulerOverlay.remove();
        rulerOverlay = null;
    }
}

function showRulerMeasurement(t1, p1, t2, p2) {
    const _c = drawCtx.chart || modal.chart;
    const _s = drawCtx.series || modal.series;
    if (!_c || !_s) return;
    removeRulerMeasurement();

    const chartEl = drawCtx.chartEl || el('cmChartBody');
    if (!chartEl) return;

    // Create persistent overlay
    const overlay = document.createElement('canvas');
    overlay.className = 'ruler-overlay';
    overlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99;';
    chartEl.appendChild(overlay);
    rulerOverlay = overlay;

    function render() {
        if (!rulerOverlay || !_c || !_s) return;
        const w = chartEl.clientWidth;
        const h = chartEl.clientHeight;
        overlay.width = w;
        overlay.height = h;
        const ctx = overlay.getContext('2d');
        ctx.clearRect(0, 0, w, h);

        const sx = _c.timeScale().timeToCoordinate(t1);
        const sy = _s.priceToCoordinate(p1);
        const ex = _c.timeScale().timeToCoordinate(t2);
        const ey = _s.priceToCoordinate(p2);
        if (sx === null || sy === null || ex === null || ey === null) return;

        const priceDiff = p2 - p1;
        const pctDiff = p1 !== 0 ? (priceDiff / p1 * 100) : 0;
        const isUp = priceDiff >= 0;
        const color = isUp ? 'rgba(34,197,94,0.9)' : 'rgba(239,68,68,0.9)';
        const colorFill = isUp ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)';

        // Dashed L-shape
        ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
        ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx, ey); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(sx, ey); ctx.lineTo(ex, ey); ctx.stroke();

        // Main line
        ctx.lineWidth = 1.5; ctx.setLineDash([]);
        ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(ex, ey); ctx.stroke();

        // Fill triangle
        ctx.fillStyle = colorFill;
        ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx, ey); ctx.lineTo(ex, ey); ctx.closePath(); ctx.fill();

        // Dots
        ctx.fillStyle = color;
        ctx.beginPath(); ctx.arc(sx, sy, 4, 0, Math.PI * 2); ctx.fill();
        ctx.beginPath(); ctx.arc(ex, ey, 4, 0, Math.PI * 2); ctx.fill();

        // Label
        const prec = getPricePrecision(Math.abs(p1));
        const sign = isUp ? '+' : '';
        const timeDiffSec = Math.abs(t2 - t1);
        let timeStr = '';
        if (timeDiffSec < 60) timeStr = Math.round(timeDiffSec) + 's';
        else if (timeDiffSec < 3600) timeStr = Math.round(timeDiffSec / 60) + 'm';
        else if (timeDiffSec < 86400) timeStr = (timeDiffSec / 3600).toFixed(1) + 'h';
        else timeStr = (timeDiffSec / 86400).toFixed(1) + 'd';
        const tfSec = { '1m': 60, '5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400 }[modal.currentTF] || 300;
        const bars = Math.round(timeDiffSec / tfSec);

        const line1 = `${sign}${priceDiff.toFixed(prec)}  (${sign}${pctDiff.toFixed(2)}%)`;
        const line2 = `${timeStr}  •  ${bars} bars`;

        const midX = (sx + ex) / 2;
        const labelY = Math.min(sy, ey) - 12;
        ctx.font = '600 12px Inter, sans-serif';
        const w1 = ctx.measureText(line1).width;
        ctx.font = '11px Inter, sans-serif';
        const w2 = ctx.measureText(line2).width;
        const boxW = Math.max(w1, w2) + 16;
        const boxH = 38;
        const boxX = midX - boxW / 2;
        const boxY = Math.max(2, labelY - boxH);

        ctx.fillStyle = isUp ? 'rgba(34,197,94,0.92)' : 'rgba(239,68,68,0.92)';
        const r = 5;
        ctx.beginPath();
        ctx.moveTo(boxX + r, boxY); ctx.lineTo(boxX + boxW - r, boxY);
        ctx.quadraticCurveTo(boxX + boxW, boxY, boxX + boxW, boxY + r);
        ctx.lineTo(boxX + boxW, boxY + boxH - r);
        ctx.quadraticCurveTo(boxX + boxW, boxY + boxH, boxX + boxW - r, boxY + boxH);
        ctx.lineTo(boxX + r, boxY + boxH);
        ctx.quadraticCurveTo(boxX, boxY + boxH, boxX, boxY + boxH - r);
        ctx.lineTo(boxX, boxY + r);
        ctx.quadraticCurveTo(boxX, boxY, boxX + r, boxY);
        ctx.fill();

        ctx.fillStyle = '#fff'; ctx.textAlign = 'center';
        ctx.font = '600 12px Inter, sans-serif';
        ctx.fillText(line1, midX, boxY + 15);
        ctx.font = '11px Inter, sans-serif';
        ctx.fillStyle = 'rgba(255,255,255,0.8)';
        ctx.fillText(line2, midX, boxY + 30);
        ctx.textAlign = 'start';
    }

    render();

    // Re-render on scroll/zoom so ruler follows the chart
    const subId = _c.timeScale().subscribeVisibleTimeRangeChange(render);
    overlay._unsub = () => {
        try { _c.timeScale().unsubscribeVisibleTimeRangeChange(render); } catch(e) {}
    };
}

function drawFibonacci(p1, p2, color, customLevels) {
    const _s = drawCtx.series || modal.series;
    if (!_s) return;
    const rawLevels = customLevels || fibConfig.load();
    const levels = rawLevels.map((item, i) => {
        if (typeof item === 'number') return { level: item, color: color || FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length] };
        return { level: item.level, color: color || item.color || FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length] };
    });
    const diff = p2 - p1;
    const fibLines = [];

    levels.forEach((item, i) => {
        const price = p1 + diff * item.level;
        const label = `${(item.level * 100).toFixed(1)}%`;
        const priceLine = _s.createPriceLine({
            price: price,
            color: item.color,
            lineWidth: 1.5,
            lineStyle: 0,
            axisLabelVisible: true,
            title: label,
        });
        fibLines.push(priceLine);
    });

    const savedColor = color || levels[0]?.color || '#4caf50';
    draw.drawings.push({
        id: ++drawIdCounter, type: 'fib', color: savedColor, locked: false,
        fibLines, priceLine: null, data: { p1, p2, levels }
    });
    persistDrawings();
}

function closeCoinModal() {
    const closingSym = modal.currentSym;
    el('coinModal').classList.add('hidden');
    removeRulerMeasurement();
    removeSelectionOverlay();
    // Abort pending fetches (klines + metrics)
    if (_modalLoadController) { _modalLoadController.abort(); _modalLoadController = null; }
    if (modal._metricsController) { modal._metricsController.abort(); modal._metricsController = null; }
    // Cleanup ResizeObserver
    if (modal._resizeObserver) { modal._resizeObserver.disconnect(); modal._resizeObserver = null; }
    // Detach library DrawingManager
    if (typeof DM !== 'undefined' && DM && DM.detach) { try { DM.detach(); } catch(e) {} }
    // Stop countdown timer
    if (modal._countdownTimer) { clearInterval(modal._countdownTimer); modal._countdownTimer = null; }
    // Clear signal markers
    if (modal._sigMarkers) { try { modal._sigMarkers.setMarkers([]); } catch(_){} modal._sigMarkers = null; }
    window._pendingSignalMarker = null;
    // Detach depth heatmap overlay
    if (typeof depthHeatmapUI !== 'undefined') { try { depthHeatmapUI.detach(); } catch(_){} }
    // Unsubscribe modal WS stream
    if (modal.wsStream) {
        if (mc.ws && mc.ws.readyState === WebSocket.OPEN) {
            mc.ws.send(JSON.stringify({ method: 'UNSUBSCRIBE', params: [modal.wsStream], id: Date.now() }));
        }
        mc.wsStreams.delete(modal.wsStream);
        modal.wsStream = null;
    }
    if (modal._infiniteUnsub) { try { modal._infiniteUnsub(); } catch(e) {} modal._infiniteUnsub = null; }
    // Cleanup ruler listeners before removing chart
    const chartEl = el('cmChartBody');
    if (chartEl && chartEl._rulerCleanup) chartEl._rulerCleanup();

    if (modal.chart) {
        modal.chart.remove();
        modal.chart = null;
        modal.series = null;
        modal.volSeries = null;
        modal.lines = [];
        modal.legend = null;
    }
    modal.currentSym = null;
    // Clear drawing chart objects (data already persisted in localStorage)
    draw.drawings = [];
    draw.selected = null;
    draw.activeTool = 'cursor';
    draw.clickCount = 0;
    hideDrawingPanel();
    removePreviewOverlay();

    // Refresh drawings on mini-chart (user may have added/removed drawings)
    if (closingSym && mc.charts[closingSym]) {
        applyDrawingsToMiniChart(closingSym);
    }
}

// Init modal event listeners (called once in initMiniCharts)
let _modalEventsInitialized = false;

function _modalEscapeHandler(e) {
    if (e.key === 'Escape' && modal.currentSym) closeCoinModal();
}

function initModalEvents() {
    if (_modalEventsInitialized) return; // Guard against double init
    _modalEventsInitialized = true;

    // Close button
    el('cmClose').addEventListener('click', closeCoinModal);

    // Overlay click
    document.querySelector('.mc-modal-overlay').addEventListener('click', closeCoinModal);

    // Heatmap toggle button
    const hmBtn = el('cmHeatmapBtn');
    if (hmBtn) {
        hmBtn.addEventListener('click', () => {
            if (typeof depthHeatmapUI !== 'undefined') {
                const vis = depthHeatmapUI.toggle();
                hmBtn.classList.toggle('active', vis);
            }
        });
        // Restore initial state
        if (typeof depthHeatmapUI !== 'undefined') hmBtn.classList.toggle('active', depthHeatmapUI.isVisible());
    }

    // Escape key (named function — removable if needed)
    document.addEventListener('keydown', _modalEscapeHandler);

    // TF buttons in modal (event delegation on container — single listener)
    el('cmTFButtons').addEventListener('click', (e) => {
        const btn = e.target.closest('.mc-tf-btn');
        if (!btn || !modal.currentSym) return;
        el('cmTFButtons').querySelectorAll('.mc-tf-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        modal.currentTF = btn.dataset.tf;
        // Detach heatmap before chart rebuild (prevents ghost canvas)
        if (typeof depthHeatmapUI !== 'undefined') try { depthHeatmapUI.detach() } catch(_){}
        // Clear chart objects before reload (data stays in localStorage)
        draw.drawings.forEach(d => removeDrawingFromChart(d));
        draw.drawings = [];
        loadModalChart(modal.currentSym, modal.currentTF);
        startCountdown();
    });
}

// ==========================================
// Multi-Chart Layout System
// ==========================================
const mch = {
    layout: 'grid',      // 'grid' | '1' | '2' | '4'
    activeSlot: 0,        // which slot receives next symbol click
    slots: [],            // { sym, tf, chart, series, volSeries, legend }
};

// Restore layout from localStorage
try { mch.layout = localStorage.getItem('mch_layout') || 'grid'; } catch(e) {}

function initLayoutPicker() {
    const picker = el('mcLayoutPicker');
    if (!picker) return;

    // Set initial active state
    picker.querySelectorAll('.mc-layout-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.layout === mch.layout);
    });

    picker.addEventListener('click', (e) => {
        const btn = e.target.closest('.mc-layout-btn');
        if (!btn) return;
        const layout = btn.dataset.layout;
        if (layout === mch.layout) return;

        picker.querySelectorAll('.mc-layout-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');

        switchLayout(layout);
    });

    // Apply saved layout on init
    if (mch.layout !== 'grid') {
        switchLayout(mch.layout);
    }
}

function switchLayout(layout) {
    mch.layout = layout;
    lsSet('mch_layout', layout);

    const miniChartsLayout = el('mcMiniChartsLayout');
    const multiChart = el('mcMultiChart');
    const sidebar = el('mcSidebar');

    if (layout === 'grid') {
        // Show mini-charts grid, hide multi-chart
        miniChartsLayout.style.display = 'flex';
        multiChart.style.display = 'none';
        // Sidebar stays inside mc-layout
        if (sidebar.parentElement !== miniChartsLayout) {
            miniChartsLayout.appendChild(sidebar);
        }
    } else {
        // Hide mini-charts grid, show multi-chart
        miniChartsLayout.style.display = 'none';
        multiChart.style.display = 'flex';
        // Move sidebar into multi-chart container
        if (sidebar.parentElement !== multiChart) {
            multiChart.appendChild(sidebar);
        }
        renderMultiChartSlots(layout);
    }
}

function renderMultiChartSlots(layout) {
    const grid = el('mchGrid');
    grid.dataset.layout = layout;
    const countMap = { '2h': 2, '2v': 2, '4': 4, '1+3': 4 };
    const count = countMap[layout] || parseInt(layout) || 2;

    // Preserve existing slot symbols
    const savedSyms = mch.slots.map(s => s.sym);

    // Restore from localStorage if first time
    if (savedSyms.length === 0) {
        try {
            const saved = JSON.parse(localStorage.getItem('mch_slots') || '[]');
            for (let i = 0; i < count; i++) {
                savedSyms[i] = saved[i] || null;
            }
        } catch(e) {}
    }

    // Destroy old chart instances
    mch.slots.forEach(slot => {
        if (slot.chart) {
            try { slot.chart.remove(); } catch(e) {}
        }
    });
    mch.slots = [];
    grid.innerHTML = '';

    for (let i = 0; i < count; i++) {
        const slot = document.createElement('div');
        slot.className = 'mch-slot' + (i === mch.activeSlot ? ' active' : '');
        slot.dataset.index = i;

        const sym = savedSyms[i] || null;

        slot.innerHTML = `
            <div class="mch-slot-header">
                <span class="mch-slot-sym">${sym ? sym.replace('USDT','') + '/USDT' : '—'}</span>
                ${sym ? `<button class="mch-copy-btn" data-ticker="${sym.toLowerCase()}" title="Copy ${sym.toLowerCase()}"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>` : ''}
                <span class="mch-slot-price"></span>
                <span class="mch-slot-change"></span>
                <div class="mch-slot-tf">
                    <button class="mc-tf-btn" data-tf="1m">1m</button>
                    <button class="mc-tf-btn" data-tf="5m">5m</button>
                    <button class="mc-tf-btn active" data-tf="15m">15m</button>
                    <button class="mc-tf-btn" data-tf="1h">1h</button>
                    <button class="mc-tf-btn" data-tf="4h">4h</button>
                    <button class="mc-tf-btn" data-tf="1d">1d</button>
                </div>
            </div>
            ${sym ? '<div class="mch-slot-chart" id="mch-chart-' + i + '"></div>' : '<div class="mch-slot-empty">Click a coin in sidebar</div>'}
        `;

        // Copy ticker button
        const copyBtn = slot.querySelector('.mch-copy-btn');
        if (copyBtn) {
            copyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                navigator.clipboard.writeText(copyBtn.dataset.ticker).then(() => {
                    copyBtn.classList.add('mc-copy-ok');
                    setTimeout(() => copyBtn.classList.remove('mc-copy-ok'), 800);
                });
            });
        }

        // Click slot to make it active
        slot.addEventListener('click', (e) => {
            if (e.target.closest('.mc-tf-btn') || e.target.closest('.mch-copy-btn')) return;
            setActiveSlot(i);
        });

        // TF buttons per slot
        slot.querySelector('.mch-slot-tf').addEventListener('click', (e) => {
            const btn = e.target.closest('.mc-tf-btn');
            if (!btn) return;
            const slotData = mch.slots[i];
            if (!slotData || !slotData.sym) return;
            slot.querySelectorAll('.mch-slot-tf .mc-tf-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            // Unsubscribe old TF stream, subscribe new
            mchWsUnsubscribe(slotData.sym, slotData.tf);
            slotData.tf = btn.dataset.tf;
            mchWsSubscribe(slotData.sym, slotData.tf);
            loadSlotChart(i);
        });

        grid.appendChild(slot);

        mch.slots.push({
            sym: sym,
            tf: mc.globalTF,
            chart: null,
            series: null,
            volSeries: null,
            legend: null,
            el: slot
        });

        // If slot has a symbol, create chart + subscribe WS
        if (sym) {
            createSlotChart(i);
            loadSlotChart(i);
            mchWsSubscribe(sym, mc.globalTF);
        }
    }
}

function setActiveSlot(index) {
    mch.activeSlot = index;
    document.querySelectorAll('.mch-slot').forEach((s, i) => {
        s.classList.toggle('active', i === index);
    });
    // Switch drawing context to active slot
    const slot = mch.slots[index];
    if (slot && slot.chart && slot.sym) {
        // Save current drawings before switching
        persistDrawings();
        // Clear current drawings from chart
        draw.drawings.forEach(d => removeDrawingFromChart(d));
        draw.drawings = [];
        draw.selected = null;
        draw.activeTool = 'cursor';
        draw.clickCount = 0;
        hideDrawingPanel();
        removePreviewOverlay();

        setDrawCtxSlot(index);
        // Restore drawings for new symbol
        restoreDrawings();
        updateModalCursor();
    }
}

function assignSymbolToSlot(sym, slotIndex) {
    const slot = mch.slots[slotIndex];
    if (!slot) return;

    // Unsubscribe old symbol WS stream
    if (slot.sym) {
        mchWsUnsubscribe(slot.sym, slot.tf);
    }

    // Destroy old chart
    if (slot.chart) {
        try { slot.chart.remove(); } catch(e) {}
        slot.chart = null;
        slot.series = null;
        slot.volSeries = null;
    }

    slot.sym = sym;
    const pair = mc.allPairs.find(p => p.symbol === sym);

    // Update header
    const header = slot.el.querySelector('.mch-slot-sym');
    header.textContent = sym ? sym.replace('USDT','') + '/USDT' : '—';

    // Update or create copy button
    let copyBtn = slot.el.querySelector('.mch-copy-btn');
    if (sym) {
        if (!copyBtn) {
            copyBtn = document.createElement('button');
            copyBtn.className = 'mch-copy-btn';
            copyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
            header.after(copyBtn);
            copyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                navigator.clipboard.writeText(copyBtn.dataset.ticker).then(() => {
                    copyBtn.classList.add('mc-copy-ok');
                    setTimeout(() => copyBtn.classList.remove('mc-copy-ok'), 800);
                });
            });
        }
        copyBtn.dataset.ticker = sym.toLowerCase();
        copyBtn.title = `Copy ${sym.toLowerCase()}`;
    } else if (copyBtn) {
        copyBtn.remove();
    }

    if (pair) {
        const prec = getPricePrecision(pair.lastPrice);
        slot.el.querySelector('.mch-slot-price').textContent = '$' + pair.lastPrice.toFixed(prec);
        const chg = pair.priceChange;
        const chgEl = slot.el.querySelector('.mch-slot-change');
        chgEl.textContent = (chg >= 0 ? '+' : '') + chg.toFixed(2) + '%';
        chgEl.style.color = chg >= 0 ? '#22c55e' : '#ef4444';
    }

    // Replace empty placeholder with chart div
    const empty = slot.el.querySelector('.mch-slot-empty');
    if (empty) {
        const chartDiv = document.createElement('div');
        chartDiv.className = 'mch-slot-chart';
        chartDiv.id = 'mch-chart-' + slotIndex;
        empty.replaceWith(chartDiv);
    }

    createSlotChart(slotIndex);
    loadSlotChart(slotIndex);
    saveSlotSymbols();

    // Subscribe to WS for live updates
    mchWsSubscribe(sym, slot.tf);

    // Auto-advance to next slot
    const nextSlot = (slotIndex + 1) % mch.slots.length;
    setActiveSlot(nextSlot);
}

function createSlotChart(slotIndex) {
    const slot = mch.slots[slotIndex];
    const chartEl = el('mch-chart-' + slotIndex);
    if (!chartEl || !slot.sym) return;

    const pair = mc.allPairs.find(p => p.symbol === slot.sym);
    const price = pair ? pair.lastPrice : 1;
    const prec = getPricePrecision(price);
    const minMove = parseFloat((1 / Math.pow(10, prec)).toFixed(prec));

    slot.chart = LightweightCharts.createChart(chartEl, {
        autoSize: true,
        ...localChartOptions,
        layout: { background: { type: 'solid', color: 'transparent' }, textColor: '#94a3b8', fontSize: 10 },
        grid: getGridOpts(),
        crosshair: { mode: 0 },
        rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)', scaleMargins: { top: 0.05, bottom: 0.05 }, minimumWidth: 45, mode: getPriceScaleMode() },
        timeScale: { borderColor: 'rgba(255,255,255,0.08)', timeVisible: true, secondsVisible: false, rightOffset: 50, shiftVisibleRangeOnNewBar: true, lockVisibleTimeRangeOnResize: true, tickMarkFormatter: localTickFormatter },
        handleScroll: { mouseWheel: true, pressedMouseMove: true, vertTouchDrag: true, horzTouchDrag: true },
        handleScale: { mouseWheel: true, pinch: true },
    });

    slot.series = addMainSeries(slot.chart, prec, minMove);

    slot.volSeries = slot.chart.addSeries(LightweightCharts.HistogramSeries, {
        priceFormat: { type: 'volume' },
        priceScaleId: 'vol',
        color: 'rgba(100,116,139,0.3)',
    });
    slot.chart.priceScale('vol').applyOptions({
        scaleMargins: { top: getVolScaleTop(), bottom: 0 },
        drawTicks: false,
        borderVisible: false,
    });

    // OHLCV legend
    const legend = document.createElement('div');
    legend.className = 'mc-ohlcv-legend';
    chartEl.appendChild(legend);
    slot.legend = legend;

    slot.chart.subscribeCrosshairMove(param => {
        if (!param || !param.time || !slot.legend) {
            if (slot.legend) slot.legend.style.display = 'none';
            return;
        }
        const data = param.seriesData.get(slot.series);
        if (!data) { slot.legend.style.display = 'none'; return; }
        const p = getPricePrecision(data.close || data.open || 1);
        const o = (data.open||0).toFixed(p), h = (data.high||0).toFixed(p);
        const l = (data.low||0).toFixed(p), c = (data.close||0).toFixed(p);
        const volData = param.seriesData.get(slot.volSeries);
        const v = volData ? (volData.value >= 1e6 ? (volData.value/1e6).toFixed(1)+'M' : (volData.value >= 1e3 ? (volData.value/1e3).toFixed(0)+'K' : volData.value.toFixed(0))) : '—';
        const color = data.close >= data.open ? '#22c55e' : '#ef4444';
        slot.legend.style.display = 'flex';
        slot.legend.innerHTML = `<span style="color:${color}">O <b>${o}</b></span><span style="color:${color}">H <b>${h}</b></span><span style="color:${color}">L <b>${l}</b></span><span style="color:${color}">C <b>${c}</b></span><span style="color:var(--text-muted)">V <b>${v}</b></span>`;
    });

    // Drawing tools on slot — switch context on any interaction
    chartEl.addEventListener('mousedown', () => {
        if (drawCtx.source !== 'slot:' + slotIndex) {
            setDrawCtxSlot(slotIndex);
            // Move toolbar to this slot
            renderDrawToolbar(chartEl);
        }
    }, true);
    chartEl.addEventListener('touchstart', () => {
        if (drawCtx.source !== 'slot:' + slotIndex) {
            setDrawCtxSlot(slotIndex);
            renderDrawToolbar(chartEl);
        }
    }, { capture: true, passive: true });

    // Attach ruler + drawing handlers
    attachRuler(chartEl, slot.chart, slot.series);
    setupDrawingHandlers(chartEl);

    // Render compact toolbar
    setDrawCtxSlot(slotIndex);
    renderDrawToolbar(chartEl);
    // Reset ctx back to modal if modal is open
    if (modal.chart) setDrawCtxModal();
}

async function loadSlotChart(slotIndex) {
    const slot = mch.slots[slotIndex];
    if (!slot || !slot.sym || !slot.series) return;

    try {
        const res = await fetch(`/api/klines?symbol=${slot.sym}&interval=${slot.tf}&limit=500`);
        if (!res.ok) return;
        const raw = await res.json();
        if (!Array.isArray(raw) || raw.length === 0) return;

        const parsed = parseKlines(raw);
        slot.candleData = parsed;
        slot.series.setData(parsed);
        slot.volSeries?.setData(extractVolume(parsed));

        // Update drawCtx if this slot is active
        if (drawCtx.source === 'slot:' + slotIndex) drawCtx.candleData = parsed;

        // Scroll to end respecting rightOffset
        slot.chart.timeScale().scrollToRealTime();

        // Apply saved drawings (per-symbol, visible on all TFs)
        applyDrawingsToSlot(slotIndex);

        // Fetch and render density walls
        applyDensityToSlot(slotIndex);

        // Auto S/R levels on slot (with Multi-TF confluence)
        if (spGet('levelsEnabled', true) && slot.candleData) {
            if (slot.autoLevels) slot.autoLevels.forEach(pl => { try { slot.series.removePriceLine(pl); } catch(e){} });
            slot.autoLevels = [];
            let levels;
            try {
                levels = await computeMultiTFLevels(slot.sym, slot.tf, slot.candleData);
            } catch { levels = computeAutoLevels(slot.candleData); }
            levels.forEach(l => {
                const line = slot.series.createPriceLine(levelToPriceLine(l, true));
                slot.autoLevels.push(line);
            });
        }

        // Auto trendlines on slot
        if (spGet('trendlinesEnabled', false) && slot.candleData) {
            if (slot.autoTrendlines) slot.autoTrendlines.forEach(obj => { try { slot.chart.removeSeries(obj.lineSeries); } catch(e){} });
            slot.autoTrendlines = [];
            const tlines = computeAutoTrendlines(slot.candleData);
            tlines.forEach(tl => {
                const opacity = tl.broken ? 0.25 : Math.max(0.4, tl.score / 100);
                const baseColor = tl.type === 'support' ? [34, 197, 94] : [239, 68, 68];
                const color = `rgba(${baseColor.join(',')},${opacity})`;
                const ls = slot.chart.addSeries(LightweightCharts.LineSeries, {
                    color, lineWidth: tl.broken ? 1 : 2, lineStyle: tl.broken ? 3 : 2,
                    crosshairMarkerVisible: false, lastValueVisible: false,
                    priceLineVisible: false, pointMarkersVisible: false,
                });
                ls.setData([
                    { time: tl.p1.time, value: tl.p1.price },
                    { time: tl.p2.time, value: tl.p2.price },
                ]);
                slot.autoTrendlines.push({ lineSeries: ls });
            });
        }

        // Channels on slot (each type individually toggled)
        // Keltner
        if (slot.keltner) slot.keltner.forEach(ls => { try { slot.chart.removeSeries(ls); } catch(e){} });
        slot.keltner = [];
        if (spGet('ch_keltner', false) && slot.candleData) {
            const kd = computeKeltnerChannel(slot.candleData);
            if (kd.length >= 2) {
                const addBand = (field, style) => {
                    const ls = slot.chart.addSeries(LightweightCharts.LineSeries, {
                        color: style.color, lineWidth: 1, lineStyle: style.dash ? 2 : 0,
                        crosshairMarkerVisible: false, lastValueVisible: false,
                        priceLineVisible: false, pointMarkersVisible: false,
                    });
                    ls.setData(kd.map(d => ({ time: d.time, value: d[field] })));
                    slot.keltner.push(ls);
                };
                addBand('upper', { color: 'rgba(251,146,60,0.6)' });
                addBand('mid',   { color: 'rgba(251,146,60,0.35)', dash: true });
                addBand('lower', { color: 'rgba(251,146,60,0.6)' });
            }
        }
        // Regression Channel on slot
        if (slot.regressionCh) slot.regressionCh.forEach(ls => { try { slot.chart.removeSeries(ls); } catch(e){} });
        slot.regressionCh = [];
        if (spGet('ch_regression', false) && slot.candleData) {
            const rd = computeRegressionChannel(slot.candleData);
            if (rd.length >= 2) {
                const addL = (f, c, d) => { const ls = slot.chart.addSeries(LightweightCharts.LineSeries, { color: c, lineWidth: 1, lineStyle: d ? 2 : 0, crosshairMarkerVisible: false, lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false }); ls.setData(rd.map(p => ({ time: p.time, value: p[f] }))); slot.regressionCh.push(ls); };
                addL('upper', 'rgba(56,189,248,0.5)', false);
                addL('mid', 'rgba(56,189,248,0.35)', true);
                addL('lower', 'rgba(56,189,248,0.5)', false);
            }
        }
        // OI indicator
        applyOI(slot, slot.sym, slot.tf);
    } catch(e) {
        console.error('[MCH] Load error slot', slotIndex, e);
    }
}

// Client-side density wall filter using settings
function filterDensityWalls(walls) {
    const depthPct = spGet('densityDepthPct', 5.0);
    const ttlMin = spGet('densityTTLMin', 1);
    const xSmall = spGet('densitySeveritySmall', 2.0);
    const blacklistRaw = spGet('densityBlacklist', 'USDC,FDUSD,TUSD,USDP,DAI,USDD,EUR');
    const blacklist = blacklistRaw ? blacklistRaw.split(',').map(s => s.trim().toUpperCase()).filter(Boolean) : [];

    return walls.filter(w => {
        // Blacklist filter
        if (blacklist.length > 0 && blacklist.some(b => w.symbol && w.symbol.includes(b))) return false;
        // Depth filter — distancePct from current price
        if (w.distancePct !== undefined && Math.abs(w.distancePct) > depthPct) return false;
        // TTL filter — minimum lifetime in minutes
        if (w.lifetimeMins !== undefined && w.lifetimeMins < ttlMin) return false;
        // Severity filter — xMult must be >= smallest severity setting
        if (w.xMult !== undefined && w.xMult < xSmall) return false;
        return true;
    });
}

async function applyDensityToSlot(slotIndex) {
    if (!spGet('densityEnabled', true)) return;
    const slot = mch.slots[slotIndex];
    if (!slot || !slot.sym || !slot.chart || !slot.series) return;

    // Clear old density lines
    if (slot.densityObjs) {
        slot.densityObjs.forEach(obj => {
            if (obj.priceLine) try { slot.series.removePriceLine(obj.priceLine); } catch(e) {}
        });
    }
    slot.densityObjs = [];

    try {
        const res = await fetch(`/densities/v2?symbols=${slot.sym}`);
        const json = await res.json();
        const items = json.data || [];
        if (items.length === 0) return;

        const entry = items[0];
        const walls = [];
        if (entry.support) walls.push({ ...entry.support, side: 'bid' });
        if (entry.resistance) walls.push({ ...entry.resistance, side: 'ask' });
        // Add extra bid/ask walls if available
        (entry.bidWalls || []).slice(1, 3).forEach(w => walls.push({ ...w, side: 'bid' }));
        (entry.askWalls || []).slice(1, 3).forEach(w => walls.push({ ...w, side: 'ask' }));

        walls.forEach(w => {
            const isBid = w.side === 'bid';
            const color = isBid ? '#22c55e' : '#ef4444';
            const notionalStr = w.notional >= 1e6 ? (w.notional / 1e6).toFixed(1) + 'M' : Math.round(w.notional / 1e3) + 'K';
            const label = `$${notionalStr} ${w.sizeVsMedian}x`;

            const priceLine = slot.series.createPriceLine({
                price: w.price,
                color: color,
                lineWidth: 1,
                lineStyle: 2,
                axisLabelVisible: true,
                title: label,
            });
            slot.densityObjs.push({ priceLine });
        });
    } catch(e) {
        // silently ignore density fetch errors
    }
}

// Apply density walls to mini-chart card (grid view)
// Batch density load — one request for all visible symbols
async function applyDensityToBatch(symbols) {
    if (!spGet('densityEnabled', true)) return;
    try {
        const res = await fetch(`/densities/v2?symbols=${symbols.join(',')}`);
        const json = await res.json();
        const items = json.data || [];
        if (items.length === 0) return;

        // Build map: symbol → flat wall list
        const bySymbol = {};
        items.forEach(entry => {
            const walls = [];
            if (entry.support) walls.push({ ...entry.support, side: 'bid' });
            if (entry.resistance) walls.push({ ...entry.resistance, side: 'ask' });
            (entry.bidWalls || []).slice(1, 2).forEach(w => walls.push({ ...w, side: 'bid' }));
            (entry.askWalls || []).slice(1, 2).forEach(w => walls.push({ ...w, side: 'ask' }));
            if (walls.length > 0) bySymbol[entry.symbol] = walls;
        });

        // Apply to each chart
        for (const sym of symbols) {
            const chartObj = mc.charts[sym];
            if (!chartObj || !chartObj.series) continue;

            if (chartObj.densityLines) {
                chartObj.densityLines.forEach(pl => { try { chartObj.series.removePriceLine(pl); } catch(e) {} });
            }
            chartObj.densityLines = [];

            const symWalls = bySymbol[sym] || [];
            symWalls.forEach(w => {
                const color = w.side === 'bid' ? '#22c55e' : '#ef4444';
                const notionalStr = w.notional >= 1e6 ? (w.notional / 1e6).toFixed(1) + 'M' : Math.round(w.notional / 1e3) + 'K';
                const priceLine = chartObj.series.createPriceLine({
                    price: w.price, color, lineWidth: 1, lineStyle: 2,
                    axisLabelVisible: false, title: `$${notionalStr} ${w.sizeVsMedian}x`,
                });
                chartObj.densityLines.push(priceLine);
            });
        }
    } catch(e) { /* ignore */ }
}

async function applyDensityToMiniChart(sym) {
    if (!spGet('densityEnabled', true)) return;
    const chartObj = mc.charts[sym];
    if (!chartObj || !chartObj.series) return;

    // Clear old density lines
    if (chartObj.densityLines) {
        chartObj.densityLines.forEach(pl => {
            try { chartObj.series.removePriceLine(pl); } catch(e) {}
        });
    }
    chartObj.densityLines = [];

    try {
        const res = await fetch(`/densities/simple?symbols=${sym}&limitSymbols=1&xFilter=2`);
        const json = await res.json();
        const walls = filterDensityWalls(json.data || []);
        if (walls.length === 0) return;

        walls.forEach(w => {
            const color = w.sideKey === 'bid' ? '#22c55e' : '#ef4444';
            const notionalStr = w.notional >= 1e6 ? (w.notional / 1e6).toFixed(1) + 'M' : Math.round(w.notional / 1e3) + 'K';
            const priceLine = chartObj.series.createPriceLine({
                price: w.price,
                color,
                lineWidth: 1,
                lineStyle: 2,
                axisLabelVisible: false,
                title: `$${notionalStr} x${w.xMult}`,
            });
            chartObj.densityLines.push(priceLine);
        });
    } catch(e) { /* ignore */ }
}

// Apply density walls to modal chart
async function applyDensityToModal() {
    if (!spGet('densityEnabled', true)) return;
    if (!modal.chart || !modal.series || !modal.currentSym) return;

    // Clear old density lines
    if (modal.densityLines) {
        modal.densityLines.forEach(pl => {
            try { modal.series.removePriceLine(pl); } catch(e) {}
        });
    }
    modal.densityLines = [];

    try {
        const res = await fetch(`/densities/v2?symbols=${modal.currentSym}`);
        const json = await res.json();
        const items = json.data || [];
        if (items.length === 0) return;

        const entry = items[0];
        const walls = [];
        if (entry.support) walls.push({ ...entry.support, side: 'bid' });
        if (entry.resistance) walls.push({ ...entry.resistance, side: 'ask' });
        // Add extra walls (up to 5 per side)
        (entry.bidWalls || []).slice(1, 5).forEach(w => walls.push({ ...w, side: 'bid' }));
        (entry.askWalls || []).slice(1, 5).forEach(w => walls.push({ ...w, side: 'ask' }));

        walls.forEach(w => {
            const isBid = w.side === 'bid';
            const color = isBid ? '#22c55e' : '#ef4444';
            const notionalStr = w.notional >= 1e6 ? (w.notional / 1e6).toFixed(1) + 'M' : Math.round(w.notional / 1e3) + 'K';
            const statusTag = w.status === 'strong' ? ' 🧱' : w.status === 'confirmed' ? ' ✓' : '';
            const priceLine = modal.series.createPriceLine({
                price: w.price,
                color,
                lineWidth: w.sizeVsMedian >= 10 ? 2 : 1,
                lineStyle: 2,
                axisLabelVisible: true,
                title: `$${notionalStr} ${w.sizeVsMedian}x${statusTag}`,
            });
            modal.densityLines.push(priceLine);
        });
    } catch(e) { /* ignore */ }
}

function applyDrawingsToSlot(slotIndex) {
    const slot = mch.slots[slotIndex];
    if (!slot || !slot.sym || !slot.chart || !slot.series) return;

    // Clear old drawing objects
    if (slot.drawObjs) {
        slot.drawObjs.forEach(obj => {
            if (obj.priceLine) try { slot.series.removePriceLine(obj.priceLine); } catch(e) {}
            if (obj.lineSeries) try { slot.chart.removeSeries(obj.lineSeries); } catch(e) {}
        });
    }
    slot.drawObjs = [];

    const saved = drawStore.load(slot.sym);
    if (!saved || saved.length === 0) return;

    saved.forEach(s => {
        if (s.type === 'hline' && s.data) {
            const pl = slot.series.createPriceLine({
                price: s.data.price,
                color: s.color || '#5b9cf6',
                lineWidth: 2,
                lineStyle: 0,
                axisLabelVisible: true,
                title: '',
            });
            slot.drawObjs.push({ priceLine: pl });
        } else if (s.type === 'fib' && s.data) {
            const rawLevels = s.data.levels || FIB_DEFAULTS_OBJ;
            const diff = s.data.p2 - s.data.p1;
            rawLevels.forEach((item, i) => {
                const lvl = typeof item === 'number' ? item : item.level;
                const clr = (typeof item === 'object' && item.color) ? item.color : (s.color || FIB_DEFAULT_COLORS[i % FIB_DEFAULT_COLORS.length]);
                const price = s.data.p1 + diff * lvl;
                const pl = slot.series.createPriceLine({
                    price,
                    color: clr,
                    lineWidth: 1.5,
                    lineStyle: 0,
                    axisLabelVisible: true,
                    title: `${(lvl * 100).toFixed(1)}%`,
                });
                slot.drawObjs.push({ priceLine: pl });
            });
        } else if (s.type === 'ray' && s.data) {
            const startTime = s.data.startTime || s.data.t1;
            const farTime = startTime + 365 * 24 * 3600;
            const ls = slot.chart.addSeries(LightweightCharts.LineSeries, {
                color: s.color || '#5b9cf6',
                lineWidth: 2,
                crosshairMarkerVisible: false,
                lastValueVisible: false,
                priceLineVisible: false,
                pointMarkersVisible: false,
            });
            ls.setData([
                { time: startTime, value: s.data.price },
                { time: farTime, value: s.data.price },
            ]);
            slot.drawObjs.push({ lineSeries: ls });
        } else if (s.type === 'trendline' && s.data) {
            const ls = slot.chart.addSeries(LightweightCharts.LineSeries, {
                color: s.color || '#5b9cf6',
                lineWidth: 2,
                crosshairMarkerVisible: false,
                lastValueVisible: false,
                priceLineVisible: false,
                pointMarkersVisible: false,
            });
            ls.setData([
                { time: s.data.t1, value: s.data.p1 },
                { time: s.data.t2, value: s.data.p2 },
            ]);
            slot.drawObjs.push({ lineSeries: ls });
        } else if (s.type === 'rect' && s.data) {
            const { t1, p1, t2, p2 } = s.data;
            const clr = s.color || '#5b9cf6';
            const pMax = Math.max(p1, p2);
            const pMin = Math.min(p1, p2);
            // Top border
            const topLs = slot.chart.addSeries(LightweightCharts.LineSeries, {
                color: clr, lineWidth: 1.5, crosshairMarkerVisible: false,
                lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
            });
            topLs.setData([{ time: t1, value: pMax }, { time: t2, value: pMax }]);
            slot.drawObjs.push({ lineSeries: topLs });
            // Bottom border
            const botLs = slot.chart.addSeries(LightweightCharts.LineSeries, {
                color: clr, lineWidth: 1.5, crosshairMarkerVisible: false,
                lastValueVisible: false, priceLineVisible: false, pointMarkersVisible: false,
            });
            botLs.setData([{ time: t1, value: pMin }, { time: t2, value: pMin }]);
            slot.drawObjs.push({ lineSeries: botLs });
        }
    });
}

function saveSlotSymbols() {
    const syms = mch.slots.map(s => s.sym || null);
    lsSet('mch_slots', JSON.stringify(syms));
}

// WS helpers for multi-chart slots
function mchWsSubscribe(sym, tf) {
    if (!sym) return;
    const stream = `${sym.toLowerCase()}@kline_${tf}`;
    if (mc.wsStreams.has(stream)) return;
    mc.wsStreams.add(stream);
    if (mc.ws && mc.ws.readyState === WebSocket.OPEN) {
        mc.ws.send(JSON.stringify({ method: 'SUBSCRIBE', params: [stream], id: Date.now() }));
    } else {
        wsConnect();
    }
}

function mchWsUnsubscribe(sym, tf) {
    if (!sym) return;
    const stream = `${sym.toLowerCase()}@kline_${tf}`;
    // Only unsubscribe if no other slot uses same stream
    const othersUsing = mch.slots.some(s => s.sym === sym && s.tf === tf && s.chart);
    if (othersUsing) return;
    if (mc.ws && mc.ws.readyState === WebSocket.OPEN) {
        mc.ws.send(JSON.stringify({ method: 'UNSUBSCRIBE', params: [stream], id: Date.now() }));
    }
    mc.wsStreams.delete(stream);
}

// Override sidebar click when in multi-chart mode
function handleSidebarCoinClick(sym) {
    if (mch.layout === 'grid') {
        openCoinModal(sym);
    } else {
        assignSymbolToSlot(sym, mch.activeSlot);
    }
}

📜 Git History

85e4ebdfix: 16-bug audit — resync storm, memory leaks, API errors, data persistence7 weeks ago
7a5cb1ffix: drawing tools — touch support, future area, magnet snap, SW cache7 weeks ago
8e168c2cleanup: remove dead code + fix mini-chart event listener leak8 weeks ago
487e3f1fix: modal chart loading freeze after 4-5 opens — abort pending fetches8 weeks ago
a995ad4fix: bypass Bottleneck for all user-facing endpoints + remove gapless from modal8 weeks ago
07d5cecfeat: market resilience tracker (book stability + recovery speed)8 weeks ago
8813c91feat: Fill:Kill ratio tracker (wall authenticity / spoof detection)8 weeks ago
b1f6e80feat: VPIN scanner (order flow toxicity indicator)8 weeks ago
0ec6ec5feat: depth heatmap engine (Bookmap-style order book visualization)8 weeks ago
b37bc10feat: gapless modal charts via createChartEx + GaplessHorzScaleBehavior8 weeks ago
Show last diff
Loading...