Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions src/features/swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,29 @@ function hoverPosition(tokenId) {
document.querySelectorAll('#liquidity-positions tr[data-position]').forEach(row => row.classList.toggle('bg-white/5', row.dataset.position === tokenId));
depthChart?.update('none');
}
/**
* The wallet's share of each level (its fees' share while the price trades there): its positions' USDC over the
* level's, both L (√hi - √lo) in USD prices / 10^12 (USDC 6 decimals, DATA 18, whichever is token0). Per level the
* share of the whole level (the tooltip), and per position the part of the level inside its range with the share
* there (drawn: a range ending inside a level covers that part of the bar only)
*/
function levelShares(levels, ranges) {
const held = ranges.filter(r => r.L > 0);
return levels.map(l => {
const parts = [];
let mine = 0;
if (held.length && l.usd > 0) {
for (const r of held) {
const [lo, hi] = [Math.max(l.lo, r.min), Math.min(l.hi, r.max)];
if (!(hi > lo)) continue;
mine += r.L * (Math.sqrt(hi) - Math.sqrt(lo)) * 1e-12;
parts.push({ lo, hi, share: Math.min(1, r.L * (Math.sqrt(l.hi) - Math.sqrt(l.lo)) * 1e-12 / l.usd) });
}
}
return { share: l.usd > 0 ? Math.min(1, mine / l.usd) : 0, parts };
});
}

const DEPTH_STEPS = [0.01, 0.02, 0.04, 0.08, 0.16]; // 25 levels each way: x1.28 up to x41

