From 6a012c5fd9cf15d41a3f4591f1b5ca1d9341d9f4 Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 16 Aug 2025 18:22:32 +0200 Subject: [PATCH] price multipliers instead of ticks --- .gitignore | 2 + onchain/analysis/scenario-visualizer.html | 313 +++++++++++++--------- 2 files changed, 186 insertions(+), 129 deletions(-) diff --git a/.gitignore b/.gitignore index 4678681..254ea10 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ docs/ *~ *.swp *.swo +.playwright-mcp/ + diff --git a/onchain/analysis/scenario-visualizer.html b/onchain/analysis/scenario-visualizer.html index 04fc0d0..42301c1 100644 --- a/onchain/analysis/scenario-visualizer.html +++ b/onchain/analysis/scenario-visualizer.html @@ -125,10 +125,19 @@

Kraiken Liquidity Position Simulator

- 📊 Anti-Arbitrage Three-Position Strategy
- Floor: Deep liquidity position - contains ETH when below current price, KRAIKEN when above
- Anchor: Shallow liquidity around current price for fast slippage
- Discovery: Edge liquidity position - contains KRAIKEN when below current price, ETH when above + 📊 Anti-Arbitrage Three-Position Strategy (Uniswap V3 1% Pool)
+
+ Position Strategy:
+ Floor: Deep liquidity position - holds ETH when ETH is cheap (below current price)
+ Anchor: Shallow liquidity around current price for fast price discovery
+ Discovery: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)
+
+ Price Multiples: Shows ETH price relative to current (1x):
+ • 0.5x = ETH is half as expensive (Floor position holds ETH)
+ • 1x = Current ETH price (red dashed line)
+ • 2x = ETH is twice as expensive (Discovery position holds KRAIKEN)
+
+ Note: The x-axis automatically adjusts based on token ordering in the pool
Loading profitable scenario data...
@@ -310,6 +319,24 @@ return Math.sqrt(price); } + // Convert tick to price multiple relative to current price + // This represents ETH price multiples (how expensive ETH is relative to current) + function tickToPriceMultiple(tick, currentTick, token0isWeth) { + const price = tickToPrice(tick); + const currentPrice = tickToPrice(currentTick); + + if (token0isWeth) { + // When ETH is token0, price = KRAIKEN/ETH + // We want ETH price multiple, so we need to invert + // Higher tick = more KRAIKEN per ETH = cheaper ETH + return currentPrice / price; + } else { + // When KRAIKEN is token0, price = ETH/KRAIKEN + // This is already ETH price, so just divide + return price / currentPrice; + } + } + // Calculate the invariant liquidity value from token amounts // This represents the actual liquidity deployed to the position, independent of current price function calculateInvariantLiquidity(token0Amount, token1Amount, tickLower, tickUpper, positionName = '') { @@ -486,24 +513,6 @@ header.innerHTML = `

${precedingAction}

