harb/onchain/analysis/scenario-visualizer.html

1181 lines
53 KiB
HTML
Raw Normal View History

2025-07-06 11:20:35 +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 Liquidity Position Simulator</title>
2025-07-06 11:20:35 +02:00
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
2025-07-06 11:20:35 +02:00
}
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;
2025-07-06 11:20:35 +02:00
}
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;
2025-07-06 11:20:35 +02:00
margin-bottom: 20px;
font-size: 14px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
2025-07-06 11:20:35 +02:00
}
.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;
}
2025-07-06 11:20:35 +02:00
}
</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;">
2025-08-16 18:22:32 +02:00
<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>
2025-08-17 15:09:41 +02:00
&lt; 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
2025-08-16 18:22:32 +02:00
• 1x = Current ETH price (red dashed line)<br>
2025-08-17 15:09:41 +02:00
&gt; 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
2025-08-16 18:22:32 +02:00
<br>
2025-08-17 15:09:41 +02:00
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em><br>
<br>
<strong>Navigation:</strong> Use the Previous/Next buttons or URL parameter <code>?row=N</code> to view specific CSV rows
</div>
2025-07-06 11:20:35 +02:00
<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>
<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;
2025-08-10 11:56:21 +02:00
// Position Economic Model:
// - Floor Position: Deep liquidity - holds KRAIKEN above price, ETH below price
// - Anchor Position: Mixed tokens around current price for shallow liquidity
2025-08-10 11:56:21 +02:00
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
2025-08-17 15:09:41 +02:00
// 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
}
2025-07-06 11:20:35 +02:00
// 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>
`;
2025-08-17 15:09:41 +02:00
// CSV load error - handled by status message above
2025-07-06 11:20:35 +02:00
});
}
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) {
2025-08-17 15:09:41 +02:00
// Get selected row from URL parameter
const selectedIndex = getRowParameter();
// Add current row info and navigation
const currentRowInfo = document.createElement('div');
currentRowInfo.style.cssText = 'margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 4px; text-align: center;';
currentRowInfo.innerHTML = `
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex].precedingAction}</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 not first row
if (selectedIndex > 0) {
const topNavDiv = document.createElement('div');
topNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
topNavDiv.innerHTML = `
<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px;">
← Previous Row (Line ${selectedIndex + 1})
</button>
`;
document.getElementById('simulations').appendChild(topNavDiv);
}
// Process only the selected row
let previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
if (selectedIndex >= 0 && selectedIndex < data.length) {
const row = data[selectedIndex];
const index = selectedIndex;
2025-07-06 11:20:35 +02:00
const precedingAction = row.precedingAction;
const currentTick = parseFloat(row.currentTick);
2025-08-10 11:56:21 +02:00
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
2025-07-06 11:20:35 +02:00
const floorTickLower = parseFloat(row.floorTickLower);
const floorTickUpper = parseFloat(row.floorTickUpper);
2025-08-17 15:09:41 +02:00
const floorLiquidity = parseFloat(row.floorLiquidity || 0);
2025-07-06 11:20:35 +02:00
const anchorTickLower = parseFloat(row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchorTickUpper);
2025-08-17 15:09:41 +02:00
const anchorLiquidity = parseFloat(row.anchorLiquidity || 0);
2025-07-06 11:20:35 +02:00
const discoveryTickLower = parseFloat(row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
2025-08-17 15:09:41 +02:00
const discoveryLiquidity = parseFloat(row.discoveryLiquidity || 0);
// Calculate token amounts from liquidity
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
// FIXED calculation - properly determine ETH amounts based on token ordering
let floorEthAmount, anchorEthAmount, discoveryEthAmount;
let floorKraikenAmount, anchorKraikenAmount, discoveryKraikenAmount;
// Simply use the amounts from getAmountsForLiquidity and determine which is ETH
if (token0isWeth) {
// token0 is WETH, token1 is KRAIKEN
floorEthAmount = floorAmounts.amount0 / 1e18;
floorKraikenAmount = floorAmounts.amount1 / 1e18;
anchorEthAmount = anchorAmounts.amount0 / 1e18;
anchorKraikenAmount = anchorAmounts.amount1 / 1e18;
discoveryEthAmount = discoveryAmounts.amount0 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount1 / 1e18;
} else {
// token0 is KRAIKEN, token1 is WETH
floorEthAmount = floorAmounts.amount1 / 1e18;
floorKraikenAmount = floorAmounts.amount0 / 1e18;
anchorEthAmount = anchorAmounts.amount1 / 1e18;
anchorKraikenAmount = anchorAmounts.amount0 / 1e18;
discoveryEthAmount = discoveryAmounts.amount1 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount0 / 1e18;
}
const totalEth = floorEthAmount + anchorEthAmount + discoveryEthAmount;
// Use the already calculated ETH and KRAIKEN amounts from above
let floorEth = floorEthAmount;
let floorKraiken = floorKraikenAmount;
let anchorEth = anchorEthAmount;
let anchorKraiken = anchorKraikenAmount;
let discoveryEth = discoveryEthAmount;
let discoveryKraiken = discoveryKraikenAmount;
2025-07-06 11:20:35 +02:00
let actionAmount = '';
let additionalInfo = '';
if (previousRow) {
2025-08-17 15:09:41 +02:00
// Calculate previous token amounts from liquidity
const prevCurrentTick = parseFloat(previousRow.currentTick);
const prevFloorLiquidity = parseFloat(previousRow.floorLiquidity || 0);
const prevAnchorLiquidity = parseFloat(previousRow.anchorLiquidity || 0);
const prevDiscoveryLiquidity = parseFloat(previousRow.discoveryLiquidity || 0);
const prevFloorAmounts = getAmountsForLiquidity(prevFloorLiquidity, floorTickLower, floorTickUpper, prevCurrentTick);
const prevAnchorAmounts = getAmountsForLiquidity(prevAnchorLiquidity, anchorTickLower, anchorTickUpper, prevCurrentTick);
const prevDiscoveryAmounts = getAmountsForLiquidity(prevDiscoveryLiquidity, discoveryTickLower, discoveryTickUpper, prevCurrentTick);
let prevFloorEth, prevFloorKraiken, prevAnchorEth, prevAnchorKraiken, prevDiscoveryEth, prevDiscoveryKraiken;
if (token0isWeth === true) {
prevFloorEth = prevFloorAmounts.amount0 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount1 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount0 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount1 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount0 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount1 / 1e18;
} else {
prevFloorEth = prevFloorAmounts.amount1 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount0 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount1 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount0 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount1 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount0 / 1e18;
}
2025-07-06 11:20:35 +02:00
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
2025-07-06 11:20:35 +02:00
2025-08-10 11:56:21 +02:00
if (precedingAction.toLowerCase().includes('buy')) {
2025-07-06 11:20:35 +02:00
actionAmount = `${precedingAction} ETH`;
additionalInfo = `(${Math.abs(kraikenDifference).toFixed(6)} KRAIKEN bought)`;
2025-08-10 11:56:21 +02:00
} else if (precedingAction.toLowerCase().includes('sell')) {
actionAmount = `${precedingAction} KRAIKEN`;
additionalInfo = `(${Math.abs(ethDifference).toFixed(6)} ETH bought)`;
2025-07-06 11:20:35 +02:00
} else {
actionAmount = precedingAction;
}
}
2025-08-15 18:21:49 +02:00
// Calculate CSV line number (index + 2 to account for header line)
const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
2025-07-06 11:20:35 +02:00
2025-08-17 15:09:41 +02:00
simulateEnhanced(headline, currentTick,
2025-08-17 15:09:41 +02:00
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, precedingAction);
// Add navigation buttons below
const bottomNavDiv = document.createElement('div');
bottomNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
const prevButton = selectedIndex > 0 ?
`<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px; margin-right: 20px;">
← Previous Row (Line ${selectedIndex + 1})
</button>` : '';
const nextButton = selectedIndex < data.length - 1 ?
`<button onclick="navigateRow(1)" style="padding: 10px 20px; font-size: 16px;">
Next Row (Line ${selectedIndex + 3}) →
</button>` : '';
bottomNavDiv.innerHTML = prevButton + nextButton;
document.getElementById('simulations').appendChild(bottomNavDiv);
}
}
// Function to navigate between rows
function navigateRow(direction) {
const currentIndex = getRowParameter();
const newLineNumber = currentIndex + direction + 2; // Convert back to CSV line number
const url = new URL(window.location);
url.searchParams.set('row', newLineNumber);
window.location = url;
2025-07-06 11:20:35 +02:00
}
// Uniswap V3 liquidity calculation functions
function tickToPrice(tick) {
return Math.pow(1.0001, tick);
}
2025-08-17 15:09:41 +02:00
function tickToSqrtPriceX96(tick) {
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
}
// Calculate token amounts from liquidity for a position
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
// Sort ticks to ensure tickLower < tickUpper
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) {
// Current price is below the range, position holds only token0
// When position was created above current price, ETH was used as amount1
// Use the amount1 formula to get the ETH that was deposited
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else if (currentTick >= tickUpper) {
// Current price is above the range, position holds only token1
// amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else {
// Current price is within the range
// amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioX96) / (sqrtRatioX96 * sqrtRatioBX96) * Q96
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
// amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
}
return { amount0, amount1 };
}
function priceToSqrtPrice(price) {
return Math.sqrt(price);
}
2025-08-16 18:22:32 +02:00
// Convert tick to price multiple relative to current price
// This represents ETH price multiples (how expensive ETH is relative to current)
function tickToPriceMultiple(tick, currentTick, token0isWeth) {
const price = tickToPrice(tick);
const currentPrice = tickToPrice(currentTick);
if (token0isWeth) {
// When ETH is token0, price = KRAIKEN/ETH
// We want ETH price multiple, so we need to invert
// Higher tick = more KRAIKEN per ETH = cheaper ETH
return currentPrice / price;
} else {
// When KRAIKEN is token0, price = ETH/KRAIKEN
// This is already ETH price, so just divide
return price / currentPrice;
}
}
// Calculate the invariant liquidity value from token amounts
// This represents the actual liquidity deployed to the position, independent of current price
function calculateInvariantLiquidity(token0Amount, token1Amount, tickLower, tickUpper, positionName = '') {
// Add safeguards for extreme tick values
if (tickLower > 180000 || tickUpper > 180000) {
console.warn(`${positionName} has extremely high ticks: [${tickLower}, ${tickUpper}]. This may cause precision issues.`);
}
const priceLower = tickToPrice(tickLower);
const priceUpper = tickToPrice(tickUpper);
const sqrtPriceLower = priceToSqrtPrice(priceLower);
const sqrtPriceUpper = priceToSqrtPrice(priceUpper);
// Handle edge cases where denominators would be zero
if (sqrtPriceUpper === sqrtPriceLower) {
return 0; // Invalid range
}
let liquidity = 0;
let calculatedFrom = '';
// If we have token0, calculate liquidity from it
if (token0Amount > 0) {
liquidity = token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower);
calculatedFrom = 'token0';
}
// If we have token1, calculate liquidity from it
else if (token1Amount > 0) {
liquidity = token1Amount / (sqrtPriceUpper - sqrtPriceLower);
calculatedFrom = 'token1';
}
// Debug logging
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,
2025-08-17 15:09:41 +02:00
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, originalAction) {
// Position data structure with liquidity calculations
const positions = {
floor: {
tickLower: floorTickLower,
tickUpper: floorTickUpper,
eth: floorEth,
kraiken: floorKraiken,
2025-08-10 11:56:21 +02:00
name: 'Floor',
2025-08-17 15:09:41 +02:00
liquidity: floorLiquidity
},
anchor: {
tickLower: anchorTickLower,
tickUpper: anchorTickUpper,
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
2025-08-17 15:09:41 +02:00
liquidity: anchorLiquidity
},
discovery: {
tickLower: discoveryTickLower,
tickUpper: discoveryTickUpper,
eth: discoveryEth,
kraiken: discoveryKraiken,
2025-08-10 11:56:21 +02:00
name: 'Discovery',
2025-08-17 15:09:41 +02:00
liquidity: discoveryLiquidity
}
};
// Calculate total active liquidity
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Create container for this scenario
const scenarioContainer = document.createElement('div');
scenarioContainer.className = 'scenario-container';
// Create header
const header = document.createElement('div');
header.className = 'scenario-header';
header.innerHTML = `<h3>${precedingAction}</h3>`;
scenarioContainer.appendChild(header);
// Create charts container
const chartsContainer = document.createElement('div');
chartsContainer.className = 'charts-container';
// Create combined chart
const chartWrapper = document.createElement('div');
chartWrapper.className = 'chart-wrapper';
chartWrapper.style.width = '100%'; // Full width for single chart
const chartTitle = document.createElement('div');
chartTitle.className = 'chart-title';
chartTitle.textContent = 'Total Liquidity Distribution (L × Tick Range)';
const combinedChart = document.createElement('div');
combinedChart.className = 'chart-div';
combinedChart.id = `combined-chart-${Date.now()}-${Math.random()}`;
chartWrapper.appendChild(chartTitle);
chartWrapper.appendChild(combinedChart);
chartsContainer.appendChild(chartWrapper);
scenarioContainer.appendChild(chartsContainer);
// Create summary panel
2025-08-17 15:09:41 +02:00
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index);
scenarioContainer.appendChild(summaryPanel);
// Add to page
document.getElementById('simulations').appendChild(scenarioContainer);
// Create the combined chart
2025-08-16 18:22:32 +02:00
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity, token0isWeth);
}
2025-08-16 18:22:32 +02:00
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) {
const positionKeys = ['floor', 'anchor', 'discovery'];
2025-08-16 18:22:32 +02:00
// Convert positions to price multiples
const priceMultiplePositions = {};
positionKeys.forEach(key => {
const pos = positions[key];
2025-08-16 18:22:32 +02:00
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
};
});
// Calculate bar widths to represent actual price multiple ranges
const barWidths = positionKeys.map(key => {
const pos = priceMultiplePositions[key];
return pos.width; // Width in price multiple space
});
2025-08-16 18:22:32 +02:00
// Calculate bar positions (centered in price multiple ranges)
const barPositions = positionKeys.map(key => {
2025-08-16 18:22:32 +02:00
const pos = priceMultiplePositions[key];
return pos.centerMultiple;
});
// ETH trace (left y-axis)
const ethTrace = {
x: barPositions,
y: positionKeys.map(key => {
const value = positions[key].eth;
// Add minimum height for zero values to make them visible
return value === 0 ? 0.01 : value;
}),
width: barWidths,
type: 'bar',
name: 'ETH',
yaxis: 'y',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.7,
line: {
color: 'white',
width: 2
}
},
text: positionKeys.map(key => {
2025-08-16 18:22:32 +02:00
const pos = priceMultiplePositions[key];
return `${pos.name}<br>ETH: ${pos.eth.toFixed(6)}<br>Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`;
}),
hoverinfo: 'text'
};
// KRAIKEN trace (right y-axis)
const kraikenTrace = {
x: barPositions, // Same position as ETH bars
y: positionKeys.map(key => {
const value = positions[key].kraiken;
// Add minimum height for zero values to make them visible
return value === 0 ? 0.01 : value;
}),
width: barWidths,
type: 'bar',
name: 'KRAIKEN',
yaxis: 'y2',
marker: {
color: positionKeys.map(key => POSITION_COLORS[key]),
opacity: 0.4,
pattern: {
shape: '/',
size: 8
2025-07-06 11:20:35 +02:00
},
line: {
color: 'white',
width: 2
}
2025-07-06 11:20:35 +02:00
},
text: positionKeys.map(key => {
2025-08-16 18:22:32 +02:00
const pos = priceMultiplePositions[key];
return `${pos.name}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`;
}),
hoverinfo: 'text'
};
// Calculate x-axis range based on position ranges with some padding
2025-08-16 18:22:32 +02:00
// Cap extreme values to keep chart readable
const MAX_REASONABLE_MULTIPLE = 50; // Cap at 50x for readability
const MIN_REASONABLE_MULTIPLE = 0.02; // Cap at 0.02x for readability
const allMultiples = positionKeys.flatMap(key => [priceMultiplePositions[key].lowerMultiple, priceMultiplePositions[key].upperMultiple]);
const cappedMultiples = allMultiples.map(m => Math.min(MAX_REASONABLE_MULTIPLE, Math.max(MIN_REASONABLE_MULTIPLE, m)));
const minMultiple = Math.min(...cappedMultiples);
const maxMultiple = Math.max(...cappedMultiples);
const multipleRange = maxMultiple - minMultiple;
const padding = multipleRange * 0.1; // 10% padding on each side
const xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x
const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max
// 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));
2025-08-16 18:22:32 +02:00
const showPriceLine = true; // Always show price line at 1x
2025-08-16 18:22:32 +02:00
// Create liquidity × ticks traces for each position using shape/filled area approach
const liquidityTraces = [];
const shapes = [];
positionKeys.forEach((key, index) => {
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
const totalLiquidity = pos.liquidity * tickRange;
2025-08-16 18:22:32 +02:00
// Skip positions with zero liquidity
if (pos.liquidity === 0 || totalLiquidity === 0) {
console.warn(`Skipping ${key} position: zero liquidity`);
return;
}
2025-08-16 18:22:32 +02:00
// Create a filled area for each position to show its exact range
// Cap display coordinates to keep within visible range
// For extremely low positions, ensure they're visible at the left edge
let displayLower = pos.lowerMultiple;
let displayUpper = pos.upperMultiple;
2025-08-16 18:22:32 +02:00
// Ensure positions are visible even at extreme values
if (pos.upperMultiple < 0.01) {
// Position is entirely below 0.01x - show it at the left edge
displayLower = xAxisMin;
displayUpper = xAxisMin * 1.5;
} else if (pos.lowerMultiple > 50) {
// Position is entirely above 50x - show it at the right edge
displayLower = xAxisMax * 0.8;
displayUpper = xAxisMax;
} else {
// Normal capping for positions that span the visible range
displayLower = Math.max(xAxisMin, Math.min(xAxisMax, pos.lowerMultiple));
displayUpper = Math.max(xAxisMin, Math.min(xAxisMax, pos.upperMultiple));
}
// Add indicator if position extends beyond visible range
let rangeText = `Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`;
let extendsBeyond = false;
if (pos.lowerMultiple < xAxisMin || pos.upperMultiple > xAxisMax) {
rangeText += ' <b>(extends beyond chart)</b>';
extendsBeyond = true;
}
// For extremely high or low multiples, show in scientific notation
if (pos.upperMultiple > 100 || pos.lowerMultiple < 0.01) {
const lowerStr = pos.lowerMultiple < 0.01 ? pos.lowerMultiple.toExponential(2) : pos.lowerMultiple.toFixed(3) + 'x';
const upperStr = pos.upperMultiple > 100 ? pos.upperMultiple.toExponential(2) : pos.upperMultiple.toFixed(3) + 'x';
rangeText = `Range: ${lowerStr} - ${upperStr}`;
// Check if this is likely a VWAP protection position
if (key === 'floor' && (pos.lowerMultiple < 0.01 || pos.upperMultiple > 100)) {
rangeText += ' <b>(VWAP Protection - ETH Scarcity)</b>';
} else {
rangeText += ' <b>(extreme range)</b>';
}
}
2025-08-16 18:22:32 +02:00
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'
},
2025-08-16 18:22:32 +02:00
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',
2025-08-16 18:22:32 +02:00
showlegend: true
};
2025-08-16 18:22:32 +02:00
liquidityTraces.push(trace);
});
const data = liquidityTraces;
// Calculate max and min total liquidity (L × ticks) for y-axis scaling
const totalLiquidities = positionKeys.map(key => {
2025-08-16 18:22:32 +02:00
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
return pos.liquidity * tickRange;
}).filter(l => l > 0); // Only consider non-zero liquidities
2025-08-16 18:22:32 +02:00
const maxTotalLiquidity = totalLiquidities.length > 0 ? Math.max(...totalLiquidities) : 1;
const minTotalLiquidity = totalLiquidities.length > 0 ? Math.min(...totalLiquidities) : 0.1;
// Ensure minimum is at least 1e-10 for log scale
2025-08-16 18:22:32 +02:00
const yMin = Math.max(1e-10, minTotalLiquidity / 100);
const yMax = maxTotalLiquidity * 10;
if (showPriceLine) {
const priceLineTrace = {
2025-08-16 18:22:32 +02:00
x: [1, 1], // Current price is always at 1x
y: [yMin, yMax], // Use calculated y range
2025-07-06 11:20:35 +02:00
mode: 'lines',
line: {
color: 'red',
width: 3,
2025-07-06 11:20:35 +02:00
dash: 'dash'
},
2025-08-16 18:22:32 +02:00
name: 'Current Price (1x)',
hoverinfo: 'name',
showlegend: true
};
data.push(priceLineTrace);
}
2025-07-06 11:20:35 +02:00
const layout = {
title: {
2025-08-16 18:22:32 +02:00
text: `Total Liquidity Distribution (L × Tick Range)`,
font: { size: 16 }
},
2025-07-06 11:20:35 +02:00
xaxis: {
2025-08-16 18:22:32 +02:00
title: 'Price Multiple (relative to current price)',
showgrid: true,
gridcolor: '#e0e0e0',
2025-08-16 18:22:32 +02:00
range: [Math.log10(xAxisMin), Math.log10(xAxisMax)],
tickformat: '.2f',
ticksuffix: 'x',
type: 'log' // Use log scale for better visualization of price multiples
2025-07-06 11:20:35 +02:00
},
yaxis: {
title: 'Total Liquidity (L × Ticks)',
type: 'log',
showgrid: true,
gridcolor: '#e0e0e0',
dtick: 1, // Major gridlines at powers of 10
tickformat: '.0e', // Scientific notation
2025-08-16 18:22:32 +02:00
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 },
2025-08-16 18:22:32 +02:00
hovermode: 'closest'
2025-07-06 11:20:35 +02:00
};
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
}
2025-07-06 11:20:35 +02:00
},
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,
2025-07-06 11:20:35 +02:00
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'
}];
2025-07-06 11:20:35 +02:00
// 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 }
},
2025-07-06 11:20:35 +02:00
xaxis: {
title: 'Price Ticks',
showgrid: true,
gridcolor: '#e0e0e0'
2025-07-06 11:20:35 +02:00
},
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 }
2025-07-06 11:20:35 +02:00
};
// 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);
});
}
2025-08-17 15:09:41 +02:00
function createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index) {
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);
2025-08-17 15:09:41 +02:00
// Add total summary
const totalItem = document.createElement('div');
totalItem.className = 'summary-item';
2025-08-17 15:09:41 +02:00
const totalHtml = `
<strong>Total Portfolio</strong><br>
2025-08-17 15:09:41 +02:00
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`;
2025-08-17 15:09:41 +02:00
totalItem.innerHTML = totalHtml;
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;
2025-08-16 18:22:32 +02:00
const lowerMultiple = tickToPriceMultiple(pos.tickLower, currentTick, token0isWeth);
const upperMultiple = tickToPriceMultiple(pos.tickUpper, currentTick, token0isWeth);
item.innerHTML = `
<strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
2025-08-17 15:09:41 +02:00
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';
2025-08-17 15:09:41 +02:00
// Calculate current price
const currentPrice = tickToPrice(currentTick);
let ethPriceInKraiken, kraikenPriceInEth;
if (token0isWeth) {
// price = KRAIKEN/ETH
ethPriceInKraiken = currentPrice;
kraikenPriceInEth = 1 / currentPrice;
} else {
// price = ETH/KRAIKEN
kraikenPriceInEth = currentPrice;
ethPriceInKraiken = 1 / currentPrice;
}
priceItem.innerHTML = `
<strong>Current Price</strong><br>
2025-08-17 15:09:41 +02:00
Tick: ${currentTick.toLocaleString()}<br>
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`;
grid.appendChild(priceItem);
panel.appendChild(grid);
return panel;
2025-07-06 11:20:35 +02:00
}
</script>
</body>
</html>