import { state } from './state.js'; import { getSeries, generateSmartMarkers } from './chart.js'; import { updatePrice, renderDom, spawnBubble, setStatus, updateDelta } from './ui.js'; // Main Socket Controller export function startSocket() { if (state.isReplay) return; // Close existing connection if switching pairs stopSocket(); const symbol = state.symbol.toUpperCase(); const exchange = state.exchange; setStatus(`CONNECTING ${exchange}...`, 'warn'); if (exchange === 'BINANCE') { startBinanceStream(symbol); } else if (exchange === 'COINBASE') { startCoinbaseStream(symbol); } } export function stopSocket() { if (state.socket) { state.socket.close(); state.socket = null; } } /* ============================================== 1. BINANCE LOGIC (USDT Pairs) ============================================== */ function startBinanceStream(symbol) { // Normalize pair (e.g., BTC -> BTCUSDT) const pair = `${symbol}USDT`.toLowerCase(); const url = `wss://stream.binance.com:9443/ws/${pair}@kline_1m/${pair}@aggTrade/${pair}@depth10`; state.socket = new WebSocket(url); state.socket.onopen = () => { setStatus('● BINANCE LIVE', 'live'); }; state.socket.onmessage = (e) => { if (state.isReplay) return; const data = JSON.parse(e.data); const type = data.e; // --- A. KLINE (CANDLES + CHART MARKERS) --- if (type === 'kline') { handleBinanceKline(data.k); } // --- B. TRADES (SIDEBAR BUBBLES & FOOTPRINT) --- if (type === 'aggTrade') { handleBinanceTrade(data); } // --- C. DEPTH (ORDER BOOK) --- if (data.lastUpdateId) { renderDom(data.bids, data.asks); } }; state.socket.onclose = () => setStatus('DISCONNECTED', 'warn'); } function handleBinanceKline(k) { const c = { time: k.t / 1000, open: parseFloat(k.o), high: parseFloat(k.h), low: parseFloat(k.l), close: parseFloat(k.c) }; // 1. Update the Chart Series getSeries().update(c); updatePrice(c.close); // 2. Sync Global State (Vital for markers) // We replace the last candle in history if timestamps match, or push if new. const lastIdx = state.candles.length - 1; if (lastIdx >= 0 && state.candles[lastIdx].time === c.time) { state.candles[lastIdx] = c; } else { state.candles.push(c); } // 3. REAL-TIME MARKER LOGIC (The new Feature) // "x": true means this candle just closed. That's the perfect time to check if it was a whale candle. if (k.x) { // Recalculate markers based on updated history generateSmartMarkers(state.candles); } } function handleBinanceTrade(data) { const qty = parseFloat(data.q); const isMaker = data.m; // Binance: maker=true implies SELL pressure // Sidebar Bubbles if (qty > 0.02) spawnBubble(qty, isMaker); // Delta / Footprint strip updateDelta(qty, isMaker); } /* ============================================== 2. COINBASE LOGIC (USD Pairs) ============================================== */ function startCoinbaseStream(symbol) { const pair = `${symbol}-USD`; state.socket = new WebSocket('wss://ws-feed.exchange.coinbase.com'); state.socket.onopen = () => { setStatus('● COINBASE LIVE', 'live'); const msg = { type: "subscribe", product_ids: [pair], channels: ["level2", "matches", "ticker"] }; state.socket.send(JSON.stringify(msg)); }; state.socket.onmessage = (e) => { if (state.isReplay) return; const data = JSON.parse(e.data); // Price & Synthetic Candle if (data.type === 'ticker') { handleCoinbaseTicker(data); } // Trade Bubbles if (data.type === 'match') { handleCoinbaseTrade(data); } // DOM (Ignoring detailed L2 updates for this simple prototype) }; } let cbLastCandleTime = 0; function handleCoinbaseTicker(data) { const price = parseFloat(data.price); const now = Math.floor(Date.now() / 1000); const time = now - (now % 60); // Snap to minute start updatePrice(price); // Synthesize Candle logic const lastIdx = state.candles.length - 1; if (state.candles.length > 0 && state.candles[lastIdx].time === time) { // Update Existing Current Minute const c = state.candles[lastIdx]; c.close = price; c.high = Math.max(c.high, price); c.low = Math.min(c.low, price); getSeries().update(c); } else { // Create New Minute Candle const c = { time: time, open: price, high: price, low: price, close: price }; state.candles.push(c); getSeries().update(c); // Previous candle definitively "closed" because time shifted. Update markers now. if (cbLastCandleTime !== 0 && cbLastCandleTime !== time) { generateSmartMarkers(state.candles); } cbLastCandleTime = time; } } function handleCoinbaseTrade(data) { const qty = parseFloat(data.size); const isSell = data.side === 'sell'; // Coinbase is straightforward if (qty > 0.01) spawnBubble(qty, isSell); updateDelta(qty, isSell); }