Spaces:
Running
Running
| 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); | |
| } |