/** * Orderflow Trading Terminal — Simplified * Full-screen chart with info overlay + Scanner sidebar */ // ════════════════════════════════════════════ // State // ════════════════════════════════════════════ const state = { ws: null, activeSymbol: null, instruments: [], priceChart: null, candleSeries: null, vpLines: [], tradeLines: [], signals: [], markers: [], reconnectTimer: null, reconnectDelay: 1000, timeframe: 60, range: 86400, chartType: 'candles', _candleBucket: null, _deltaCumulative: null, _biasData: {}, _vpData: {}, _strategyData: {}, _microData: {}, }; // Asset class grouping const ASSET_GROUPS = { 'Forex': ['EURUSD','GBPUSD','USDJPY','AUDUSD','USDCAD','USDCHF','NZDUSD','EURGBP','EURJPY','GBPJPY'], 'Metals': ['XAUUSDT','XAGUSD'], 'Indices': ['NAS100USDT','SP500','DJ30','DAX40','UK100'], 'Crypto': ['BTCUSDT','ETHUSDT','SOLUSDT','XRPUSDT','BNBUSDT'], 'Stocks': ['AAPL','TSLA','AMZN','MSFT','NVDA','META','GOOGL'], }; // ════════════════════════════════════════════ // Initialization // ════════════════════════════════════════════ document.addEventListener('DOMContentLoaded', async () => { await fetchInstruments(); initControls(); connectWebSocket(); setInterval(() => refreshFastData(), 1000); setInterval(() => refreshSlowData(), 5000); refreshScannerLoop(); setInterval(() => refreshScannerLoop(), 5000); window.addEventListener('resize', handleResize); updatePriceDisplay(null, null); updateScanner([]); }); // ════════════════════════════════════════════ // Controls // ════════════════════════════════════════════ function initControls() { document.querySelectorAll('.tf-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tf-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.timeframe = parseInt(btn.dataset.tf); if (state.activeSymbol) loadSymbolData(state.activeSymbol); }); }); document.querySelectorAll('.range-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.range = parseInt(btn.dataset.range); if (state.activeSymbol) loadSymbolData(state.activeSymbol); }); }); document.getElementById('symbolSelect').addEventListener('change', (e) => { switchInstrument(e.target.value); }); document.querySelectorAll('.chart-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.chart-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.chartType = btn.dataset.chart; switchChartType(state.chartType); }); }); } // ════════════════════════════════════════════ // Instruments // ════════════════════════════════════════════ async function fetchInstruments() { try { const resp = await fetch('/api/instruments'); const data = await resp.json(); state.instruments = Array.isArray(data) ? data : []; } catch (e) { state.instruments = []; } renderInstrumentDropdown(); } function renderInstrumentDropdown() { const select = document.getElementById('symbolSelect'); select.innerHTML = ''; const placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = '— Select pair —'; placeholder.disabled = true; placeholder.selected = !state.activeSymbol; select.appendChild(placeholder); for (const [groupName, symbols] of Object.entries(ASSET_GROUPS)) { const group = document.createElement('optgroup'); group.label = groupName; symbols.forEach(sym => { const opt = document.createElement('option'); opt.value = sym; opt.textContent = sym; group.appendChild(opt); }); if (group.children.length > 0) select.appendChild(group); } if (state.activeSymbol) select.value = state.activeSymbol; } // ════════════════════════════════════════════ // Data Refresh // ════════════════════════════════════════════ async function refreshFastData() { if (!state.activeSymbol) return; const sym = state.activeSymbol; const tf = state.timeframe; const range = state.range; try { const [candles, delta, micro] = await Promise.all([ fetchJSON(`/api/candles/${sym}?tf=${tf}&range=${range}`), fetchJSON(`/api/delta/${sym}?tf=${tf}&range=${range}`), fetchJSON(`/api/microstructure/${sym}`), ]); if (candles && candles.length > 0 && state.candleSeries) { state.candleSeries.setData(mapCandleData(candles)); const last = candles[candles.length - 1]; updatePriceDisplay(last.close, last.delta); state._candleBucket = { time: last.time, open: last.open, high: last.high, low: last.low, close: last.close, }; } if (delta && delta.length > 0) { state._deltaCumulative = delta[delta.length - 1].value; updateChartInfoOverlay(); } if (micro) { state._microData = micro; updateChartInfoOverlay(); } } catch (e) { /* silent */ } } async function refreshScannerLoop() { try { const scanner = await fetchJSON('/api/scanner'); if (scanner) updateScanner(scanner); } catch (e) { /* silent */ } } async function refreshSlowData() { if (!state.activeSymbol) return; const sym = state.activeSymbol; try { const [strategy, vps, signals, bias] = await Promise.all([ fetchJSON(`/api/strategy-status/${sym}`), fetchJSON(`/api/volume-profile/${sym}?range=${state.range}`), fetchJSON(`/api/signals/${sym}`), fetchJSON(`/api/bias/${sym}`), ]); if (strategy) { state._strategyData = strategy; updateTradeLines(); updateChartInfoOverlay(); } if (vps && vps.length > 0) { state._vpData = vps[0]; updateChartInfoOverlay(); } if (signals && signals.length > 0) { state.signals = signals; } if (bias) { state._biasData = bias; updateVPLines(bias); updateChartInfoOverlay(); } } catch (e) { /* silent */ } } // ════════════════════════════════════════════ // Symbol Loading // ════════════════════════════════════════════ async function switchInstrument(symbol) { state.activeSymbol = symbol; const select = document.getElementById('symbolSelect'); if (select.value !== symbol) select.value = symbol; const overlay = document.getElementById('waitingOverlay'); if (overlay) overlay.classList.add('hidden'); if (!state.priceChart) createPriceChart(); await loadSymbolData(symbol); } async function loadSymbolData(symbol) { const tf = state.timeframe; const range = state.range; state._candleBucket = null; state._deltaCumulative = null; const [candles, delta, bias, vps, strategy, signals, micro] = await Promise.all([ fetchJSON(`/api/candles/${symbol}?tf=${tf}&range=${range}`), fetchJSON(`/api/delta/${symbol}?tf=${tf}&range=${range}`), fetchJSON(`/api/bias/${symbol}`), fetchJSON(`/api/volume-profile/${symbol}?range=${range}`), fetchJSON(`/api/strategy-status/${symbol}`), fetchJSON(`/api/signals/${symbol}`), fetchJSON(`/api/microstructure/${symbol}`), ]); // Price chart if (candles && candles.length > 0 && state.candleSeries) { state.candleSeries.setData(mapCandleData(candles)); if (state.chartType === 'baseline' && candles.length > 0) { state.candleSeries.applyOptions({ baseValue: { type: 'price', price: candles[0].open }, }); } } else if (state.candleSeries) { state.candleSeries.setData([]); } // Delta (for overlay) if (delta && delta.length > 0) { state._deltaCumulative = delta[delta.length - 1].value; } // Bias + VP lines if (bias) { state._biasData = bias; updateVPLines(bias); } if (vps && vps.length > 0) state._vpData = vps[0]; if (strategy) { state._strategyData = strategy; updateTradeLines(); } if (signals && signals.length > 0) state.signals = signals; if (micro) state._microData = micro; // Price display if (candles && candles.length > 0) { const last = candles[candles.length - 1]; updatePriceDisplay(last.close, last.delta); state._candleBucket = { time: last.time, open: last.open, high: last.high, low: last.low, close: last.close, }; } // Chart markers const markers = await fetchJSON(`/api/markers/${symbol}`); state.markers = markers || []; applyMarkers(); updateChartInfoOverlay(); } async function fetchJSON(url) { try { const resp = await fetch(url); if (!resp.ok) return null; return await resp.json(); } catch { return null; } } // ════════════════════════════════════════════ // TradingView Lightweight Charts // ════════════════════════════════════════════ function createPriceChart() { const container = document.getElementById('priceChartContainer'); state.priceChart = LightweightCharts.createChart(container, { autoSize: true, layout: { background: { type: 'solid', color: '#1c2128' }, textColor: '#8b949e', fontSize: 11, fontFamily: "'Consolas', monospace", }, grid: { vertLines: { color: 'rgba(48, 54, 61, 0.5)' }, horzLines: { color: 'rgba(48, 54, 61, 0.5)' }, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, vertLine: { color: 'rgba(88, 166, 255, 0.3)', width: 1 }, horzLine: { color: 'rgba(88, 166, 255, 0.3)', width: 1 }, }, rightPriceScale: { borderColor: '#30363d', scaleMargins: { top: 0.1, bottom: 0.1 }, }, timeScale: { borderColor: '#30363d', timeVisible: true, secondsVisible: false, }, handleScroll: { vertTouchDrag: false }, }); state.candleSeries = state.priceChart.addCandlestickSeries({ upColor: '#3fb950', downColor: '#f85149', borderUpColor: '#3fb950', borderDownColor: '#f85149', wickUpColor: '#3fb950', wickDownColor: '#f85149', }); } // ════════════════════════════════════════════ // Chart Type Switching // ════════════════════════════════════════════ function switchChartType(type) { if (!state.priceChart) return; if (state.candleSeries) { state.priceChart.removeSeries(state.candleSeries); state.candleSeries = null; } switch (type) { case 'line': state.candleSeries = state.priceChart.addLineSeries({ color: '#58a6ff', lineWidth: 2 }); break; case 'area': state.candleSeries = state.priceChart.addAreaSeries({ lineColor: '#58a6ff', topColor: 'rgba(88, 166, 255, 0.25)', bottomColor: 'rgba(88, 166, 255, 0.02)', lineWidth: 2, }); break; case 'bars': state.candleSeries = state.priceChart.addBarSeries({ upColor: '#3fb950', downColor: '#f85149' }); break; case 'baseline': state.candleSeries = state.priceChart.addBaselineSeries({ baseValue: { type: 'price', price: 0 }, topLineColor: '#3fb950', topFillColor1: 'rgba(63, 185, 80, 0.2)', topFillColor2: 'rgba(63, 185, 80, 0.02)', bottomLineColor: '#f85149', bottomFillColor1: 'rgba(248, 81, 73, 0.02)', bottomFillColor2: 'rgba(248, 81, 73, 0.2)', lineWidth: 2, }); break; default: state.candleSeries = state.priceChart.addCandlestickSeries({ upColor: '#3fb950', downColor: '#f85149', borderUpColor: '#3fb950', borderDownColor: '#f85149', wickUpColor: '#3fb950', wickDownColor: '#f85149', }); break; } if (state.activeSymbol) loadSymbolData(state.activeSymbol); } function mapCandleData(candles) { const type = state.chartType; if (type === 'line' || type === 'area' || type === 'baseline') { return candles.map(c => ({ time: c.time, value: c.close })); } return candles.map(c => ({ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close })); } function mapCandleUpdate(bucket) { const type = state.chartType; if (type === 'line' || type === 'area' || type === 'baseline') { return { time: bucket.time, value: bucket.close }; } return { ...bucket }; } // ════════════════════════════════════════════ // Chart Markers // ════════════════════════════════════════════ function signalToMarker(sig) { const isBuy = (sig.direction === 'buy' || sig.direction === 'long'); const time = Math.floor((sig.timestamp_ms || sig.receivedAt || Date.now()) / 1000); const sigType = (sig.signals && sig.signals[0] && sig.signals[0].signal_type) || sig.signal_type || ''; const action = sig.action || 'alert_only'; if (action === 'enter') return { time, position: isBuy ? 'belowBar' : 'aboveBar', color: isBuy ? '#00e676' : '#ff1744', shape: 'circle', text: 'ENTRY' }; if (action === 'break_even') return { time, position: 'aboveBar', color: '#42a5f5', shape: 'square', text: 'BE' }; if (action === 'trail') return { time, position: 'aboveBar', color: '#26c6da', shape: 'square', text: 'TRAIL' }; if (action === 'exit') return { time, position: 'aboveBar', color: '#ff1744', shape: 'circle', text: 'EXIT' }; if (action === 'exit_warning') return { time, position: 'aboveBar', color: '#ffeb3b', shape: 'circle', text: 'WARN' }; if (sigType.includes('absorption')) return { time, position: isBuy ? 'belowBar' : 'aboveBar', color: isBuy ? '#26a69a' : '#ef5350', shape: isBuy ? 'arrowUp' : 'arrowDown', text: 'ABS' }; if (sigType.includes('initiative')) return { time, position: isBuy ? 'belowBar' : 'aboveBar', color: isBuy ? '#66bb6a' : '#ffa726', shape: isBuy ? 'arrowUp' : 'arrowDown', text: 'INIT' }; if (sigType.includes('sweep')) return { time, position: 'aboveBar', color: '#ab47bc', shape: 'arrowDown', text: 'SWEEP' }; if (sigType.includes('exhaustion')) return { time, position: 'aboveBar', color: '#ffeb3b', shape: 'circle', text: 'EXHAUST' }; if (sigType.includes('divergence')) return { time, position: 'aboveBar', color: '#ff9800', shape: 'circle', text: 'DIV' }; return { time, position: 'aboveBar', color: '#9e9e9e', shape: 'circle', text: 'SIG' }; } function applyMarkers() { if (!state.candleSeries) return; if (state.chartType !== 'candles' && state.chartType !== 'bars') return; const sorted = [...state.markers].sort((a, b) => a.time - b.time); state.candleSeries.setMarkers(sorted); } // ════════════════════════════════════════════ // VP Lines on Chart // ════════════════════════════════════════════ function updateVPLines(bias) { if (!state.candleSeries) return; state.vpLines.forEach(line => { try { state.candleSeries.removePriceLine(line); } catch {} }); state.vpLines = []; const lineConfigs = [ { price: bias.poc, title: 'POC', color: '#d29922', style: 0, width: 2 }, { price: bias.vah, title: 'VAH', color: '#f85149', style: 2, width: 1 }, { price: bias.val, title: 'VAL', color: '#3fb950', style: 2, width: 1 }, ]; if (bias.merged_vah) lineConfigs.push({ price: bias.merged_vah, title: 'M-VAH', color: '#f85149', style: 1, width: 1 }); if (bias.merged_val) lineConfigs.push({ price: bias.merged_val, title: 'M-VAL', color: '#3fb950', style: 1, width: 1 }); if (bias.qualified_levels && bias.qualified_levels.length > 0) { bias.qualified_levels.forEach(lv => { const c = lv.direction === 'buy' ? '#3fb950' : '#f85149'; const label = `${lv.level_type || 'LV'} ${lv.direction === 'buy' ? '▲' : '▼'}`; lineConfigs.push({ price: lv.price, title: label, color: c, style: 1, width: 1 }); }); } lineConfigs.forEach(cfg => { if (cfg.price && cfg.price > 0) { const line = state.candleSeries.createPriceLine({ price: cfg.price, color: cfg.color, lineWidth: cfg.width || 1, lineStyle: cfg.style, axisLabelVisible: true, title: cfg.title, }); state.vpLines.push(line); } }); } // ════════════════════════════════════════════ // Trade Lines (Entry / SL / TP) // ════════════════════════════════════════════ function updateTradeLines() { if (!state.candleSeries) return; state.tradeLines.forEach(line => { try { state.candleSeries.removePriceLine(line); } catch {} }); state.tradeLines = []; const strategy = state._strategyData || {}; const trade = strategy.trade || {}; const phase = (strategy.phase || trade.phase || '').toLowerCase(); // Only draw when there's an active trade if (!phase || phase === 'idle' || phase === 'none' || phase === 'closed' || phase === 'waiting_for_price') return; const lines = []; if (trade.entry_price && trade.entry_price > 0) { lines.push({ price: trade.entry_price, title: '► ENTRY', color: '#58a6ff', style: 0, width: 2 }); } if (trade.stop_loss && trade.stop_loss > 0) { lines.push({ price: trade.stop_loss, title: '✕ SL', color: '#f85149', style: 2, width: 2 }); } if (trade.take_profit && trade.take_profit > 0) { lines.push({ price: trade.take_profit, title: '✓ TP', color: '#3fb950', style: 2, width: 2 }); } lines.forEach(cfg => { const line = state.candleSeries.createPriceLine({ price: cfg.price, color: cfg.color, lineWidth: cfg.width, lineStyle: cfg.style, axisLabelVisible: true, title: cfg.title, }); state.tradeLines.push(line); }); } // ════════════════════════════════════════════ // Scanner // ════════════════════════════════════════════ const _SCAN_RANK = { 'IN_TRADE': 100, 'TRAILING': 95, 'BREAK_EVEN': 90, 'ENTRY_READY': 80, 'AT_LEVEL_SCANNING': 60, 'WATCHING': 50, 'WAITING_FOR_PRICE': 20, 'IDLE': 10, }; function updateScanner(pairs) { const feed = document.getElementById('scannerFeed'); const countEl = document.getElementById('scannerCount'); if (!feed || !countEl) return; pairs = Array.isArray(pairs) ? pairs : []; countEl.textContent = pairs.length; if (pairs.length === 0) { feed.innerHTML = '