โ† ะะฐะทะฐะด
/** * Settings Panel โ€” Slide-out panel with sections * Sections: Charts, Densities, Signals, Layout, Reset * Persistence: localStorage (free) + server sync (pro, future) * * Usage: settingsPanel.get('candleType'), settingsPanel.onChange(cb) */ const settingsPanel = (() => { // --- Defaults --- const DEFAULTS = { // Charts candleType: 'Candlestick', // Candlestick, Bar, Line, Area logScale: false, volumeHeight: 15, // % of chart barWidth: 2, // thin=1, normal=2, wide=3 showGrid: true, showWatermark: true, defaultTF: '5m', // default timeframe // Sidebar & Grid cardsPerRow: 4, // 3, 4, 5, 6 cardSize: 'normal', // compact, normal, large colChg: true, // show Chg% column colNatr: true, // show NATR column colVol: true, // show Vol column // Watchlist watchlistOnly: false, // show only watchlist coins in sidebar // Theme theme: 'dark', // dark, darker, amoled candleUp: '#22c55e', // green candleDown: '#ef4444', // red // Densities densityEnabled: true, densitySeverityLarge: 5.0, // multiplier for Large densitySeverityMedium: 3.5, // multiplier for Medium densitySeveritySmall: 2.0, // multiplier for Small densityDepthPct: 5.0, // depth % from price densityTTLMin: 1, // min lifetime in minutes // Density blacklist densityBlacklist: 'USDC,FDUSD,TUSD,USDP,DAI,USDD,EUR', // Signals signalMinRatio: 2, // volume spike min ratio (2x-20x) signalMinConfidence: 50, // min confidence to show signal (30-90) signalNotifications: false, // in-tab browser notifications (toast + Notification API) signalPush: false, // server push notifications (works with browser closed / on phone) signalSound: false, // sound on new signal signalCooldown: 5, // minutes between same-symbol alerts (1, 5, 15, 30) signalWatchlistOnly: false, // only show signals for watchlist coins signalTypes: ['volume_spike', 'oi_longs', 'oi_shorts', 'oi_squeeze', 'oi_liquidation', 'oi_divergence', 'oi_funding_squeeze', 'liq_sweep', 'channel'], // enabled signal types for push // Channel Signal channelTf5m: true, // show/push 5m channel signals channelTf15m: true, // show/push 15m channel signals channelTf1h: true, // show/push 1h channel signals // Liq Sweep sweepLevelSwing: true, // show signals from swing high/low levels sweepLevelWall: true, // show signals from order book walls sweepLevelRound: true, // show signals from round numbers sweepMinWickPct: 60, // min wick ratio % to show (60-90) // Signals on charts showSignalsOnCharts: false, // show signal markers on mini-charts and modal // Indicators indicatorOI: false, // show OI overlay on charts indicatorOIColor: '#eab308', // OI line color (yellow) // Data autoRefresh: true, // auto-refresh data refreshInterval: 30, // seconds (10, 30, 60, 120) defaultSort: 'change', // change, volume, natr, symbol defaultSortDir: 'asc', // asc, desc minVolume: 50, // min 24h volume in $M (0=off, 10, 50, 100, 250, 500) // Layout layout: '1', // '1', '2h', '2v', '4', '1+3' // Channel overlays ch_keltner: false, ch_regression: false, // Drawing defaults drawLineStyle: 'solid', // solid, dashed, dotted drawDefaultColor: '#ffffff', } const STORAGE_KEY = 'fs_settings' const WL_KEY = 'fs_watchlist' let settings = {} let listeners = [] let panelOpen = false let watchlist = new Set() // --- Watchlist --- function loadWatchlist() { try { watchlist = new Set(JSON.parse(localStorage.getItem(WL_KEY) || '[]')) } catch { watchlist = new Set() } } function saveWatchlist() { lsSet(WL_KEY, JSON.stringify([...watchlist])) } function wlAdd(sym) { watchlist.add(sym); saveWatchlist(); notify('__watchlist', [...watchlist]) } function wlRemove(sym) { watchlist.delete(sym); saveWatchlist(); notify('__watchlist', [...watchlist]) } function wlToggle(sym) { watchlist.has(sym) ? wlRemove(sym) : wlAdd(sym) } function wlHas(sym) { return watchlist.has(sym) } function wlList() { return [...watchlist] } function wlClear() { watchlist.clear(); saveWatchlist(); notify('__watchlist', []) } // Sync watchlist across tabs via storage event window.addEventListener('storage', (e) => { if (e.key === WL_KEY) { try { watchlist = new Set(JSON.parse(e.newValue || '[]')) } catch { return } notify('__watchlist', [...watchlist]) } if (e.key === STORAGE_KEY) { try { settings = { ...DEFAULTS, ...JSON.parse(e.newValue || '{}') } } catch { return } notify('__settings', settings) } }) // --- Load / Save --- function load() { try { const raw = localStorage.getItem(STORAGE_KEY) settings = raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS } // Migration: fix old density defaults that filtered everything if (settings.densityTTLMin === 15) settings.densityTTLMin = DEFAULTS.densityTTLMin if (settings.densityDepthPct === 3.0) settings.densityDepthPct = DEFAULTS.densityDepthPct if (!settings.densityBlacklist) settings.densityBlacklist = DEFAULTS.densityBlacklist save() } catch { settings = { ...DEFAULTS } } } function save() { lsSet(STORAGE_KEY, JSON.stringify(settings)) // Future: if pro, sync to server via authUI.authFetch } function get(key) { return settings[key] !== undefined ? settings[key] : DEFAULTS[key] } function set(key, value) { settings[key] = value save() notify(key, value) } function getAll() { return { ...settings } } function resetAll() { settings = { ...DEFAULTS } save() listeners.forEach(cb => cb('__reset', null)) renderActiveSection() } // --- Change listeners --- function onChange(cb) { listeners.push(cb) } function notify(key, value) { listeners.forEach(cb => cb(key, value)) } // --- Panel DOM --- const SECTIONS = [ { id: 'charts', icon: '๐Ÿ“Š', label: 'Charts' }, { id: 'densities', icon: 'โ—‰', label: 'Densities' }, { id: 'signals', icon: '๐Ÿ””', label: 'Signals' }, { id: 'watchlist', icon: 'โญ', label: 'Watchlist' }, { id: 'theme', icon: '๐ŸŽจ', label: 'Theme' }, { id: 'data', icon: '๐Ÿ“ก', label: 'Data' }, { id: 'indicators',icon: '๐Ÿ“‰', label: 'Indicators' }, { id: 'layout', icon: 'โŠž', label: 'Layout' }, { id: 'reset', icon: '๐Ÿ—‘', label: 'Reset' }, ] let activeSection = 'charts' function createPanel() { // Overlay const overlay = document.createElement('div') overlay.id = 'settingsOverlay' overlay.className = 'settings-overlay hidden' overlay.addEventListener('click', closePanel) // Panel const panel = document.createElement('div') panel.id = 'settingsPanel' panel.className = 'settings-panel hidden' panel.innerHTML = ` <div class="sp-header"> <button class="sp-back hidden" id="spBack">โ†</button> <h3 class="sp-title">Settings</h3> <button class="sp-close" id="spClose">&times;</button> </div> <div class="sp-body"> <div class="sp-nav" id="spNav"> ${SECTIONS.map(s => ` <button class="sp-nav-item${s.id === activeSection ? ' active' : ''}" data-section="${s.id}"> <span class="sp-nav-icon">${s.icon}</span> <span class="sp-nav-label">${s.label}</span> </button> `).join('')} </div> <div class="sp-content" id="spContent"></div> </div> ` document.body.appendChild(overlay) document.body.appendChild(panel) // Events panel.querySelector('#spClose').addEventListener('click', closePanel) panel.querySelector('#spBack').addEventListener('click', () => { panel.querySelector('#spBack').classList.add('hidden') panel.querySelector('.sp-title').textContent = 'Settings' panel.querySelector('#spNav').classList.remove('hidden') panel.querySelector('#spContent').innerHTML = '' }) panel.querySelectorAll('.sp-nav-item').forEach(btn => { btn.addEventListener('click', () => { activeSection = btn.dataset.section panel.querySelectorAll('.sp-nav-item').forEach(b => b.classList.remove('active')) btn.classList.add('active') renderActiveSection() // Mobile: hide nav, show back if (window.innerWidth < 600) { panel.querySelector('#spNav').classList.add('hidden') panel.querySelector('#spBack').classList.remove('hidden') panel.querySelector('.sp-title').textContent = SECTIONS.find(s => s.id === activeSection)?.label || 'Settings' } }) }) renderActiveSection() } function renderActiveSection() { const content = document.getElementById('spContent') if (!content) return switch (activeSection) { case 'charts': content.innerHTML = renderChartsSection(); break case 'densities': content.innerHTML = renderDensitiesSection(); break case 'signals': content.innerHTML = renderSignalsSection(); break case 'watchlist': content.innerHTML = renderWatchlistSection(); break case 'theme': content.innerHTML = renderThemeSection(); break case 'data': content.innerHTML = renderDataSection(); break case 'indicators': content.innerHTML = renderIndicatorsSection(); break case 'layout': content.innerHTML = renderLayoutSection(); break case 'reset': content.innerHTML = renderResetSection(); break } bindSectionEvents(content) } // --- Section Renderers --- function updateCandlePreview(container, up, down) { container.querySelectorAll('.sp-candle.up').forEach(c => c.style.setProperty('--c', up)) container.querySelectorAll('.sp-candle.down').forEach(c => c.style.setProperty('--c', down)) } const CANDLE_PRESETS = [ { label: 'Classic', up: '#22c55e', down: '#ef4444' }, { label: 'Blue/Orange', up: '#3b82f6', down: '#f97316' }, { label: 'Cyan/Pink', up: '#06b6d4', down: '#ec4899' }, { label: 'Lime/Purple', up: '#84cc16', down: '#a855f7' }, { label: 'TradingView', up: '#26a69a', down: '#ef5350' }, { label: 'Monochrome', up: '#94a3b8', down: '#475569' }, ] function renderThemeSection() { const currentUp = get('candleUp') const currentDown = get('candleDown') return ` <div class="sp-section"> <div class="sp-section-title">Theme</div> <div class="sp-radio-group"> ${['dark', 'darker', 'amoled'].map(t => ` <label class="sp-radio"> <input type="radio" name="theme" value="${t}" ${get('theme') === t ? 'checked' : ''} data-key="theme" /> <span>${t.charAt(0).toUpperCase() + t.slice(1)}</span> </label> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Candle Colors</div> <div class="sp-color-presets"> ${CANDLE_PRESETS.map(p => ` <button class="sp-color-preset${currentUp === p.up && currentDown === p.down ? ' active' : ''}" data-up="${p.up}" data-down="${p.down}" title="${p.label}"> <span class="sp-color-dot" style="background:${p.up}"></span> <span class="sp-color-dot" style="background:${p.down}"></span> <span class="sp-color-name">${p.label}</span> </button> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Custom Colors</div> <div class="sp-color-row"> <label class="sp-color-label"> <span>Up</span> <input type="color" value="${currentUp}" data-key="candleUp" class="sp-color-input" /> </label> <label class="sp-color-label"> <span>Down</span> <input type="color" value="${currentDown}" data-key="candleDown" class="sp-color-input" /> </label> </div> <div class="sp-candle-preview"> <div class="sp-candle up" style="--c:${currentUp}"></div> <div class="sp-candle down" style="--c:${currentDown}"></div> <div class="sp-candle up small" style="--c:${currentUp}"></div> <div class="sp-candle down" style="--c:${currentDown}"></div> <div class="sp-candle up" style="--c:${currentUp}"></div> <div class="sp-candle down small" style="--c:${currentDown}"></div> </div> </div> ` } function renderWatchlistSection() { const list = wlList() return ` <div class="sp-section"> <div class="sp-section-title">Your Watchlist (${list.length})</div> ${list.length === 0 ? `<p class="sp-hint">Click โญ on any coin in the sidebar to add it</p>` : ` <div class="sp-wl-list"> ${list.map(sym => ` <div class="sp-wl-item"> <span class="sp-wl-name">${sym.replace('USDT', '')}</span> <button class="sp-wl-remove" data-sym="${sym}" title="Remove">โœ•</button> </div> `).join('')} </div> `} </div> <div class="sp-section"> <div class="sp-section-title">Filter</div> <label class="sp-toggle"> <input type="checkbox" id="spWlOnly" ${get('watchlistOnly') ? 'checked' : ''} /> <span>Show watchlist only in sidebar</span> </label> </div> ${list.length > 0 ? ` <div class="sp-section"> <button class="sp-danger-btn" id="spClearWatchlist">Clear watchlist</button> </div> ` : ''} ` } function renderDataSection() { return ` <div class="sp-section"> <div class="sp-section-title">Auto-Refresh</div> <label class="sp-toggle"> <input type="checkbox" ${get('autoRefresh') ? 'checked' : ''} data-key="autoRefresh" /> <span>Enable auto-refresh</span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Refresh Interval</div> <div class="sp-radio-group"> ${[10, 30, 60, 120].map(s => ` <label class="sp-radio"> <input type="radio" name="refreshInterval" value="${s}" ${get('refreshInterval') === s ? 'checked' : ''} data-key="refreshInterval" /> <span>${s < 60 ? s + 's' : (s / 60) + 'min'}</span> </label> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Default Sort</div> <div class="sp-radio-group"> ${[ { v: 'change', l: 'Chg%' }, { v: 'volume', l: 'Volume' }, { v: 'natr', l: 'NATR' }, { v: 'symbol', l: 'Name' }, ].map(s => ` <label class="sp-radio"> <input type="radio" name="defaultSort" value="${s.v}" ${get('defaultSort') === s.v ? 'checked' : ''} data-key="defaultSort" /> <span>${s.l}</span> </label> `).join('')} </div> <div style="margin-top:8px;"> <div class="sp-radio-group"> ${[ { v: 'asc', l: 'โ†‘ Ascending' }, { v: 'desc', l: 'โ†“ Descending' }, ].map(s => ` <label class="sp-radio"> <input type="radio" name="defaultSortDir" value="${s.v}" ${get('defaultSortDir') === s.v ? 'checked' : ''} data-key="defaultSortDir" /> <span>${s.l}</span> </label> `).join('')} </div> </div> </div> <div class="sp-section"> <div class="sp-section-title">Min 24h Volume</div> <div class="sp-radio-group"> ${[ { v: 0, l: 'Off' }, { v: 10, l: '$10M+' }, { v: 50, l: '$50M+' }, { v: 100, l: '$100M+' }, { v: 250, l: '$250M+' }, { v: 500, l: '$500M+' }, ].map(s => ` <label class="sp-radio"> <input type="radio" name="minVolume" value="${s.v}" ${get('minVolume') === s.v ? 'checked' : ''} data-key="minVolume" /> <span>${s.l}</span> </label> `).join('')} </div> </div> ` } function renderIndicatorsSection() { return ` <div class="sp-section"> <div class="sp-section-title">Open Interest</div> <label class="sp-toggle"> <input type="checkbox" ${get('indicatorOI') ? 'checked' : ''} data-key="indicatorOI" /> <span>Show OI overlay on charts</span> </label> <p class="sp-hint">Displays Open Interest as a line overlay on mini-charts and modal</p> </div> <div class="sp-section"> <div class="sp-section-title">OI Line Color</div> <div class="sp-color-row"> <label class="sp-color-label"> <span>Color</span> <input type="color" value="${get('indicatorOIColor')}" data-key="indicatorOIColor" class="sp-color-input" /> </label> </div> </div> <div class="sp-section"> <p class="sp-hint">More indicators coming soon: Funding Rate, CVD, Liquidations</p> </div> ` } function renderLayoutSection() { const current = get('layout') const layouts = [ { id: '1', label: '1', icon: 'โ–ฃ' }, { id: '2h', label: '2', icon: 'โ—ซ' }, { id: '2v', label: '2V', icon: 'โฌ’' }, { id: '4', label: '2ร—2', icon: 'โŠž' }, { id: '1+3', label: '1+3', icon: 'โ—จ' }, ] return ` <div class="sp-section"> <div class="sp-section-title">Multi-Chart Layout</div> <div class="sp-layout-grid"> ${layouts.map(l => ` <button class="sp-layout-btn${current === l.id ? ' active' : ''}" data-key="layout" data-value="${l.id}" title="${l.label}"> <span class="sp-layout-icon">${l.icon}</span> <span class="sp-layout-label">${l.label}</span> </button> `).join('')} </div> </div> ` } function renderChartsSection() { return ` <div class="sp-section"> <div class="sp-section-title">Candle Type</div> <div class="sp-radio-group"> ${['Candlestick', 'Bar'].map(t => ` <label class="sp-radio"> <input type="radio" name="candleType" value="${t}" ${get('candleType') === t ? 'checked' : ''} data-key="candleType" /> <span>${t}</span> </label> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Volume Height</div> <div class="sp-slider-row"> <input type="range" min="5" max="40" value="${get('volumeHeight')}" data-key="volumeHeight" class="sp-slider" /> <span class="sp-slider-val" data-for="volumeHeight">${get('volumeHeight')}%</span> </div> </div> <div class="sp-section"> <div class="sp-section-title">Grid</div> <label class="sp-toggle"> <input type="checkbox" ${get('showGrid') ? 'checked' : ''} data-key="showGrid" /> <span>Show grid lines</span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Watermark</div> <label class="sp-toggle"> <input type="checkbox" ${get('showWatermark') ? 'checked' : ''} data-key="showWatermark" /> <span>Show symbol watermark</span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Default Timeframe</div> <div class="sp-radio-group"> ${['1m', '5m', '15m', '1h', '4h', '1d'].map(t => ` <label class="sp-radio"> <input type="radio" name="defaultTF" value="${t}" ${get('defaultTF') === t ? 'checked' : ''} data-key="defaultTF" /> <span>${t}</span> </label> `).join('')} </div> </div> <div class="sp-divider"></div> <div class="sp-section"> <div class="sp-section-title">Cards per Row</div> <div class="sp-layout-grid"> ${[1, 2, 3, 4, 5, 6].map(n => ` <button class="sp-layout-btn${get('cardsPerRow') === n ? ' active' : ''}" data-key="cardsPerRow" data-value="${n}" title="${n} per row"> <span class="sp-layout-label">${n}</span> </button> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Card Size</div> <div class="sp-radio-group"> ${['compact', 'normal', 'large'].map(s => ` <label class="sp-radio"> <input type="radio" name="cardSize" value="${s}" ${get('cardSize') === s ? 'checked' : ''} data-key="cardSize" /> <span>${s.charAt(0).toUpperCase() + s.slice(1)}</span> </label> `).join('')} </div> </div> <div class="sp-section"> <div class="sp-section-title">Sidebar Columns</div> <label class="sp-toggle"> <input type="checkbox" ${get('colChg') ? 'checked' : ''} data-key="colChg" /> <span>Chg%</span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('colNatr') ? 'checked' : ''} data-key="colNatr" /> <span>NATR</span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('colVol') ? 'checked' : ''} data-key="colVol" /> <span>Volume</span> </label> </div> ` } function renderDensitiesSection() { return ` <div class="sp-section"> <label class="sp-toggle"> <input type="checkbox" ${get('densityEnabled') ? 'checked' : ''} data-key="densityEnabled" /> <span>Show density levels on charts</span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Severity Multipliers</div> <div class="sp-severity-row"> <span class="sp-severity-dot large"></span> <span class="sp-severity-label">Large</span> <input type="number" value="${get('densitySeverityLarge')}" step="0.5" min="1" max="20" data-key="densitySeverityLarge" class="sp-num-input" /> <span class="sp-severity-x">ร—</span> </div> <div class="sp-severity-row"> <span class="sp-severity-dot medium"></span> <span class="sp-severity-label">Medium</span> <input type="number" value="${get('densitySeverityMedium')}" step="0.5" min="1" max="20" data-key="densitySeverityMedium" class="sp-num-input" /> <span class="sp-severity-x">ร—</span> </div> <div class="sp-severity-row"> <span class="sp-severity-dot small"></span> <span class="sp-severity-label">Small</span> <input type="number" value="${get('densitySeveritySmall')}" step="0.5" min="1" max="20" data-key="densitySeveritySmall" class="sp-num-input" /> <span class="sp-severity-x">ร—</span> </div> </div> <div class="sp-section"> <div class="sp-section-title">Depth from price</div> <div class="sp-slider-row"> <input type="range" min="0.5" max="10" step="0.5" value="${get('densityDepthPct')}" data-key="densityDepthPct" class="sp-slider" /> <span class="sp-slider-val" data-for="densityDepthPct">${get('densityDepthPct')}%</span> </div> </div> <div class="sp-section"> <div class="sp-section-title">Min lifetime</div> <div class="sp-slider-row"> <input type="range" min="1" max="60" value="${get('densityTTLMin')}" data-key="densityTTLMin" class="sp-slider" /> <span class="sp-slider-val" data-for="densityTTLMin">${get('densityTTLMin')} min</span> </div> </div> <div class="sp-section"> <div class="sp-section-title">Blacklist</div> <input type="text" class="sp-text-input" id="spDensityBlacklist" value="${get('densityBlacklist')}" placeholder="USDC,FDUSD,TUSD..." /> <p class="sp-hint">Comma-separated symbols to exclude from density scan</p> </div> ` } function renderSignalsSection() { const types = get('signalTypes') || [] return ` <!-- General --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>โš™๏ธ General</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <div class="sp-section-title">Min Confidence</div> <div class="sp-slider-row"> <input type="range" min="30" max="90" step="5" value="${get('signalMinConfidence')}" data-key="signalMinConfidence" class="sp-slider" /> <span class="sp-slider-val" data-for="signalMinConfidence">${get('signalMinConfidence')}%</span> </div> <p class="sp-hint">Hide signals below this confidence level</p> </div> <div class="sp-section"> <div class="sp-section-title">Alert Cooldown</div> <div class="sp-radio-group"> ${[1, 5, 15, 30].map(m => ` <label class="sp-radio"> <input type="radio" name="signalCooldown" value="${m}" ${get('signalCooldown') === m ? 'checked' : ''} data-key="signalCooldown" /> <span>${m}min</span> </label> `).join('')} </div> </div> <div class="sp-section"> <label class="sp-toggle"> <input type="checkbox" ${get('signalWatchlistOnly') ? 'checked' : ''} data-key="signalWatchlistOnly" /> <span>Watchlist coins only</span> </label> <label class="sp-toggle" style="margin-top:6px;"> <input type="checkbox" ${get('showSignalsOnCharts') ? 'checked' : ''} data-key="showSignalsOnCharts" /> <span>Show signals on charts</span> </label> <p class="sp-hint">Display signal markers on mini-charts and modal</p> </div> </div> </div> <!-- Volume Spike --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐Ÿ“Š Volume Spike</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <div class="sp-section-title">Min Ratio vs SMA(20)</div> <div class="sp-slider-row"> <input type="range" min="2" max="20" step="1" value="${get('signalMinRatio')}" data-key="signalMinRatio" class="sp-slider" /> <span class="sp-slider-val" data-for="signalMinRatio">${get('signalMinRatio')}x</span> </div> </div> <div class="sp-section"> <label class="sp-toggle"> <input type="checkbox" ${types.includes('volume_spike') ? 'checked' : ''} data-signal-type="volume_spike" /> <span>Push notifications</span> </label> </div> </div> </div> <!-- OI + CVD --> <div class="sp-group"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐Ÿ”ฎ OI + CVD</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <div class="sp-section-title">Push by sub-type</div> ${[ { key: 'oi_longs', label: '๐ŸŸข Longs Build-up' }, { key: 'oi_shorts', label: '๐Ÿ”ด Shorts Build-up' }, { key: 'oi_squeeze', label: 'โšก Squeeze' }, { key: 'oi_liquidation', label: '๐Ÿ’ฅ Liquidation' }, ].map(t => ` <label class="sp-toggle"> <input type="checkbox" ${types.includes(t.key) ? 'checked' : ''} data-signal-type="${t.key}" /> <span>${t.label}</span> </label> `).join('')} </div> </div> </div> <!-- OI Divergence --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐Ÿ”€ OI Divergence</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <p class="sp-hint">Price/OI divergence โ€” detects exhaustion or hidden accumulation</p> <label class="sp-toggle"> <input type="checkbox" ${types.includes('oi_divergence') ? 'checked' : ''} data-signal-type="oi_divergence" /> <span>Push notifications</span> </label> </div> </div> </div> <!-- Funding Squeeze --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>โšก Funding Squeeze</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <p class="sp-hint">Contrarian โ€” trades against overcrowded leverage (OI spike + extreme funding)</p> <label class="sp-toggle"> <input type="checkbox" ${types.includes('oi_funding_squeeze') ? 'checked' : ''} data-signal-type="oi_funding_squeeze" /> <span>Push notifications</span> </label> </div> </div> </div> <!-- Liq Sweep --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐ŸŽฏ Liq Sweep</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <div class="sp-section-title">Level Types</div> <p class="sp-hint">Which liquidity sources to show</p> <label class="sp-toggle"> <input type="checkbox" ${get('sweepLevelSwing') ? 'checked' : ''} data-key="sweepLevelSwing" /> <span>๐Ÿ“ Swing High/Low <span style="color:var(--text-muted);font-size:11px;">โ€” structural levels from 1h candles</span></span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('sweepLevelWall') ? 'checked' : ''} data-key="sweepLevelWall" /> <span>๐Ÿงฑ Order Book Walls <span style="color:var(--text-muted);font-size:11px;">โ€” density walls from live orderbook</span></span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('sweepLevelRound') ? 'checked' : ''} data-key="sweepLevelRound" /> <span>๐Ÿ”ข Round Numbers <span style="color:var(--text-muted);font-size:11px;">โ€” psychological price levels</span></span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Min Wick Ratio</div> <div class="sp-slider-row"> <input type="range" min="50" max="90" step="5" value="${get('sweepMinWickPct')}" data-key="sweepMinWickPct" class="sp-slider" /> <span class="sp-slider-val" data-for="sweepMinWickPct">${get('sweepMinWickPct')}%</span> </div> <p class="sp-hint">Higher = cleaner pin bars only</p> </div> <div class="sp-section"> <label class="sp-toggle"> <input type="checkbox" ${types.includes('liq_sweep') ? 'checked' : ''} data-signal-type="liq_sweep" /> <span>Push notifications</span> </label> </div> </div> </div> <!-- Channel Signal --> <div class="sp-group open"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐Ÿ“ Channel Signal</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <div class="sp-section-title">Timeframes</div> <p class="sp-hint">Uncheck to hide from feed & push (still saved to DB for stats)</p> <label class="sp-toggle"> <input type="checkbox" ${get('channelTf5m') ? 'checked' : ''} data-key="channelTf5m" /> <span>5m <span style="color:var(--text-muted);font-size:11px;">โ€” scan every 60s</span></span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('channelTf15m') ? 'checked' : ''} data-key="channelTf15m" /> <span>15m <span style="color:var(--text-muted);font-size:11px;">โ€” scan every 90s</span></span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('channelTf1h') ? 'checked' : ''} data-key="channelTf1h" /> <span>1h <span style="color:var(--text-muted);font-size:11px;">โ€” scan every 5min</span></span> </label> </div> <div class="sp-section"> <div class="sp-section-title">Signal Sub-types</div> <label class="sp-toggle"> <input type="checkbox" ${types.includes('channel') ? 'checked' : ''} data-signal-type="channel" /> <span>Push notifications</span> </label> </div> </div> </div> <!-- Notifications --> <div class="sp-group"> <div class="sp-group-header" onclick="this.parentElement.classList.toggle('open')"> <span>๐Ÿ”” Notifications</span> <span class="sp-group-arrow">โ–ถ</span> </div> <div class="sp-group-body"> <div class="sp-section"> <label class="sp-toggle"> <input type="checkbox" ${get('signalNotifications') ? 'checked' : ''} data-key="signalNotifications" /> <span>In-tab alerts</span> </label> <label class="sp-toggle"> <input type="checkbox" ${get('signalSound') ? 'checked' : ''} data-key="signalSound" /> <span>Sound alert</span> </label> <p class="sp-hint">Toast + browser notification when tab is open</p> </div> <div class="sp-section"> <div class="sp-section-title">๐Ÿ“ฒ Push Notifications</div> <label class="sp-toggle"> <input type="checkbox" ${get('signalPush') ? 'checked' : ''} data-key="signalPush" /> <span>Push when browser closed</span> </label> <p class="sp-hint">Server push โ€” works on phone & with browser closed</p> </div> </div> </div> ` } function renderResetSection() { return ` <div class="sp-section"> <div class="sp-section-title">Share Settings</div> <div style="display:flex; gap:8px;"> <button class="sp-action-btn" id="spExportSettings" style="flex:1;">๐Ÿ“ค Export</button> <button class="sp-action-btn" id="spImportSettings" style="flex:1;">๐Ÿ“ฅ Import</button> </div> <p class="sp-hint">Export your settings as a code or import someone else's</p> <div id="spShareBox" class="sp-share-box hidden"></div> </div> <div class="sp-section"> <button class="sp-danger-btn" id="spClearDrawings">Clear all drawings</button> <p class="sp-hint">Removes all lines, rays, fibs from all charts</p> </div> <div class="sp-section"> <button class="sp-danger-btn orange" id="spResetSettings">Reset all settings</button> <p class="sp-hint">Restores all settings to defaults</p> </div> ` } // --- Bind events for dynamic content --- function bindSectionEvents(container) { // Radio buttons const numericRadios = ['refreshInterval', 'minVolume', 'signalCooldown'] container.querySelectorAll('input[type="radio"][data-key]').forEach(el => { el.addEventListener('change', () => { const val = numericRadios.includes(el.dataset.key) ? Number(el.value) : el.value set(el.dataset.key, val) }) }) // Checkboxes container.querySelectorAll('input[type="checkbox"][data-key]').forEach(el => { el.addEventListener('change', () => { set(el.dataset.key, el.checked) // Web Push: subscribe/unsubscribe when push toggled (separate from in-tab notifications) if (el.dataset.key === 'signalPush') { if (el.checked) { if (typeof subscribeToPush === 'function') subscribeToPush() } else { if (typeof unsubscribeFromPush === 'function') unsubscribeFromPush() } } // Channel TF toggles: re-sync push + re-render signal list if (el.dataset.key.startsWith('channelTf')) { if (typeof subscribeToPush === 'function' && get('signalPush')) subscribeToPush() if (typeof renderSignalList === 'function') renderSignalList() } }) }) // Sliders container.querySelectorAll('input[type="range"][data-key]').forEach(el => { el.addEventListener('input', () => { const val = parseFloat(el.value) set(el.dataset.key, val) const label = container.querySelector(`[data-for="${el.dataset.key}"]`) if (label) { const suffix = el.dataset.key.includes('Ratio') ? 'x' : el.dataset.key.includes('Pct') ? '%' : el.dataset.key.includes('TTL') ? ' min' : '%' label.textContent = val + suffix } // Re-sync push filters when signal settings change if (['signalMinRatio', 'signalMinConfidence'].includes(el.dataset.key)) { if (typeof subscribeToPush === 'function' && get('signalPush')) subscribeToPush() } }) }) // Signal type checkboxes container.querySelectorAll('input[data-signal-type]').forEach(el => { el.addEventListener('change', () => { const types = get('signalTypes') || [] const t = el.dataset.signalType if (el.checked && !types.includes(t)) types.push(t) if (!el.checked) { const i = types.indexOf(t); if (i >= 0) types.splice(i, 1) } set('signalTypes', [...types]) // Re-sync push subscription with updated types if (typeof subscribeToPush === 'function' && get('signalPush')) subscribeToPush() }) }) // Number inputs container.querySelectorAll('input[type="number"][data-key]').forEach(el => { el.addEventListener('change', () => set(el.dataset.key, parseFloat(el.value))) }) // Layout buttons container.querySelectorAll('.sp-layout-btn[data-key]').forEach(btn => { btn.addEventListener('click', () => { // Only deactivate siblings with same key container.querySelectorAll(`.sp-layout-btn[data-key="${btn.dataset.key}"]`).forEach(b => b.classList.remove('active')) btn.classList.add('active') const val = btn.dataset.value set(btn.dataset.key, isNaN(val) ? val : parseInt(val)) }) }) // Density blacklist input const blInput = container.querySelector('#spDensityBlacklist') if (blInput) { let blTimer = null blInput.addEventListener('input', () => { clearTimeout(blTimer) blTimer = setTimeout(() => set('densityBlacklist', blInput.value), 500) }) } // Cleanup buttons const clearBtn = container.querySelector('#spClearDrawings') if (clearBtn) { clearBtn.addEventListener('click', () => { // Clear all drawings from localStorage const keys = Object.keys(localStorage).filter(k => k.startsWith('drawings_')) keys.forEach(k => localStorage.removeItem(k)) clearBtn.textContent = 'Cleared!' clearBtn.disabled = true setTimeout(() => { clearBtn.textContent = 'Clear all drawings'; clearBtn.disabled = false }, 2000) notify('__clearDrawings', true) }) } const resetBtn = container.querySelector('#spResetSettings') if (resetBtn) { resetBtn.addEventListener('click', () => { if (confirm('Reset all settings to defaults?')) { resetAll() resetBtn.textContent = 'Reset!' setTimeout(() => { resetBtn.textContent = 'Reset all settings' }, 2000) } }) } // Color presets container.querySelectorAll('.sp-color-preset').forEach(btn => { btn.addEventListener('click', () => { container.querySelectorAll('.sp-color-preset').forEach(b => b.classList.remove('active')) btn.classList.add('active') set('candleUp', btn.dataset.up) set('candleDown', btn.dataset.down) // Update custom color inputs + preview const upInput = container.querySelector('[data-key="candleUp"]') const downInput = container.querySelector('[data-key="candleDown"]') if (upInput) upInput.value = btn.dataset.up if (downInput) downInput.value = btn.dataset.down updateCandlePreview(container, btn.dataset.up, btn.dataset.down) }) }) // Color inputs container.querySelectorAll('.sp-color-input').forEach(el => { el.addEventListener('input', () => { set(el.dataset.key, el.value) container.querySelectorAll('.sp-color-preset').forEach(b => b.classList.remove('active')) const up = get('candleUp'), down = get('candleDown') updateCandlePreview(container, up, down) }) }) // Watchlist remove buttons container.querySelectorAll('.sp-wl-remove').forEach(btn => { btn.addEventListener('click', () => { wlRemove(btn.dataset.sym) showToast(btn.dataset.sym.replace('USDT', '') + ' removed') renderActiveSection() }) }) // Watchlist only toggle const wlOnlyToggle = container.querySelector('#spWlOnly') if (wlOnlyToggle) { wlOnlyToggle.addEventListener('change', () => { set('watchlistOnly', wlOnlyToggle.checked) showToast(wlOnlyToggle.checked ? 'Watchlist filter ON' : 'Watchlist filter OFF') }) } // Clear watchlist const clearWlBtn = container.querySelector('#spClearWatchlist') if (clearWlBtn) { clearWlBtn.addEventListener('click', () => { if (confirm('Clear entire watchlist?')) { wlClear() showToast('Watchlist cleared') renderActiveSection() } }) } // Export settings const exportBtn = container.querySelector('#spExportSettings') if (exportBtn) { exportBtn.addEventListener('click', () => { const box = container.querySelector('#spShareBox') const data = getAll() const code = btoa(JSON.stringify(data)) box.classList.remove('hidden') box.innerHTML = ` <div class="sp-share-label">Your settings code:</div> <textarea class="sp-share-textarea" id="spShareCode" readonly rows="3">${code}</textarea> <button class="sp-action-btn small" id="spCopyCode">๐Ÿ“‹ Copy</button> ` const copyBtn = box.querySelector('#spCopyCode') copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(code).then(() => { copyBtn.textContent = 'โœ… Copied!' showToast('Settings code copied') setTimeout(() => { copyBtn.textContent = '๐Ÿ“‹ Copy' }, 2000) }) }) }) } // Import settings const importBtn = container.querySelector('#spImportSettings') if (importBtn) { importBtn.addEventListener('click', () => { const box = container.querySelector('#spShareBox') box.classList.remove('hidden') box.innerHTML = ` <div class="sp-share-label">Paste settings code:</div> <textarea class="sp-share-textarea" id="spPasteCode" rows="3" placeholder="Paste code here..."></textarea> <button class="sp-action-btn small" id="spApplyCode">โœ… Apply</button> ` const applyBtn = box.querySelector('#spApplyCode') applyBtn.addEventListener('click', () => { const textarea = box.querySelector('#spPasteCode') const code = textarea.value.trim() if (!code) return try { const data = JSON.parse(atob(code)) if (typeof data !== 'object') throw new Error('bad') // Apply each setting Object.entries(data).forEach(([k, v]) => { if (DEFAULTS.hasOwnProperty(k)) set(k, v) }) applyBtn.textContent = '๐ŸŽ‰ Applied!' showToast('Settings imported successfully') setTimeout(() => { box.classList.add('hidden') }, 1500) } catch(e) { textarea.style.borderColor = '#ef4444' showToast('Invalid settings code') } }) }) } } // --- Open / Close --- function openPanel() { const panel = document.getElementById('settingsPanel') const overlay = document.getElementById('settingsOverlay') if (!panel) createPanel() document.getElementById('settingsPanel').classList.remove('hidden') document.getElementById('settingsOverlay').classList.remove('hidden') panelOpen = true renderActiveSection() } function closePanel() { const panel = document.getElementById('settingsPanel') const overlay = document.getElementById('settingsOverlay') if (panel) panel.classList.add('hidden') if (overlay) overlay.classList.add('hidden') panelOpen = false } function isOpen() { return panelOpen } // --- Wire up Settings button in header --- const settingsBtn = document.getElementById('toggleFiltersBtn') if (settingsBtn) { settingsBtn.addEventListener('click', (e) => { e.stopPropagation() openPanel() }) } // Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panelOpen) closePanel() }) // --- Toast notification --- function showToast(msg) { let toast = document.getElementById('settingsToast') if (!toast) { toast = document.createElement('div') toast.id = 'settingsToast' toast.style.cssText = 'position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:rgba(34,197,94,0.9); color:#fff; padding:8px 20px; border-radius:8px; font-size:13px; font-weight:600; z-index:9999; opacity:0; transition:opacity 0.3s; pointer-events:none;' document.body.appendChild(toast) } toast.textContent = msg toast.style.opacity = '1' clearTimeout(toast._timer) toast._timer = setTimeout(() => { toast.style.opacity = '0' }, 2000) } // --- Init --- load() loadWatchlist() return { get, set, getAll, resetAll, onChange, openPanel, closePanel, isOpen, showToast, wlAdd, wlRemove, wlToggle, wlHas, wlList, wlClear, } })()