/** The pool's liquidity by price in bars (USDC blue, DATA orange), the price between them, the wallet's ranges shaded behind */
Expand All @@ -1028,7 +1051,10 @@ function renderDepth(depth, ranges) {
const colors = levels.map(l => (l.side === 'bid' ? 'rgba(59, 130, 246, 0.6)' : 'rgba(249, 115, 22, 0.6)'));
const data = {
labels: levels.map(l => formatPrice(Math.sqrt(l.lo * l.hi))),
datasets: [{ data: levels.map(l => l.usd), backgroundColor: colors, borderWidth: 0, barPercentage: 1, categoryPercentage: 0.9, levels, ranges }]
datasets: [{ data: levels.map(l => l.usd), backgroundColor: colors, borderWidth: 0, barPercentage: 1, categoryPercentage: 0.9, levels, ranges, ...(() => {
const shares = levelShares(levels, ranges);
return { shares: shares.map(x => x.share), shareParts: shares.map(x => x.parts) };
})() }]
};
// Wider levels show a wider range of prices
const zoom = $('liquidity-zoom');
Expand Down Expand Up @@ -1086,6 +1112,29 @@ function renderDepth(depth, ranges) {
}
};
// The price: a line between the bids and the asks
// The wallet's share of each level: the bar's lower part, in white, over the prices of its ranges only
const shareBars = {
id: 'liquidityShares',
afterDatasetsDraw(chart) {
const { shareParts, levels } = chart.data.datasets[0];
const bars = chart.getDatasetMeta(0).data;
if (!shareParts?.some(parts => parts.length) || bars.length !== levels.length) return;
const { ctx } = chart;
ctx.save();
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
bars.forEach((bar, k) => {
const { x, y, base, width } = bar.getProps(['x', 'y', 'base', 'width'], true);
const l = levels[k];
// A price's x inside the bar, in proportion of the level's log width
const at = (price) => x - width / 2 + Math.log(price / l.lo) / Math.log(l.hi / l.lo) * width;
for (const part of shareParts[k]) {
const height = (base - y) * part.share;
ctx.fillRect(at(part.lo), base - height, at(part.hi) - at(part.lo), height);
}
});
ctx.restore();
}
};
const priceLine = {
id: 'liquidityPrice',
afterDatasetsDraw(chart) {
Expand All @@ -1109,7 +1158,7 @@ function renderDepth(depth, ranges) {
depthChart = new Chart(container.querySelector('canvas').getContext('2d'), {
type: 'bar',
data,
plugins: [rangeBands, priceLine],
plugins: [rangeBands, shareBars, priceLine],
options: {
responsive: true,
maintainAspectRatio: false,
Expand Down Expand Up @@ -1140,7 +1189,9 @@ function renderDepth(depth, ranges) {
const total = l.side === 'bid'
? `Total USDC ${Utils.formatBigNumber(path.reduce((sum, x) => sum + x.counter, 0).toFixed(2))}`
: `Total DATA ${formatData(data)}`;
return [total, `Average price ${formatPrice(data > 0 ? usd / data : l.lo)}`];
const share = item.dataset.shares?.[item.dataIndex] || 0;
const yours = share > 0 ? [`Your share ${share < 0.01 ? '<1' : Math.round(share * 100)}%`] : [];
return [total, `Average price ${formatPrice(data > 0 ? usd / data : l.lo)}`, ...yours];
}
}
}
Expand Down Expand Up @@ -1187,7 +1238,7 @@ function fitDraft() {

/** The ranges on the chain's chart: the wallet's open positions, and the new position's while it is being made */
function depthRanges() {
const ranges = (Liquidity.positions() || []).filter(p => p.chain === state.chain && !p.closed).map(p => ({ tokenId: p.tokenId, min: p.min, max: p.max }));
const ranges = (Liquidity.positions() || []).filter(p => p.chain === state.chain && !p.closed).map(p => ({ tokenId: p.tokenId, min: p.min, max: p.max, L: Number(p.liquidity) }));
const draft = LiquidityManage.draft(state.chain);
return draft ? [...ranges, { tokenId: 'new', draft: true, ...draft }] : ranges;
}
Expand Down
22 changes: 22 additions & 0 deletions tests/swap.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,28 @@ test('the Liquidity tab shows the pool, and the wallet\'s v4 positions with thei
expect(dataset.colors.filter(c => c.startsWith('rgba(59, 130, 246')).length).toBe(25);
expect(dataset.ranges).toHaveLength(1);
expect(dataset.ranges[0].min).toBeCloseTo(0.00023518, 7);
// The wallet's share of each level: all of it inside its range (the mocked pool holds that position only), none outside
const shares = await page.evaluate(() => {
const d = window.Chart.getChart(document.querySelector('#liquidity-depth canvas')).data.datasets[0];
const inside = d.levels.map((l, k) => [l, d.shares[k]]).filter(([l]) => l.lo >= d.ranges[0].min && l.hi <= d.ranges[0].max);
const outside = d.levels.map((l, k) => [l, d.shares[k]]).filter(([l]) => l.hi <= d.ranges[0].min || l.lo >= d.ranges[0].max);
const k = d.levels.findIndex(l => l.side === 'ask');
const chart = window.Chart.getChart(document.querySelector('#liquidity-depth canvas'));
return { inside: inside.map(([, x]) => x), outside: outside.map(([, x]) => x), label: chart.options.plugins.tooltip.callbacks.label({ dataset: d, dataIndex: k }) };
});
expect(shares.inside.length).toBeGreaterThan(2);
shares.inside.forEach(x => expect(x).toBeCloseTo(1, 2));
shares.outside.forEach(x => expect(x).toBe(0));
expect(shares.label[2]).toBe('Your share 100%');
// A level the range only enters: its white part over the range's prices, not the whole bar
const edge = await page.evaluate(() => {
const d = window.Chart.getChart(document.querySelector('#liquidity-depth canvas')).data.datasets[0];
const k = d.levels.findIndex(l => l.lo < d.ranges[0].min && l.hi > d.ranges[0].min);
return k < 0 ? null : { level: d.levels[k], parts: d.shareParts[k] };
});
expect(edge.parts).toHaveLength(1);
expect(edge.parts[0].lo).toBeCloseTo(edge.level.lo < 0 ? 0 : Math.max(edge.level.lo, 0.00023518), 7);
expect(edge.parts[0].hi).toBe(edge.level.hi);
// A bar's tooltip sums the levels from the price to it (as the Depth): the second ask holds the first two
const tooltip = await page.evaluate(() => {
const chart = window.Chart.getChart(document.querySelector('#liquidity-depth canvas'));
Expand Down
Loading