price multipliers instead of ticks

This commit is contained in:
johba 2025-08-16 18:22:32 +02:00
parent 2205ae719b
commit 6a012c5fd9
2 changed files with 186 additions and 129 deletions

2
.gitignore vendored
View file

@ -23,3 +23,5 @@ docs/
*~
*.swp
*.swo
.playwright-mcp/

View file

@ -125,10 +125,19 @@
<body>
<h2>Kraiken Liquidity Position Simulator</h2>
<div style="background-color: #e3f2fd; border-radius: 4px; padding: 15px; margin-bottom: 20px; border-left: 4px solid #2196f3;">
<strong>📊 Anti-Arbitrage Three-Position Strategy</strong><br>
<em>Floor</em>: Deep liquidity position - contains ETH when below current price, KRAIKEN when above<br>
<em>Anchor</em>: Shallow liquidity around current price for fast slippage<br>
<em>Discovery</em>: Edge liquidity position - contains KRAIKEN when below current price, ETH when above
<strong>📊 Anti-Arbitrage Three-Position Strategy (Uniswap V3 1% Pool)</strong><br>
<br>
<strong>Position Strategy:</strong><br>
<em>Floor</em>: Deep liquidity position - holds ETH when ETH is cheap (below current price)<br>
<em>Anchor</em>: Shallow liquidity around current price for fast price discovery<br>
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
<br>
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
• 0.5x = ETH is half as expensive (Floor position holds ETH)<br>
• 1x = Current ETH price (red dashed line)<br>
• 2x = ETH is twice as expensive (Discovery position holds KRAIKEN)<br>
<br>
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em>
</div>
<div id="status">Loading profitable scenario data...</div>
<textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea>
@ -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 = `<h3>${precedingAction}</h3>`;
scenarioContainer.appendChild(header);
// Create legend
const legend = document.createElement('div');
legend.className = 'legend';
legend.innerHTML = `
<div class="legend-item">
<div class="legend-color floor"></div>
<span>Floor Position (Foundation)</span>
</div>
<div class="legend-item">
<div class="legend-color anchor"></div>
<span>Anchor Position (Current Price)</span>
</div>
<div class="legend-item">
<div class="legend-color discovery"></div>
<span>Discovery Position (Growth)</span>
</div>
`;
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
});
// Calculate bar positions (centered in tick ranges)
// 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 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}<br>ETH: ${pos.eth.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
const pos = priceMultiplePositions[key];
return `${pos.name}<br>ETH: ${pos.eth.toFixed(6)}<br>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}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
const pos = priceMultiplePositions[key];
return `${pos.name}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>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;
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
// 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));
}
// 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 += ' <b>(extends beyond chart)</b>';
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 += ' <b>(VWAP Protection - ETH Scarcity)</b>';
} else {
rangeText += ' <b>(extreme range)</b>';
}
}
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}<br>Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>Tick Range: ${tickRange.toLocaleString()}<br>Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}<br>ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>Range: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]`,
name: pos.name,
text: `${pos.name}<br>Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>Tick Range: ${tickRange.toLocaleString()}<br>Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}<br>ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>${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 += `<br><i>(Width expanded for visibility - actual: ${actualWidth} ticks)</i>`;
}
});
// 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 += `<br><i>(Height adjusted for visibility - actual: ${actualValue.toExponential(2)})</i>`;
}
});
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 = `
<strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
Range: ${tickRange} ticks
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
`;
grid.appendChild(item);
});