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);
});