`; scenarioContainer.appendChild(header); - // Create legend - const legend = document.createElement('div'); - legend.className = 'legend'; - legend.innerHTML = ` -
-
- Floor Position (Foundation) -
-
-
- Anchor Position (Current Price) -
-
-
- Discovery Position (Growth) -
- `; - scenarioContainer.appendChild(legend); // Create charts container const chartsContainer = document.createElement('div'); @@ -526,29 +535,60 @@ scenarioContainer.appendChild(chartsContainer); // Create summary panel - const summaryPanel = createSummaryPanel(positions, currentTick); + const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth); scenarioContainer.appendChild(summaryPanel); // Add to page document.getElementById('simulations').appendChild(scenarioContainer); // Create the combined chart - createCombinedChart(combinedChart, positions, currentTick, totalLiquidity); + createCombinedChart(combinedChart, positions, currentTick, totalLiquidity, token0isWeth); } - function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity) { + function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) { const positionKeys = ['floor', 'anchor', 'discovery']; - // Calculate bar widths to represent actual tick ranges - const barWidths = positionKeys.map(key => { + // Convert positions to price multiples + const priceMultiplePositions = {}; + + positionKeys.forEach(key => { const pos = positions[key]; - return pos.tickUpper - pos.tickLower; // Width = actual tick range + const lowerMultiple = tickToPriceMultiple(pos.tickLower, currentTick, token0isWeth); + const upperMultiple = tickToPriceMultiple(pos.tickUpper, currentTick, token0isWeth); + const centerMultiple = tickToPriceMultiple(pos.tickLower + (pos.tickUpper - pos.tickLower) / 2, currentTick, token0isWeth); + + priceMultiplePositions[key] = { + lowerMultiple: lowerMultiple, + upperMultiple: upperMultiple, + centerMultiple: centerMultiple, + width: upperMultiple - lowerMultiple, + ...pos + }; + + console.log(`Position ${key}:`, { + ticks: [pos.tickLower, pos.tickUpper], + currentTick: currentTick, + multiples: [lowerMultiple, upperMultiple], + centerMultiple: centerMultiple, + token0isWeth: token0isWeth + }); + + // Warn about extreme positions + if (pos.tickLower > 180000 || pos.tickUpper > 180000) { + console.warn(`EXTREME TICKS: ${key} position has ticks above 180000, which represents extreme price multiples`); + } + }); + + // Calculate bar widths to represent actual price multiple ranges + const barWidths = positionKeys.map(key => { + const pos = priceMultiplePositions[key]; + return pos.width; // Width in price multiple space }); - // Calculate bar positions (centered in tick ranges) + // Calculate bar positions (centered in price multiple ranges) const barPositions = positionKeys.map(key => { - const pos = positions[key]; - return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2; + const pos = priceMultiplePositions[key]; + return pos.centerMultiple; }); // ETH trace (left y-axis) @@ -572,8 +612,8 @@ } }, text: positionKeys.map(key => { - const pos = positions[key]; - return `${pos.name}
ETH: ${pos.eth.toFixed(6)}
Range: [${pos.tickLower}, ${pos.tickUpper}]`; + const pos = priceMultiplePositions[key]; + return `${pos.name}
ETH: ${pos.eth.toFixed(6)}
Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`; }), hoverinfo: 'text' }; @@ -603,20 +643,25 @@ } }, text: positionKeys.map(key => { - const pos = positions[key]; - return `${pos.name}
KRAIKEN: ${pos.kraiken.toFixed(6)}
Range: [${pos.tickLower}, ${pos.tickUpper}]`; + const pos = priceMultiplePositions[key]; + return `${pos.name}
KRAIKEN: ${pos.kraiken.toFixed(6)}
Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`; }), hoverinfo: 'text' }; // Calculate x-axis range based on position ranges with some padding - const allTicks = positionKeys.flatMap(key => [positions[key].tickLower, positions[key].tickUpper]); - const minTick = Math.min(...allTicks); - const maxTick = Math.max(...allTicks); - const tickRange = maxTick - minTick; - const padding = tickRange * 0.1; // 10% padding on each side - const xAxisMin = minTick - padding; - const xAxisMax = maxTick + padding; + // Cap extreme values to keep chart readable + const MAX_REASONABLE_MULTIPLE = 50; // Cap at 50x for readability + const MIN_REASONABLE_MULTIPLE = 0.02; // Cap at 0.02x for readability + + const allMultiples = positionKeys.flatMap(key => [priceMultiplePositions[key].lowerMultiple, priceMultiplePositions[key].upperMultiple]); + const cappedMultiples = allMultiples.map(m => Math.min(MAX_REASONABLE_MULTIPLE, Math.max(MIN_REASONABLE_MULTIPLE, m))); + const minMultiple = Math.min(...cappedMultiples); + const maxMultiple = Math.max(...cappedMultiples); + const multipleRange = maxMultiple - minMultiple; + const padding = multipleRange * 0.1; // 10% padding on each side + const xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x + const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max // Debug logging for chart range console.log('Chart x-axis range:', { xAxisMin, xAxisMax }); @@ -630,14 +675,23 @@ // Calculate max values for proper y-axis alignment const maxEth = Math.max(...positionKeys.map(key => positions[key].eth)); const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken)); - const showPriceLine = currentTick >= xAxisMin && currentTick <= xAxisMax; + const showPriceLine = true; // Always show price line at 1x - // Create liquidity × ticks traces for each position - const liquidityTraces = positionKeys.map(key => { - const pos = positions[key]; + // Create liquidity × ticks traces for each position using shape/filled area approach + const liquidityTraces = []; + const shapes = []; + + positionKeys.forEach((key, index) => { + const pos = priceMultiplePositions[key]; const tickRange = pos.tickUpper - pos.tickLower; const totalLiquidity = pos.liquidity * tickRange; + // Skip positions with zero liquidity + if (pos.liquidity === 0 || totalLiquidity === 0) { + console.warn(`Skipping ${key} position: zero liquidity`); + return; + } + // Debug logging for very small or large values if (totalLiquidity < 1 || tickRange > 10000 || pos.tickLower > 180000) { console.log(`Warning: ${key} position has unusual values:`, { @@ -645,108 +699,104 @@ tickRange: tickRange, totalLiquidity: totalLiquidity, ticks: [pos.tickLower, pos.tickUpper], - tickCenter: pos.tickLower + (pos.tickUpper - pos.tickLower) / 2 + lowerMultiple: pos.lowerMultiple, + upperMultiple: pos.upperMultiple }); } - // Note: minVisibleLiquidity will be calculated after all positions are processed + // Create a filled area for each position to show its exact range + // Cap display coordinates to keep within visible range + // For extremely low positions, ensure they're visible at the left edge + let displayLower = pos.lowerMultiple; + let displayUpper = pos.upperMultiple; - // Note: Width adjustment will be done after x-axis range is calculated - const visibleWidth = tickRange; + // Ensure positions are visible even at extreme values + if (pos.upperMultiple < 0.01) { + // Position is entirely below 0.01x - show it at the left edge + displayLower = xAxisMin; + displayUpper = xAxisMin * 1.5; + } else if (pos.lowerMultiple > 50) { + // Position is entirely above 50x - show it at the right edge + displayLower = xAxisMax * 0.8; + displayUpper = xAxisMax; + } else { + // Normal capping for positions that span the visible range + displayLower = Math.max(xAxisMin, Math.min(xAxisMax, pos.lowerMultiple)); + displayUpper = Math.max(xAxisMin, Math.min(xAxisMax, pos.upperMultiple)); + } - return { - x: [pos.tickLower + (pos.tickUpper - pos.tickLower) / 2], - y: [totalLiquidity], // Will be adjusted later - width: visibleWidth, - type: 'bar', - name: `${pos.name} Total Liquidity`, - marker: { - color: POSITION_COLORS[key], - opacity: 0.8, - line: { - color: 'white', - width: 2 - } + // Add indicator if position extends beyond visible range + let rangeText = `Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`; + let extendsBeyond = false; + if (pos.lowerMultiple < xAxisMin || pos.upperMultiple > xAxisMax) { + rangeText += ' (extends beyond chart)'; + extendsBeyond = true; + } + + // For extremely high or low multiples, show in scientific notation + if (pos.upperMultiple > 100 || pos.lowerMultiple < 0.01) { + const lowerStr = pos.lowerMultiple < 0.01 ? pos.lowerMultiple.toExponential(2) : pos.lowerMultiple.toFixed(3) + 'x'; + const upperStr = pos.upperMultiple > 100 ? pos.upperMultiple.toExponential(2) : pos.upperMultiple.toFixed(3) + 'x'; + rangeText = `Range: ${lowerStr} - ${upperStr}`; + + // Check if this is likely a VWAP protection position + if (key === 'floor' && (pos.lowerMultiple < 0.01 || pos.upperMultiple > 100)) { + rangeText += ' (VWAP Protection - ETH Scarcity)'; + } else { + rangeText += ' (extreme range)'; + } + } + + const trace = { + x: [displayLower, displayLower, displayUpper, displayUpper], + y: [0, totalLiquidity, totalLiquidity, 0], + fill: 'toself', + fillcolor: POSITION_COLORS[key], + opacity: extendsBeyond ? 0.5 : 0.7, + type: 'scatter', + mode: 'lines', + line: { + color: POSITION_COLORS[key], + width: 2, + dash: extendsBeyond ? 'dash' : 'solid' }, - text: `${pos.name}
Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Tick Range: ${tickRange.toLocaleString()}
Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
ETH: ${pos.eth.toFixed(6)}
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Range: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]`, + name: pos.name, + text: `${pos.name}
Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Tick Range: ${tickRange.toLocaleString()}
Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
ETH: ${pos.eth.toFixed(6)}
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
${rangeText}`, hoverinfo: 'text', - showlegend: false + showlegend: true }; + + liquidityTraces.push(trace); }); const data = liquidityTraces; // Calculate max and min total liquidity (L × ticks) for y-axis scaling const totalLiquidities = positionKeys.map(key => { - const pos = positions[key]; - return pos.liquidity * (pos.tickUpper - pos.tickLower); - }); - const maxTotalLiquidity = Math.max(...totalLiquidities); - const minTotalLiquidity = Math.min(...totalLiquidities.filter(l => l > 0)); + const pos = priceMultiplePositions[key]; + const tickRange = pos.tickUpper - pos.tickLower; + return pos.liquidity * tickRange; + }).filter(l => l > 0); // Only consider non-zero liquidities - // Calculate x-axis range first - const allTicksForRange = []; - positionKeys.forEach(key => { - allTicksForRange.push(positions[key].tickLower, positions[key].tickUpper); - }); - allTicksForRange.push(currentTick); - - const minTickForWidth = Math.min(...allTicksForRange); - const maxTickForWidth = Math.max(...allTicksForRange); - const tickRangeTotal = maxTickForWidth - minTickForWidth; - const paddingForWidth = tickRangeTotal * 0.1; - const xAxisMinForWidth = minTickForWidth - paddingForWidth; - const xAxisMaxForWidth = maxTickForWidth + paddingForWidth; - const xRange = xAxisMaxForWidth - xAxisMinForWidth; - - // Calculate minimum visible width as 2% of x-axis range - const minVisibleWidth = xRange * 0.02; - - // Adjust bar widths to ensure visibility - liquidityTraces.forEach((trace, index) => { - const pos = positions[positionKeys[index]]; - const actualWidth = pos.tickUpper - pos.tickLower; - if (actualWidth < minVisibleWidth) { - trace.width = minVisibleWidth; - // Add note about width adjustment - trace.text += `
(Width expanded for visibility - actual: ${actualWidth} ticks)`; - } - }); - - // Sort liquidity values to find appropriate thresholds - const sortedLiquidities = [...totalLiquidities].sort((a, b) => a - b); - - // Ensure all bars are visible on the log scale - // Set minimum height to be 2% of the median value, which should make all bars clearly visible - const medianLiquidity = sortedLiquidities[Math.floor(sortedLiquidities.length / 2)]; - const minVisibleLiquidity = medianLiquidity * 0.02; - - // Adjust y values to ensure minimum visibility - liquidityTraces.forEach((trace, index) => { - const actualValue = totalLiquidities[index]; - if (actualValue < minVisibleLiquidity) { - trace.y[0] = minVisibleLiquidity; - // Add a note to the hover text that this value was adjusted for visibility - trace.text += `
(Height adjusted for visibility - actual: ${actualValue.toExponential(2)})`; - } - }); + const maxTotalLiquidity = totalLiquidities.length > 0 ? Math.max(...totalLiquidities) : 1; + const minTotalLiquidity = totalLiquidities.length > 0 ? Math.min(...totalLiquidities) : 0.1; // Ensure minimum is at least 1e-10 for log scale - const yMin = Math.max(1e-10, Math.min(minTotalLiquidity / 100, minVisibleLiquidity / 10)); + const yMin = Math.max(1e-10, minTotalLiquidity / 100); + const yMax = maxTotalLiquidity * 10; if (showPriceLine) { const priceLineTrace = { - x: [currentTick, currentTick], - y: [yMin, maxTotalLiquidity * 10], // Use log scale range + x: [1, 1], // Current price is always at 1x + y: [yMin, yMax], // Use calculated y range mode: 'lines', line: { color: 'red', width: 3, dash: 'dash' }, - name: 'Current Price', - hoverinfo: 'x', - text: [`Current Price: ${currentTick}`], + name: 'Current Price (1x)', + hoverinfo: 'name', showlegend: true }; data.push(priceLineTrace); @@ -754,14 +804,17 @@ const layout = { title: { - text: `Total Liquidity Distribution (L × Tick Range) - Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'}`, + text: `Total Liquidity Distribution (L × Tick Range)`, font: { size: 16 } }, xaxis: { - title: 'Price Ticks', + title: 'Price Multiple (relative to current price)', showgrid: true, gridcolor: '#e0e0e0', - range: [xAxisMin, xAxisMax] + range: [Math.log10(xAxisMin), Math.log10(xAxisMax)], + tickformat: '.2f', + ticksuffix: 'x', + type: 'log' // Use log scale for better visualization of price multiples }, yaxis: { title: 'Total Liquidity (L × Ticks)', @@ -770,7 +823,7 @@ gridcolor: '#e0e0e0', dtick: 1, // Major gridlines at powers of 10 tickformat: '.0e', // Scientific notation - range: [Math.log10(yMin), Math.log10(maxTotalLiquidity * 10)] + range: [Math.log10(yMin), Math.log10(yMax)] }, showlegend: true, legend: { @@ -783,7 +836,7 @@ plot_bgcolor: 'white', paper_bgcolor: 'white', margin: { l: 60, r: 60, t: 60, b: 50 }, - barmode: 'group' + hovermode: 'closest' }; Plotly.newPlot(chartDiv, data, layout, {responsive: true}); @@ -974,7 +1027,7 @@ }); } - function createSummaryPanel(positions, currentTick) { + function createSummaryPanel(positions, currentTick, token0isWeth) { const panel = document.createElement('div'); panel.className = 'summary-panel'; @@ -1010,13 +1063,15 @@ // Calculate position-specific liquidity percentage const liquidityPercent = totalUniV3Liquidity > 0 ? (pos.liquidity / totalUniV3Liquidity * 100).toFixed(1) : '0.0'; const tickRange = pos.tickUpper - pos.tickLower; + const lowerMultiple = tickToPriceMultiple(pos.tickLower, currentTick, token0isWeth); + const upperMultiple = tickToPriceMultiple(pos.tickUpper, currentTick, token0isWeth); item.innerHTML = ` ${pos.name} Position
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)
- Range: ${tickRange} ticks + Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x `; grid.appendChild(item); });