harb/onchain/analysis/scenario-visualizer.html
johba 2205ae719b feat: Optimize discovery position depth calculation
- Implement dynamic discovery depth based on anchor position share
- Add configurable discovery_max_multiple (1.5-4x) for flexible adjustment
- Update BullMarketOptimizer with new depth calculation logic
- Fix scenario visualizer floor position visibility
- Add comprehensive tests for discovery depth behavior

The discovery position now dynamically adjusts its depth based on the anchor
position's share of total liquidity, allowing for more effective price discovery
while maintaining protection against manipulation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-16 16:45:24 +02:00

1039 lines
No EOL
45 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</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
</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);
}
// 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 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');
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);
scenarioContainer.appendChild(summaryPanel);
// Add to page
document.getElementById('simulations').appendChild(scenarioContainer);
// Create the combined chart
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity);
}
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity) {
const positionKeys = ['floor', 'anchor', 'discovery'];
// Calculate bar widths to represent actual tick ranges
const barWidths = positionKeys.map(key => {
const pos = positions[key];
return pos.tickUpper - pos.tickLower; // Width = actual tick range
});
// 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 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 = positions[key];
return `${pos.name}<br>ETH: ${pos.eth.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
}),
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 = positions[key];
return `${pos.name}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
}),
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;
// 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 = currentTick >= xAxisMin && currentTick <= xAxisMax;
// Create liquidity × ticks traces for each position
const liquidityTraces = positionKeys.map(key => {
const pos = positions[key];
const tickRange = pos.tickUpper - pos.tickLower;
const totalLiquidity = pos.liquidity * tickRange;
// 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],
tickCenter: pos.tickLower + (pos.tickUpper - pos.tickLower) / 2
});
}
// Note: minVisibleLiquidity will be calculated after all positions are processed
// 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
}
},
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()}]`,
hoverinfo: 'text',
showlegend: false
};
});
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));
// 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>`;
}
});
// Ensure minimum is at least 1e-10 for log scale
const yMin = Math.max(1e-10, Math.min(minTotalLiquidity / 100, minVisibleLiquidity / 10));
if (showPriceLine) {
const priceLineTrace = {
x: [currentTick, currentTick],
y: [yMin, maxTotalLiquidity * 10], // Use log scale range
mode: 'lines',
line: {
color: 'red',
width: 3,
dash: 'dash'
},
name: 'Current Price',
hoverinfo: 'x',
text: [`Current Price: ${currentTick}`],
showlegend: true
};
data.push(priceLineTrace);
}
const layout = {
title: {
text: `Total Liquidity Distribution (L × Tick Range) - Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'}`,
font: { size: 16 }
},
xaxis: {
title: 'Price Ticks',
showgrid: true,
gridcolor: '#e0e0e0',
range: [xAxisMin, xAxisMax]
},
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(maxTotalLiquidity * 10)]
},
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 },
barmode: 'group'
};
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) {
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;
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
`;
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>