feat(mse): whale trade circles on chart via aggTrade canvas overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-25 13:30:03 +03:00
parent 0411e43c08
commit b3975789fa

View file

@ -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 @@
<div id="center-pane">
<div id="chart-container">
<div id="chart"></div>
<canvas id="whale-canvas"></canvas>
</div>
<div id="log-section">
<div class="pane-hdr">
@ -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();