// The wallets liquidated for the most over the last 7 days, in a table, and the biggest wallet
// liquidated on a bar marked with its address. Hyperliquid names the liquidated wallet on every
// liquidation; Lighter names an account index, shown as it is. One position is often liquidated in
// several fills a moment apart, so fills of one wallet within a minute count as one liquidation.
indicator('Most-liquidated wallets', { overlay: true, shortName: 'Liquidated wallets' });
const rows = input.int('Wallets', 8, { min: 1, max: 20 });
const markUsd = input.float('Mark a wallet from ($)', 1_000_000, { min: 0 });
const position = input.choice('Position', 'top-right', ['top-left', 'top-right', 'bottom-left', 'bottom-right'], { legend: false });
const chart = bars();
const liq = liquidations();
// Per wallet, across every bar. An event handler may change only tallies and live state.
const usdBy = state.map<number>(50_000, 'live');
const timesBy = state.map<number>(50_000, 'live');
const longUsdBy = state.map<number>(50_000, 'live');
const lastSeen = state.map<number>(50_000, 'live');
// This bar's liquidated USD per wallet, for the marker.
const thisBar = tallyBy(2_000);
const marks = plot.markers('Big wallet liquidations');
const table = draw.table({ position, columns: 4, rows: 21, background: color.withAlpha(color.panel, 0.92), border: color.withAlpha(color.neutral, 0.12), frame: color.withAlpha(color.neutral, 0.28) });
const MINUTE = 60_000;
const short = (wallet: string) => (wallet.length > 12 ? `${wallet.slice(0, 6)}…${wallet.slice(-4)}` : wallet);
liq.onLiquidation((l) => {
if (l.user === null) return;
const wallet = l.user.toLowerCase();
const last = lastSeen.get(wallet);
if (last === undefined || l.time - last > MINUTE) timesBy.set(wallet, (timesBy.get(wallet) ?? 0) + 1);
lastSeen.set(wallet, l.time);
usdBy.set(wallet, (usdBy.get(wallet) ?? 0) + l.notional);
if (l.side === 'long') longUsdBy.set(wallet, (longUsdBy.get(wallet) ?? 0) + l.notional);
thisBar.add(wallet, l.side === 'long' ? l.notional : -l.notional);
});
chart.onBar(() => {
let top = '';
let topUsd = 0;
for (const [wallet, usd] of thisBar.entries()) {
if (Math.abs(usd) > Math.abs(topUsd)) { top = wallet; topUsd = usd; }
}
// `available` is false on bars older than the history, and on the newest while it can still change.
if (liq.available && Math.abs(topUsd) >= markUsd) {
// Longs are liquidated on the way down, so their mark sits under the candle.
const long = topUsd > 0;
marks.add({ position: long ? 'below' : 'above', shape: 'circle', text: `${short(top)} ${fmt.usd(Math.abs(topUsd))}`, color: long ? color.down : color.up });
}
if (!chart.isLast) return;
const ranked = [...usdBy.entries()].sort((a, b) => b[1] - a[1]).slice(0, rows);
const dim = color.withAlpha(color.neutral, 0.7);
table.clear();
table.cell(0, 0, 'Wallet', { align: 'left', size: 'small', color: dim });
table.cell(1, 0, 'Liquidated', { align: 'right', size: 'small', color: dim });
table.cell(2, 0, 'Times', { align: 'right', size: 'small', color: dim });
table.cell(3, 0, 'Side', { align: 'left', size: 'small', color: dim });
ranked.forEach(([wallet, usd], i) => {
const longShare = (longUsdBy.get(wallet) ?? 0) / usd;
const side = longShare > 0.5 ? 'Long' : 'Short';
table.cell(0, i + 1, short(wallet), { align: 'left', tooltip: wallet });
table.cell(1, i + 1, fmt.usd(usd), { align: 'right' });
table.cell(2, i + 1, String(timesBy.get(wallet) ?? 0), { align: 'right' });
table.cell(3, i + 1, side, { align: 'left', color: side === 'Long' ? color.down : color.up });
});
});