harb/onchain/analysis/scenario-visualizer.html
2025-08-10 11:56:21 +02:00

856 lines
No EOL
35 KiB
HTML

<!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 => {
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 || row.floorHarb) / 1e18;
const floorKraiken = parseFloat(row.floorToken0 || row.floorEth) / 1e18;
const anchorTickLower = parseFloat(row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchorTickUpper);
const anchorEth = parseFloat(row.anchorToken0 || row.anchorEth) / 1e18;
const anchorKraiken = parseFloat(row.anchorToken1 || row.anchorHarb) / 1e18;
const discoveryTickLower = parseFloat(row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
// Swap discovery values to match expected behavior
const discoveryEth = parseFloat(row.discoveryToken1 || row.discoveryHarb) / 1e18;
const discoveryKraiken = parseFloat(row.discoveryToken0 || row.discoveryEth) / 1e18;
let actionAmount = '';
let additionalInfo = '';
if (previousRow) {
const prevFloorEth = parseFloat(previousRow.floorToken1 || previousRow.floorHarb) / 1e18;
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || previousRow.floorEth) / 1e18;
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || previousRow.anchorEth) / 1e18;
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || previousRow.anchorHarb) / 1e18;
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || previousRow.discoveryHarb) / 1e18;
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || previousRow.discoveryEth) / 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;
}
}
const ethFormatted = (floorEth + anchorEth + discoveryEth).toFixed(6);
const kraikenFormatted = (floorKraiken + anchorKraiken + discoveryKraiken).toFixed(6);
const headline = `${precedingAction} ${additionalInfo} | Total ETH: ${ethFormatted}, Total KRAIKEN: ${kraikenFormatted}`;
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);
}
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 ?
calculateUniV3Liquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, currentTick) :
calculateUniV3Liquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, currentTick)
},
anchor: {
tickLower: anchorTickLower,
tickUpper: anchorTickUpper,
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: token0isWeth ?
calculateUniV3Liquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, currentTick) :
calculateUniV3Liquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, currentTick)
},
discovery: {
tickLower: discoveryTickLower,
tickUpper: discoveryTickUpper,
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: token0isWeth ?
calculateUniV3Liquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, currentTick) :
calculateUniV3Liquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, currentTick)
}
};
// 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 = 'Token Distribution by Position';
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 => positions[key].eth),
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 => positions[key].kraiken),
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;
// Calculate max values for proper y-axis alignment
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken));
const showPriceLine = currentTick >= xAxisMin && currentTick <= xAxisMax;
const data = [ethTrace, kraikenTrace];
if (showPriceLine) {
const priceLineTrace = {
x: [currentTick, currentTick],
y: [0, maxEth * 1.1],
mode: 'lines',
line: {
color: 'red',
width: 3,
dash: 'dash'
},
name: 'Current Price',
yaxis: 'y',
hoverinfo: 'x',
text: [`Current Price: ${currentTick}`],
showlegend: true
};
data.push(priceLineTrace);
}
const layout = {
title: {
text: `Token Distribution by Position (Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'})`,
font: { size: 16 }
},
xaxis: {
title: 'Price Ticks',
showgrid: true,
gridcolor: '#e0e0e0',
range: [xAxisMin, xAxisMax]
},
yaxis: {
title: 'ETH Amount',
side: 'left',
showgrid: true,
gridcolor: '#e0e0e0',
titlefont: { color: '#1f77b4' },
tickfont: { color: '#1f77b4' },
range: [0, maxEth * 1.1] // Start from 0, add 10% padding
},
yaxis2: {
title: 'KRAIKEN Amount',
side: 'right',
overlaying: 'y',
showgrid: false,
titlefont: { color: '#ff7f0e' },
tickfont: { color: '#ff7f0e' },
range: [0, maxKraiken * 1.1] // Start from 0, add 10% padding
},
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 }
};
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.toFixed(6)}<br>
Token KRAIKEN: ${totalKraiken.toFixed(6)}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toFixed(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.toFixed(6)}<br>
KRAIKEN: ${pos.kraiken.toFixed(6)}<br>
Uniswap V3 Liquidity: ${pos.liquidity.toFixed(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>