harb/onchain/analysis/scenario-visualizer.html
2025-08-16 18:22:32 +02:00

1094 lines
No EOL
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kraiken Liquidity Position Simulator</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}
textarea {
width: 100%;
box-sizing: border-box;
height: 100px;
}
button {
margin-top: 10px;
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.scenario-container {
margin-bottom: 40px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.scenario-header {
margin: 0 0 20px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.charts-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.chart-wrapper {
flex: 1;
min-width: 0;
}
.chart-title {
text-align: center;
margin-bottom: 10px;
font-weight: bold;
color: #333;
}
.chart-div {
width: 100%;
height: 500px;
border: 1px solid #ddd;
border-radius: 4px;
}
.summary-panel {
background-color: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 10px;
}
.summary-item {
background: white;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.summary-item.floor {
border-left-color: #1f77b4;
}
.summary-item.anchor {
border-left-color: #ff7f0e;
}
.summary-item.discovery {
border-left-color: #2ca02c;
}
.legend {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
font-size: 14px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
}
.legend-color.floor {
background-color: #1f77b4;
}
.legend-color.anchor {
background-color: #ff7f0e;
}
.legend-color.discovery {
background-color: #2ca02c;
}
@media (max-width: 768px) {
.charts-container {
flex-direction: column;
}
}
</style>
</head>
<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 (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>
<button onclick="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button>
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
<div id="simulations"></div>
<script>
// Position color scheme
const POSITION_COLORS = {
floor: '#1f77b4', // Dark Blue - Foundation/Stability
anchor: '#ff7f0e', // Orange - Current Price/Center
discovery: '#2ca02c' // Green - Growth/Expansion
};
// Position names for display
const POSITION_NAMES = {
floor: 'Floor',
anchor: 'Anchor',
discovery: 'Discovery'
};
// Token ordering configuration - set this based on your deployment
// If ethIsToken0 = true: ETH is token0, KRAIKEN is token1
// If ethIsToken0 = false: KRAIKEN is token0, ETH is token1
// Default matches test setup: DEFAULT_TOKEN0_IS_WETH = false
const ethIsToken0 = false;
// Position Economic Model:
// - Floor Position: Deep liquidity - holds KRAIKEN above price, ETH below price
// - Anchor Position: Mixed tokens around current price for shallow liquidity
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
// Auto-load CSV data on page load
document.addEventListener('DOMContentLoaded', function() {
loadCSVData();
});
function loadCSVData() {
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Loading profitable scenario data...';
// Try to load the CSV file generated by the analysis script
fetch('./profitable_scenario.csv')
.then(response => {
if (!response.ok) {
throw new Error('CSV file not found');
}
return response.text();
})
.then(csvText => {
statusDiv.textContent = 'Profitable scenario data loaded successfully!';
statusDiv.style.color = 'green';
const data = parseCSV(csvText);
simulateCSVData(data);
})
.catch(error => {
statusDiv.innerHTML = `
<div style="color: orange;">
<strong>Cannot load CSV automatically due to browser security restrictions.</strong><br>
<br>
<strong>Solution 1:</strong> Run a local server:<br>
<code>cd analysis && python3 -m http.server 8000</code><br>
Then open: <a href="http://localhost:8000/scenario-visualizer.html">http://localhost:8000/scenario-visualizer.html</a><br>
<br>
<strong>Solution 2:</strong> Use manual input mode below<br>
<br>
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
</div>
`;
console.log('CSV load error:', error);
});
}
function toggleManualInput() {
const csvInput = document.getElementById('csvInput');
const button = document.getElementById('manualButton');
const parseButton = document.querySelector('button[onclick="parseAndSimulateCSV()"]');
if (csvInput.style.display === 'none') {
csvInput.style.display = 'block';
parseButton.style.display = 'inline-block';
button.textContent = 'Hide Manual Input';
} else {
csvInput.style.display = 'none';
parseButton.style.display = 'none';
button.textContent = 'Manual Input Mode';
}
}
function parseCSV(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
const entry = {};
headers.forEach((header, index) => {
entry[header] = values[index];
});
return entry;
});
return data;
}
function parseAndSimulateCSV() {
const csvInput = document.getElementById('csvInput').value;
const data = parseCSV(csvInput);
simulateCSVData(data);
// Clear input field after processing
document.getElementById('csvInput').value = '';
}
function simulateCSVData(data) {
let previousRow = null;
data.forEach((row, index) => {
const precedingAction = row.precedingAction;
const currentTick = parseFloat(row.currentTick);
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
const floorTickLower = parseFloat(row.floorTickLower);
const floorTickUpper = parseFloat(row.floorTickUpper);
// Swap floor values to match expected behavior
const floorEth = parseFloat(row.floorToken1 || 0) / 1e18;
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
const anchorTickLower = parseFloat(row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchorTickUpper);
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18;
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
const discoveryTickLower = parseFloat(row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
// Swap discovery values to match expected behavior
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18;
let actionAmount = '';
let additionalInfo = '';
if (previousRow) {
const prevFloorEth = parseFloat(previousRow.floorToken1 || 0) / 1e18;
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || 0) / 1e18;
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || 0) / 1e18;
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || 0) / 1e18;
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || 0) / 1e18;
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || 0) / 1e18;
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
if (precedingAction.toLowerCase().includes('buy')) {
actionAmount = `${precedingAction} ETH`;
additionalInfo = `(${Math.abs(kraikenDifference).toFixed(6)} KRAIKEN bought)`;
} else if (precedingAction.toLowerCase().includes('sell')) {
actionAmount = `${precedingAction} KRAIKEN`;
additionalInfo = `(${Math.abs(ethDifference).toFixed(6)} ETH bought)`;
} else {
actionAmount = precedingAction;
}
}
// Calculate CSV line number (index + 2 to account for header line)
const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
simulateEnhanced(headline, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth);
previousRow = row;
});
}
// Uniswap V3 liquidity calculation functions
function tickToPrice(tick) {
return Math.pow(1.0001, tick);
}
function priceToSqrtPrice(price) {
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 = '') {
// Add safeguards for extreme tick values
if (tickLower > 180000 || tickUpper > 180000) {
console.warn(`${positionName} has extremely high ticks: [${tickLower}, ${tickUpper}]. This may cause precision issues.`);
}
const priceLower = tickToPrice(tickLower);
const priceUpper = tickToPrice(tickUpper);
const sqrtPriceLower = priceToSqrtPrice(priceLower);
const sqrtPriceUpper = priceToSqrtPrice(priceUpper);
// Handle edge cases where denominators would be zero
if (sqrtPriceUpper === sqrtPriceLower) {
return 0; // Invalid range
}
let liquidity = 0;
let calculatedFrom = '';
// If we have token0, calculate liquidity from it
if (token0Amount > 0) {
liquidity = token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower);
calculatedFrom = 'token0';
}
// If we have token1, calculate liquidity from it
else if (token1Amount > 0) {
liquidity = token1Amount / (sqrtPriceUpper - sqrtPriceLower);
calculatedFrom = 'token1';
}
// Debug logging
if (positionName) {
console.log(`${positionName} liquidity calculation:`, {
token0Amount,
token1Amount,
tickRange: [tickLower, tickUpper],
sqrtPriceLower,
sqrtPriceUpper,
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
liquidity,
calculatedFrom
});
}
return liquidity;
}
function calculateUniV3Liquidity(token0Amount, token1Amount, tickLower, tickUpper, currentTick) {
const priceLower = tickToPrice(tickLower);
const priceUpper = tickToPrice(tickUpper);
const priceCurrent = tickToPrice(currentTick);
const sqrtPriceLower = priceToSqrtPrice(priceLower);
const sqrtPriceUpper = priceToSqrtPrice(priceUpper);
const sqrtPriceCurrent = priceToSqrtPrice(priceCurrent);
// Handle edge cases where denominators would be zero
if (sqrtPriceUpper === sqrtPriceLower) {
return 0; // Invalid range
}
if (priceCurrent <= priceLower) {
// Price below range - position holds only token0 (ETH)
// Calculate liquidity from ETH amount using the full price range
if (token0Amount > 0) {
return token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower);
}
return 0;
} else if (priceCurrent >= priceUpper) {
// Price above range - position holds only token1 (KRAIKEN)
// Calculate liquidity from KRAIKEN amount using the full price range
if (token1Amount > 0) {
return token1Amount / (sqrtPriceUpper - sqrtPriceLower);
}
return 0;
} else {
// Price within range - calculate from both tokens and take minimum
let liquidityFromToken0 = 0;
let liquidityFromToken1 = 0;
if (token0Amount > 0 && sqrtPriceUpper > sqrtPriceCurrent) {
liquidityFromToken0 = token0Amount * (sqrtPriceUpper * sqrtPriceCurrent) / (sqrtPriceUpper - sqrtPriceCurrent);
}
if (token1Amount > 0 && sqrtPriceCurrent > sqrtPriceLower) {
liquidityFromToken1 = token1Amount / (sqrtPriceCurrent - sqrtPriceLower);
}
// Return the non-zero value, or minimum if both are present
if (liquidityFromToken0 > 0 && liquidityFromToken1 > 0) {
return Math.min(liquidityFromToken0, liquidityFromToken1);
}
return Math.max(liquidityFromToken0, liquidityFromToken1);
}
}
function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
// Position data structure with liquidity calculations
const positions = {
floor: {
tickLower: floorTickLower,
tickUpper: floorTickUpper,
eth: floorEth,
kraiken: floorKraiken,
name: 'Floor',
liquidity: token0isWeth ?
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
},
anchor: {
tickLower: anchorTickLower,
tickUpper: anchorTickUpper,
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: token0isWeth ?
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
},
discovery: {
tickLower: discoveryTickLower,
tickUpper: discoveryTickUpper,
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: token0isWeth ?
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
}
};
// Debug logging for all positions
console.log('Position liquidity values:', {
floor: {
liquidity: positions.floor.liquidity,
eth: floorEth,
kraiken: floorKraiken,
range: [floorTickLower, floorTickUpper]
},
anchor: {
liquidity: positions.anchor.liquidity,
eth: anchorEth,
kraiken: anchorKraiken,
range: [anchorTickLower, anchorTickUpper]
},
discovery: {
liquidity: positions.discovery.liquidity,
eth: discoveryEth,
kraiken: discoveryKraiken,
range: [discoveryTickLower, discoveryTickUpper]
},
currentTick: currentTick,
token0isWeth: token0isWeth
});
// Calculate total active liquidity
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Create container for this scenario
const scenarioContainer = document.createElement('div');
scenarioContainer.className = 'scenario-container';
// Create header
const header = document.createElement('div');
header.className = 'scenario-header';
header.innerHTML = `<h3>${precedingAction}</h3>`;
scenarioContainer.appendChild(header);
// Create charts container
const chartsContainer = document.createElement('div');
chartsContainer.className = 'charts-container';
// Create combined chart
const chartWrapper = document.createElement('div');
chartWrapper.className = 'chart-wrapper';
chartWrapper.style.width = '100%'; // Full width for single chart
const chartTitle = document.createElement('div');
chartTitle.className = 'chart-title';
chartTitle.textContent = 'Total Liquidity Distribution (L × Tick Range)';
const combinedChart = document.createElement('div');
combinedChart.className = 'chart-div';
combinedChart.id = `combined-chart-${Date.now()}-${Math.random()}`;
chartWrapper.appendChild(chartTitle);
chartWrapper.appendChild(combinedChart);
chartsContainer.appendChild(chartWrapper);
scenarioContainer.appendChild(chartsContainer);
// Create summary panel
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, token0isWeth);
}
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) {
const positionKeys = ['floor', 'anchor', 'discovery'];
// Convert positions to price multiples
const priceMultiplePositions = {};
positionKeys.forEach(key => {
const pos = positions[key];
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 price multiple ranges)
const barPositions = positionKeys.map(key => {
const pos = priceMultiplePositions[key];
return pos.centerMultiple;
});
// ETH trace (left y-axis)
const ethTrace = {
x: barPositions,
y: positionKeys.map(key => {
const value = positions[key].eth;
// Add minimum height for zero values to make them visible
return value === 0 ? 0.01 : value;
}),
width: barWidths,
type: 'bar',
name: 'ETH',
yaxis: 'y',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.7,
line: {
color: 'white',
width: 2
}
},
text: positionKeys.map(key => {
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'
};
// KRAIKEN trace (right y-axis)
const kraikenTrace = {
x: barPositions, // Same position as ETH bars
y: positionKeys.map(key => {
const value = positions[key].kraiken;
// Add minimum height for zero values to make them visible
return value === 0 ? 0.01 : value;
}),
width: barWidths,
type: 'bar',
name: 'KRAIKEN',
yaxis: 'y2',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.4,
pattern: {
shape: '/',
size: 8
},
line: {
color: 'white',
width: 2
}
},
text: positionKeys.map(key => {
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
// 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 });
console.log('Bar positions:', barPositions);
console.log('Bar widths:', barWidths);
console.log('ETH values:', positionKeys.map(key => positions[key].eth));
console.log('KRAIKEN values:', positionKeys.map(key => positions[key].kraiken));
console.log('ETH trace y values (with min):', ethTrace.y);
console.log('KRAIKEN trace y values (with min):', kraikenTrace.y);
// 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 = true; // Always show price line at 1x
// 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:`, {
liquidity: pos.liquidity,
tickRange: tickRange,
totalLiquidity: totalLiquidity,
ticks: [pos.tickLower, pos.tickUpper],
lowerMultiple: pos.lowerMultiple,
upperMultiple: pos.upperMultiple
});
}
// 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;
// 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'
},
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: 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 = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
return pos.liquidity * tickRange;
}).filter(l => l > 0); // Only consider non-zero liquidities
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, minTotalLiquidity / 100);
const yMax = maxTotalLiquidity * 10;
if (showPriceLine) {
const priceLineTrace = {
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 (1x)',
hoverinfo: 'name',
showlegend: true
};
data.push(priceLineTrace);
}
const layout = {
title: {
text: `Total Liquidity Distribution (L × Tick Range)`,
font: { size: 16 }
},
xaxis: {
title: 'Price Multiple (relative to current price)',
showgrid: true,
gridcolor: '#e0e0e0',
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)',
type: 'log',
showgrid: true,
gridcolor: '#e0e0e0',
dtick: 1, // Major gridlines at powers of 10
tickformat: '.0e', // Scientific notation
range: [Math.log10(yMin), Math.log10(yMax)]
},
showlegend: true,
legend: {
x: 0.02,
y: 0.98,
bgcolor: 'rgba(255,255,255,0.8)',
bordercolor: '#ccc',
borderwidth: 1
},
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 60, t: 60, b: 50 },
hovermode: 'closest'
};
Plotly.newPlot(chartDiv, data, layout, {responsive: true});
}
function createDualCharts(ethChartDiv, kraikenChartDiv, positions, currentTick, totalLiquidity) {
const positionKeys = ['floor', 'anchor', 'discovery'];
// Calculate bar widths proportional to actual Uniswap V3 liquidity
const baseWidth = 50; // Base width for tick ranges
const barWidths = positionKeys.map(key => {
const pos = positions[key];
const liquidityRatio = totalLiquidity > 0 ? pos.liquidity / totalLiquidity : 0;
return Math.max(baseWidth * 0.3, baseWidth * liquidityRatio * 3); // Scale for visibility
});
// Calculate bar positions (centered in tick ranges)
const barPositions = positionKeys.map(key => {
const pos = positions[key];
return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2;
});
// ETH Chart Data
const ethData = [{
x: barPositions,
y: positionKeys.map(key => positions[key].eth),
width: barWidths,
type: 'bar',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.8,
line: {
color: 'white',
width: 2
}
},
text: positionKeys.map(key => {
const pos = positions[key];
let tooltip = `${pos.name} Position<br>`;
// Show token amounts and actual Uniswap V3 liquidity
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
return tooltip;
}),
hoverinfo: 'text',
name: 'ETH Liquidity'
}];
// Kraiken Chart Data
const kraikenData = [{
x: barPositions,
y: positionKeys.map(key => positions[key].kraiken),
width: barWidths,
type: 'bar',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.8,
line: {
color: 'white',
width: 2
}
},
text: positionKeys.map(key => {
const pos = positions[key];
let tooltip = `${pos.name} Position<br>`;
// Show token amounts and actual Uniswap V3 liquidity
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
return tooltip;
}),
hoverinfo: 'text',
name: 'KRAIKEN Liquidity'
}];
// Add current price line to both charts
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken));
const priceLineEth = {
x: [currentTick, currentTick],
y: [0, maxEth * 1.1],
mode: 'lines',
line: {
color: 'red',
width: 3,
dash: 'dash'
},
name: 'Current Price',
hoverinfo: 'x',
text: [`Current Price: ${currentTick}`]
};
const priceLineKraiken = {
x: [currentTick, currentTick],
y: [0, maxKraiken * 1.1],
mode: 'lines',
line: {
color: 'red',
width: 3,
dash: 'dash'
},
name: 'Current Price',
hoverinfo: 'x',
text: [`Current Price: ${currentTick}`]
};
ethData.push(priceLineEth);
kraikenData.push(priceLineKraiken);
// Create synchronized layouts
const ethLayout = {
title: {
text: 'ETH Liquidity by Position',
font: { size: 16 }
},
xaxis: {
title: 'Price Ticks',
showgrid: true,
gridcolor: '#e0e0e0'
},
yaxis: {
title: 'ETH Amount',
showgrid: true,
gridcolor: '#e0e0e0'
},
showlegend: false,
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 30, t: 60, b: 50 }
};
const kraikenLayout = {
title: {
text: 'KRAIKEN Liquidity by Position',
font: { size: 16 }
},
xaxis: {
title: 'Price Ticks',
showgrid: true,
gridcolor: '#e0e0e0'
},
yaxis: {
title: 'KRAIKEN Amount',
showgrid: true,
gridcolor: '#e0e0e0'
},
showlegend: false,
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 30, t: 60, b: 50 }
};
// Plot both charts
Plotly.newPlot(ethChartDiv, ethData, ethLayout, {responsive: true});
Plotly.newPlot(kraikenChartDiv, kraikenData, kraikenLayout, {responsive: true});
// Add synchronized interactions
synchronizeCharts(ethChartDiv, kraikenChartDiv);
}
function synchronizeCharts(chart1, chart2) {
// Synchronize hover events
chart1.on('plotly_hover', function(data) {
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
const pointIndex = data.points[0].pointNumber;
Plotly.Fx.hover(chart2, [{curveNumber: 0, pointNumber: pointIndex}]);
}
});
chart2.on('plotly_hover', function(data) {
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
const pointIndex = data.points[0].pointNumber;
Plotly.Fx.hover(chart1, [{curveNumber: 0, pointNumber: pointIndex}]);
}
});
// Synchronize unhover events
chart1.on('plotly_unhover', function() {
Plotly.Fx.unhover(chart2);
});
chart2.on('plotly_unhover', function() {
Plotly.Fx.unhover(chart1);
});
}
function createSummaryPanel(positions, currentTick, token0isWeth) {
const panel = document.createElement('div');
panel.className = 'summary-panel';
const title = document.createElement('h4');
title.textContent = 'Position Summary';
title.style.margin = '0 0 15px 0';
panel.appendChild(title);
const grid = document.createElement('div');
grid.className = 'summary-grid';
// Calculate totals
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0);
const totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Add total summary
const totalItem = document.createElement('div');
totalItem.className = 'summary-item';
totalItem.innerHTML = `
<strong>Total Portfolio</strong><br>
Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
`;
grid.appendChild(totalItem);
// Add position summaries
Object.entries(positions).forEach(([key, pos]) => {
const item = document.createElement('div');
item.className = `summary-item ${key}`;
// 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: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
`;
grid.appendChild(item);
});
// Add current price info
const priceItem = document.createElement('div');
priceItem.className = 'summary-item';
priceItem.innerHTML = `
<strong>Current Price</strong><br>
Tick: ${currentTick}<br>
<small>Price line shown in red</small>
`;
grid.appendChild(priceItem);
panel.appendChild(grid);
return panel;
}
</script>
</body>
</html>