From b3975789fa853d7127e2c81d384557d9e9504e75 Mon Sep 17 00:00:00 2001 From: Mukan Erkin Date: Sat, 25 Apr 2026 13:30:03 +0300 Subject: [PATCH] feat(mse): whale trade circles on chart via aggTrade canvas overlay Co-Authored-By: Claude Sonnet 4.6 --- src/web/bot.html | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/web/bot.html b/src/web/bot.html index 995c774..54b0c16 100644 --- a/src/web/bot.html +++ b/src/web/bot.html @@ -104,6 +104,7 @@ #center-pane { display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--border); } #chart-container { flex: 6; min-height: 0; position: relative; } #chart { width: 100%; height: 100%; } + #whale-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; } #log-section { flex: 3; display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border); } #log-terminal { flex: 1; background: #0a0a0e; padding: 8px 10px; overflow-y: auto; @@ -239,6 +240,7 @@
+
@@ -504,6 +506,8 @@ function initChart() { }).observe(container); loadKlines(); startKlineWs(); + initWhaleCanvas(); + startAggTradeWs(); } async function loadKlines() { @@ -610,6 +614,89 @@ function drawSupportResistance(candles) { }); } +// ── Whale trades (aggTrade canvas overlay) ──────── +const WHALE_THRESHOLD = 10000; // USDT +const WHALE_MAX_AGE = 120; // saniye, ekranda kalma süresi +const WHALE_MAX_R = 28; // maks çember yarıçapı (px) +const WHALE_MIN_R = 6; // min çember yarıçapı (px) +const WHALE_SCALE_AT = 500000; // bu hacimde max boyuta ulaşır + +let whaleCanvas = null; +let whaleCtx = null; +let aggTradeWs = null; +let whaleTrades = []; // { price, qty, isBuy, usdtVal, ts } +let whaleRafId = null; + +function startAggTradeWs() { + if (aggTradeWs) aggTradeWs.close(); + const sym = bot.symbol.toLowerCase(); + aggTradeWs = new WebSocket(`wss://stream.binance.com:9443/ws/${sym}@aggTrade`); + aggTradeWs.onmessage = e => { + const t = JSON.parse(e.data); + const price = parseFloat(t.p); + const qty = parseFloat(t.q); + const usdtVal = price * qty; + if (usdtVal < WHALE_THRESHOLD) return; + whaleTrades.push({ price, qty, isBuy: !t.m, usdtVal, ts: Date.now() / 1000 }); + if (whaleTrades.length > 500) whaleTrades.shift(); + }; + aggTradeWs.onerror = () => {}; + aggTradeWs.onclose = () => setTimeout(startAggTradeWs, 5000); +} + +function initWhaleCanvas() { + whaleCanvas = document.getElementById('whale-canvas'); + whaleCtx = whaleCanvas.getContext('2d'); + const container = document.getElementById('chart-container'); + function resize() { + whaleCanvas.width = container.clientWidth; + whaleCanvas.height = container.clientHeight; + } + resize(); + new ResizeObserver(resize).observe(container); + renderWhaleTrades(); +} + +function renderWhaleTrades() { + whaleRafId = requestAnimationFrame(renderWhaleTrades); + if (!whaleCtx || !chart || !candleSeries) return; + + const W = whaleCanvas.width; + const H = whaleCanvas.height; + whaleCtx.clearRect(0, 0, W, H); + + const now = Date.now() / 1000; + // eski trade'leri temizle + whaleTrades = whaleTrades.filter(t => now - t.ts < WHALE_MAX_AGE); + + for (const t of whaleTrades) { + const x = chart.timeScale().timeToCoordinate(Math.floor(t.ts)); + const y = candleSeries.priceToCoordinate(t.price); + if (x === null || y === null) continue; + + const age = now - t.ts; + const fade = Math.max(0, 1 - age / WHALE_MAX_AGE); + const ratio = Math.min(t.usdtVal / WHALE_SCALE_AT, 1); + const r = WHALE_MIN_R + (WHALE_MAX_R - WHALE_MIN_R) * ratio; + const alpha = fade * (t.isBuy ? 0.75 : 0.65); + + const color = t.isBuy + ? `rgba(46,204,113,${alpha})` + : `rgba(231,76,60,${alpha})`; + const stroke = t.isBuy + ? `rgba(46,204,113,${Math.min(1, alpha * 1.4)})` + : `rgba(231,76,60,${Math.min(1, alpha * 1.4)})`; + + whaleCtx.beginPath(); + whaleCtx.arc(x, y, r, 0, Math.PI * 2); + whaleCtx.fillStyle = color; + whaleCtx.fill(); + whaleCtx.strokeStyle = stroke; + whaleCtx.lineWidth = 1.5; + whaleCtx.stroke(); + } +} + let klineWs = null; function startKlineWs() { if (klineWs) klineWs.close();