harb/onchain/analysis/run-visualizer.html
2025-08-24 18:38:48 +02:00

923 lines
No EOL
38 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 Fuzzing Run Visualizer</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
}
.controls {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.file-selector {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 200px;
cursor: pointer;
}
select:hover {
border-color: #667eea;
}
button {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
button:hover {
opacity: 0.9;
}
.info-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.info-item {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #667eea;
}
.info-label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.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;
}
.navigation {
margin: 20px 0;
text-align: center;
}
.navigation button {
margin: 0 10px;
padding: 10px 20px;
font-size: 16px;
}
.row-info {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
text-align: center;
}
.instructions {
background-color: #e3f2fd;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
border-left: 4px solid #2196f3;
}
.error {
background-color: #ffebee;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
border-left: 4px solid #f44336;
color: #c62828;
}
@media (max-width: 768px) {
.charts-container {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="header">
<h1>🐙 Kraiken Fuzzing Run Visualizer</h1>
<p>Analyze individual trades from fuzzing runs</p>
</div>
<div class="instructions">
<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>
&lt; 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
• 1x = Current ETH price (red dashed line)<br>
&gt; 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
<br>
<strong>Usage:</strong> Select a CSV file from the dropdown and use Previous/Next buttons to navigate through trades
</div>
<div class="controls">
<div class="file-selector">
<label for="csvSelect">Select CSV File:</label>
<select id="csvSelect">
<option value="">Loading files...</option>
</select>
<button onclick="refreshFileList()">Refresh List</button>
<button onclick="loadSelectedFile()">Load File</button>
</div>
<div class="info-panel" id="fileInfo" style="display: none;">
<div class="info-item">
<div class="info-label">File</div>
<div class="info-value" id="fileName">-</div>
</div>
<div class="info-item">
<div class="info-label">Total Rows</div>
<div class="info-value" id="totalRows">-</div>
</div>
<div class="info-item">
<div class="info-label">Scenario ID</div>
<div class="info-value" id="scenarioId">-</div>
</div>
<div class="info-item">
<div class="info-label">P&L</div>
<div class="info-value" id="pnl">-</div>
</div>
</div>
</div>
<div id="errorMessage" class="error" style="display: none;"></div>
<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'
};
// Global data storage
let currentData = null;
let currentFile = null;
// Token ordering configuration
const ethIsToken0 = false; // Default matches test setup
// Get row parameter from URL
function getRowParameter() {
const urlParams = new URLSearchParams(window.location.search);
const row = urlParams.get('row');
return row ? parseInt(row) - 2 : 0; // Convert CSV line number to array index
}
// Set row parameter in URL
function setRowParameter(lineNumber) {
const url = new URL(window.location);
url.searchParams.set('row', lineNumber);
window.history.pushState({}, '', url);
}
// Auto-load on page load
document.addEventListener('DOMContentLoaded', function() {
refreshFileList();
});
async function refreshFileList() {
const select = document.getElementById('csvSelect');
select.innerHTML = '<option value="">Loading files...</option>';
try {
// Fetch list of CSV files from the server
const response = await fetch('./');
const text = await response.text();
// Parse the directory listing to find CSV files
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const links = doc.querySelectorAll('a');
const csvFiles = [];
links.forEach(link => {
const href = link.getAttribute('href');
if (href && href.endsWith('.csv')) {
// Filter for fuzz-XXXX-NNN.csv pattern
if (href.match(/fuzz-[A-Z0-9]{4}-\d{3}\.csv/)) {
csvFiles.push(href);
}
}
});
// Sort files by name
csvFiles.sort();
// Populate select
select.innerHTML = '<option value="">-- Select a file --</option>';
csvFiles.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file;
select.appendChild(option);
});
if (csvFiles.length === 0) {
select.innerHTML = '<option value="">No CSV files found</option>';
showError('No fuzzing CSV files found. Run ./run-fuzzing.sh to generate data.');
}
} catch (error) {
console.error('Error fetching file list:', error);
select.innerHTML = '<option value="">Error loading files</option>';
showError('Failed to load file list. Make sure the server is running from the analysis folder.');
}
}
async function loadSelectedFile() {
const select = document.getElementById('csvSelect');
const fileName = select.value;
if (!fileName) {
showError('Please select a CSV file');
return;
}
try {
const response = await fetch(`./${fileName}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileName}`);
}
const csvText = await response.text();
currentFile = fileName;
loadCSVData(csvText, fileName);
hideError();
} catch (error) {
console.error('Error loading file:', error);
showError(`Failed to load ${fileName}. Make sure the file exists and the server has access.`);
}
}
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
function hideError() {
document.getElementById('errorMessage').style.display = 'none';
}
function loadCSVData(csvText, fileName) {
const data = parseCSV(csvText);
currentData = data;
// Update file info
const fileInfo = document.getElementById('fileInfo');
fileInfo.style.display = 'block';
document.getElementById('fileName').textContent = fileName;
document.getElementById('totalRows').textContent = data.length;
// Extract scenario ID from filename
const scenarioMatch = fileName.match(/fuzz-([A-Z0-9]{4})-/);
if (scenarioMatch) {
document.getElementById('scenarioId').textContent = scenarioMatch[1];
}
// Calculate P&L
const initRow = data.find(row => (row.action === 'INIT' || row.precedingAction === 'INIT'));
const finalRow = data.find(row => (row.action === 'FINAL' || row.precedingAction === 'FINAL'));
if (initRow && finalRow) {
const initEth = parseFloat(initRow.eth_balance || initRow.ethBalance) || 0;
const finalEth = parseFloat(finalRow.eth_balance || finalRow.ethBalance) || 0;
if (initEth > 0) {
const pnl = ((finalEth - initEth) / initEth * 100).toFixed(2);
const pnlElement = document.getElementById('pnl');
pnlElement.textContent = `${pnl > 0 ? '+' : ''}${pnl}%`;
pnlElement.style.color = pnl > 0 ? '#4CAF50' : '#f44336';
}
}
simulateCSVData(data);
}
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 simulateCSVData(data) {
// Clear previous simulations
document.getElementById('simulations').innerHTML = '';
// Get selected row from URL parameter
const selectedIndex = getRowParameter();
// Add current row info and navigation
const currentRowInfo = document.createElement('div');
currentRowInfo.className = 'row-info';
currentRowInfo.innerHTML = `
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex]?.action || data[selectedIndex]?.precedingAction || 'Unknown'}</strong><br>
<span style="color: #666; font-size: 14px;">Total rows: ${data.length} (Lines 2-${data.length + 1})</span>
`;
document.getElementById('simulations').appendChild(currentRowInfo);
// Add navigation buttons above
if (selectedIndex > 0 || selectedIndex < data.length - 1) {
const topNavDiv = document.createElement('div');
topNavDiv.className = 'navigation';
topNavDiv.innerHTML = `
${selectedIndex > 0 ? `
<button onclick="navigateRow(-1)">
← Previous (Line ${selectedIndex + 1})
</button>
` : ''}
${selectedIndex < data.length - 1 ? `
<button onclick="navigateRow(1)">
Next (Line ${selectedIndex + 3}) →
</button>
` : ''}
`;
document.getElementById('simulations').appendChild(topNavDiv);
}
// Process selected row
if (selectedIndex >= 0 && selectedIndex < data.length) {
const row = data[selectedIndex];
const previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
processRow(row, selectedIndex, previousRow);
// Add navigation buttons below
const bottomNavDiv = document.createElement('div');
bottomNavDiv.className = 'navigation';
bottomNavDiv.innerHTML = `
${selectedIndex > 0 ? `
<button onclick="navigateRow(-1)">
← Previous (Line ${selectedIndex + 1})
</button>
` : ''}
${selectedIndex < data.length - 1 ? `
<button onclick="navigateRow(1)">
Next (Line ${selectedIndex + 3}) →
</button>
` : ''}
`;
document.getElementById('simulations').appendChild(bottomNavDiv);
}
}
function navigateRow(direction) {
const currentIndex = getRowParameter();
const newIndex = currentIndex + direction;
if (currentData && newIndex >= 0 && newIndex < currentData.length) {
setRowParameter(newIndex + 2); // Convert to CSV line number
simulateCSVData(currentData);
}
}
function processRow(row, index, previousRow) {
// Map column names from actual CSV format
const precedingAction = row.action || row.precedingAction;
const currentTick = parseFloat(row.tick || row.currentTick);
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true || false;
const floorTickLower = parseFloat(row.floor_lower || row.floorTickLower);
const floorTickUpper = parseFloat(row.floor_upper || row.floorTickUpper);
const floorLiquidity = parseFloat(row.floor_liq || row.floorLiquidity || 0);
const anchorTickLower = parseFloat(row.anchor_lower || row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchor_upper || row.anchorTickUpper);
const anchorLiquidity = parseFloat(row.anchor_liq || row.anchorLiquidity || 0);
const discoveryTickLower = parseFloat(row.discovery_lower || row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discovery_upper || row.discoveryTickUpper);
const discoveryLiquidity = parseFloat(row.discovery_liq || row.discoveryLiquidity || 0);
// Extract staking metrics if available
const percentageStaked = row.percentageStaked ? parseFloat(row.percentageStaked) : 0;
const avgTaxRate = row.avgTaxRate ? parseFloat(row.avgTaxRate) : 0;
// Calculate token amounts
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
let floorEth, anchorEth, discoveryEth;
let floorKraiken, anchorKraiken, discoveryKraiken;
// Note: In KRAIKEN protocol, positions are named opposite to their price location:
// - "Floor" position is actually at high ticks (above current price)
// - "Discovery" position is at low ticks (below current price)
// This is counterintuitive but matches the contract implementation
if (token0isWeth) {
// Floor position (high ticks, above current) holds token0 (ETH) when above price
floorEth = floorAmounts.amount0 / 1e18;
floorKraiken = floorAmounts.amount1 / 1e18;
anchorEth = anchorAmounts.amount0 / 1e18;
anchorKraiken = anchorAmounts.amount1 / 1e18;
// Discovery position (low ticks, below current) holds token1 (KRAIKEN) when below price
discoveryEth = discoveryAmounts.amount0 / 1e18;
discoveryKraiken = discoveryAmounts.amount1 / 1e18;
} else {
floorEth = floorAmounts.amount1 / 1e18;
floorKraiken = floorAmounts.amount0 / 1e18;
anchorEth = anchorAmounts.amount1 / 1e18;
anchorKraiken = anchorAmounts.amount0 / 1e18;
discoveryEth = discoveryAmounts.amount1 / 1e18;
discoveryKraiken = discoveryAmounts.amount0 / 1e18;
}
// Create headline
const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction}`;
simulateEnhanced(headline, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
token0isWeth, index, precedingAction, percentageStaked, avgTaxRate);
}
// Uniswap V3 calculation functions
function tickToPrice(tick) {
return Math.pow(1.0001, tick);
}
function tickToSqrtPriceX96(tick) {
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
}
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
if (tickLower > tickUpper) {
[tickLower, tickUpper] = [tickUpper, tickLower];
}
const sqrtRatioAX96 = tickToSqrtPriceX96(tickLower);
const sqrtRatioBX96 = tickToSqrtPriceX96(tickUpper);
const sqrtRatioX96 = tickToSqrtPriceX96(currentTick);
let amount0 = 0;
let amount1 = 0;
const Q96 = 2 ** 96;
if (currentTick < tickLower) {
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else if (currentTick >= tickUpper) {
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else {
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
}
return { amount0, amount1 };
}
function tickToPriceMultiple(tick, currentTick, token0isWeth) {
const price = tickToPrice(tick);
const currentPrice = tickToPrice(currentTick);
if (token0isWeth) {
return currentPrice / price;
} else {
return price / currentPrice;
}
}
function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
token0isWeth, index, originalAction, percentageStaked = 0, avgTaxRate = 0) {
const positions = {
floor: {
tickLower: floorTickLower,
tickUpper: floorTickUpper,
eth: floorEth,
kraiken: floorKraiken,
name: 'Floor',
liquidity: floorLiquidity
},
anchor: {
tickLower: anchorTickLower,
tickUpper: anchorTickUpper,
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: anchorLiquidity
},
discovery: {
tickLower: discoveryTickLower,
tickUpper: discoveryTickUpper,
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: discoveryLiquidity
}
};
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Create container
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%';
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, originalAction || precedingAction, index, percentageStaked, avgTaxRate);
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 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
};
});
// Create liquidity traces
const liquidityTraces = [];
const MAX_REASONABLE_MULTIPLE = 50;
const MIN_REASONABLE_MULTIPLE = 0.02;
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;
const xAxisMin = Math.max(0.01, minMultiple - padding);
const xAxisMax = Math.min(100, maxMultiple + padding);
positionKeys.forEach(key => {
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
const totalLiquidity = pos.liquidity * tickRange;
if (pos.liquidity === 0 || totalLiquidity === 0) {
return;
}
let displayLower = pos.lowerMultiple;
let displayUpper = pos.upperMultiple;
if (pos.upperMultiple < 0.01) {
displayLower = xAxisMin;
displayUpper = xAxisMin * 1.5;
} else if (pos.lowerMultiple > 50) {
displayLower = xAxisMax * 0.8;
displayUpper = xAxisMax;
} else {
displayLower = Math.max(xAxisMin, Math.min(xAxisMax, pos.lowerMultiple));
displayUpper = Math.max(xAxisMin, Math.min(xAxisMax, pos.upperMultiple));
}
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;
}
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}`;
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 y-axis range
const totalLiquidities = positionKeys.map(key => {
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
return pos.liquidity * tickRange;
}).filter(l => l > 0);
const maxTotalLiquidity = totalLiquidities.length > 0 ? Math.max(...totalLiquidities) : 1;
const minTotalLiquidity = totalLiquidities.length > 0 ? Math.min(...totalLiquidities) : 0.1;
const yMin = Math.max(1e-10, minTotalLiquidity / 100);
const yMax = maxTotalLiquidity * 10;
// Add price line
const priceLineTrace = {
x: [1, 1],
y: [yMin, yMax],
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'
},
yaxis: {
title: 'Total Liquidity (L × Ticks)',
type: 'log',
showgrid: true,
gridcolor: '#e0e0e0',
dtick: 1,
tickformat: '.0e',
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 createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index, percentageStaked = 0, avgTaxRate = 0) {
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>
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`;
grid.appendChild(totalItem);
// Add position summaries
Object.entries(positions).forEach(([key, pos]) => {
const item = document.createElement('div');
item.className = `summary-item ${key}`;
const liquidityPercent = totalUniV3Liquidity > 0 ? (pos.liquidity / totalUniV3Liquidity * 100).toFixed(1) : '0.0';
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>
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
`;
grid.appendChild(item);
});
// Add current price info
const priceItem = document.createElement('div');
priceItem.className = 'summary-item';
const currentPrice = tickToPrice(currentTick);
let ethPriceInKraiken, kraikenPriceInEth;
if (token0isWeth) {
ethPriceInKraiken = currentPrice;
kraikenPriceInEth = 1 / currentPrice;
} else {
kraikenPriceInEth = currentPrice;
ethPriceInKraiken = 1 / currentPrice;
}
priceItem.innerHTML = `
<strong>Current Price</strong><br>
Tick: ${currentTick.toLocaleString()}<br>
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`;
grid.appendChild(priceItem);
// Add staking metrics if available
if (percentageStaked > 0 || avgTaxRate > 0) {
const stakingItem = document.createElement('div');
stakingItem.className = 'summary-item';
const stakingPercent = (percentageStaked / 1e16).toFixed(2);
const taxRatePercent = (avgTaxRate / 1e16).toFixed(2);
stakingItem.innerHTML = `
<strong>Staking Metrics</strong><br>
Staked: ${stakingPercent}%<br>
Avg Tax Rate: ${taxRatePercent}%
`;
grid.appendChild(stakingItem);
}
panel.appendChild(grid);
return panel;
}
</script>
</body>
</html>