harb/onchain/analysis/run-visualizer.html

923 lines
38 KiB
HTML
Raw Permalink Normal View History

2025-08-23 22:32:41 +02:00
<!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;
2025-08-24 18:38:48 +02:00
// 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
2025-08-23 22:32:41 +02:00
if (token0isWeth) {
2025-08-24 18:38:48 +02:00
// Floor position (high ticks, above current) holds token0 (ETH) when above price
2025-08-23 22:32:41 +02:00
floorEth = floorAmounts.amount0 / 1e18;
floorKraiken = floorAmounts.amount1 / 1e18;
anchorEth = anchorAmounts.amount0 / 1e18;
anchorKraiken = anchorAmounts.amount1 / 1e18;
2025-08-24 18:38:48 +02:00
// Discovery position (low ticks, below current) holds token1 (KRAIKEN) when below price
2025-08-23 22:32:41 +02:00
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>