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">
|
2025-07-15 11:46:25 +02:00
|
|
|
|
<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;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
background-color: #f5f5f5;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
height: 100px;
|
|
|
|
|
|
}
|
|
|
|
|
|
button {
|
|
|
|
|
|
margin-top: 10px;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
}
|
2025-07-15 11:46:25 +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;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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
|
|
|
|
}
|
2025-07-15 11:46:25 +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>
|
2025-07-15 11:46:25 +02:00
|
|
|
|
<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>
|
|
|
|
|
|
• 0.5x = ETH is half as expensive (Floor position holds ETH)<br>
|
|
|
|
|
|
• 1x = Current ETH price (red dashed line)<br>
|
|
|
|
|
|
• 2x = ETH is twice as expensive (Discovery position holds KRAIKEN)<br>
|
|
|
|
|
|
<br>
|
|
|
|
|
|
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em>
|
2025-07-15 11:46:25 +02:00
|
|
|
|
</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>
|
|
|
|
|
|
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
|
|
|
|
|
|
<div id="simulations"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-07-15 11:46:25 +02:00
|
|
|
|
// 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
|
2025-07-15 11:46:25 +02:00
|
|
|
|
// - 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-07-15 11:46:25 +02:00
|
|
|
|
|
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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2025-08-15 18:21:49 +02:00
|
|
|
|
data.forEach((row, index) => {
|
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-10 11:56:21 +02:00
|
|
|
|
// Swap floor values to match expected behavior
|
2025-08-16 16:45:24 +02:00
|
|
|
|
const floorEth = parseFloat(row.floorToken1 || 0) / 1e18;
|
|
|
|
|
|
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
const anchorTickLower = parseFloat(row.anchorTickLower);
|
|
|
|
|
|
const anchorTickUpper = parseFloat(row.anchorTickUpper);
|
2025-08-16 16:45:24 +02:00
|
|
|
|
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18;
|
|
|
|
|
|
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
const discoveryTickLower = parseFloat(row.discoveryTickLower);
|
|
|
|
|
|
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
|
2025-08-10 11:56:21 +02:00
|
|
|
|
// Swap discovery values to match expected behavior
|
2025-08-16 16:45:24 +02:00
|
|
|
|
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
|
|
|
|
|
|
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
|
|
|
|
|
|
let actionAmount = '';
|
|
|
|
|
|
let additionalInfo = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (previousRow) {
|
2025-08-16 16:45:24 +02:00
|
|
|
|
const prevFloorEth = parseFloat(previousRow.floorToken1 || 0) / 1e18;
|
|
|
|
|
|
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || 0) / 1e18;
|
|
|
|
|
|
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || 0) / 1e18;
|
|
|
|
|
|
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || 0) / 1e18;
|
|
|
|
|
|
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || 0) / 1e18;
|
|
|
|
|
|
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || 0) / 1e18;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
|
|
|
|
|
|
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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`;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
additionalInfo = `(${Math.abs(kraikenDifference).toFixed(6)} KRAIKEN bought)`;
|
2025-08-10 11:56:21 +02:00
|
|
|
|
} else if (precedingAction.toLowerCase().includes('sell')) {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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-07-15 11:46:25 +02:00
|
|
|
|
simulateEnhanced(headline, currentTick,
|
|
|
|
|
|
floorTickLower, floorTickUpper, floorEth, floorKraiken,
|
|
|
|
|
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
|
2025-08-10 11:56:21 +02:00
|
|
|
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth);
|
2025-07-06 11:20:35 +02:00
|
|
|
|
previousRow = row;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 11:46:25 +02:00
|
|
|
|
// Uniswap V3 liquidity calculation functions
|
|
|
|
|
|
function tickToPrice(tick) {
|
|
|
|
|
|
return Math.pow(1.0001, tick);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-16 16:45:24 +02:00
|
|
|
|
// Calculate the invariant liquidity value from token amounts
|
|
|
|
|
|
// This represents the actual liquidity deployed to the position, independent of current price
|
|
|
|
|
|
function calculateInvariantLiquidity(token0Amount, token1Amount, tickLower, tickUpper, positionName = '') {
|
|
|
|
|
|
// Add safeguards for extreme tick values
|
|
|
|
|
|
if (tickLower > 180000 || tickUpper > 180000) {
|
|
|
|
|
|
console.warn(`${positionName} has extremely high ticks: [${tickLower}, ${tickUpper}]. This may cause precision issues.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const priceLower = tickToPrice(tickLower);
|
|
|
|
|
|
const priceUpper = tickToPrice(tickUpper);
|
|
|
|
|
|
|
|
|
|
|
|
const sqrtPriceLower = priceToSqrtPrice(priceLower);
|
|
|
|
|
|
const sqrtPriceUpper = priceToSqrtPrice(priceUpper);
|
|
|
|
|
|
|
|
|
|
|
|
// Handle edge cases where denominators would be zero
|
|
|
|
|
|
if (sqrtPriceUpper === sqrtPriceLower) {
|
|
|
|
|
|
return 0; // Invalid range
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let liquidity = 0;
|
|
|
|
|
|
let calculatedFrom = '';
|
|
|
|
|
|
|
|
|
|
|
|
// If we have token0, calculate liquidity from it
|
|
|
|
|
|
if (token0Amount > 0) {
|
|
|
|
|
|
liquidity = token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower);
|
|
|
|
|
|
calculatedFrom = 'token0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we have token1, calculate liquidity from it
|
|
|
|
|
|
else if (token1Amount > 0) {
|
|
|
|
|
|
liquidity = token1Amount / (sqrtPriceUpper - sqrtPriceLower);
|
|
|
|
|
|
calculatedFrom = 'token1';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Debug logging
|
|
|
|
|
|
if (positionName) {
|
|
|
|
|
|
console.log(`${positionName} liquidity calculation:`, {
|
|
|
|
|
|
token0Amount,
|
|
|
|
|
|
token1Amount,
|
|
|
|
|
|
tickRange: [tickLower, tickUpper],
|
|
|
|
|
|
sqrtPriceLower,
|
|
|
|
|
|
sqrtPriceUpper,
|
|
|
|
|
|
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
|
|
|
|
|
|
liquidity,
|
|
|
|
|
|
calculatedFrom
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return liquidity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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,
|
2025-08-10 11:56:21 +02:00
|
|
|
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
|
liquidity: token0isWeth ?
|
2025-08-16 16:45:24 +02:00
|
|
|
|
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
|
|
|
|
|
|
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
|
2025-07-15 11:46:25 +02:00
|
|
|
|
},
|
|
|
|
|
|
anchor: {
|
|
|
|
|
|
tickLower: anchorTickLower,
|
|
|
|
|
|
tickUpper: anchorTickUpper,
|
|
|
|
|
|
eth: anchorEth,
|
|
|
|
|
|
kraiken: anchorKraiken,
|
|
|
|
|
|
name: 'Anchor (Shallow Pool)',
|
2025-08-10 11:56:21 +02:00
|
|
|
|
liquidity: token0isWeth ?
|
2025-08-16 16:45:24 +02:00
|
|
|
|
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
|
|
|
|
|
|
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
|
2025-07-15 11:46:25 +02:00
|
|
|
|
},
|
|
|
|
|
|
discovery: {
|
|
|
|
|
|
tickLower: discoveryTickLower,
|
|
|
|
|
|
tickUpper: discoveryTickUpper,
|
|
|
|
|
|
eth: discoveryEth,
|
|
|
|
|
|
kraiken: discoveryKraiken,
|
2025-08-10 11:56:21 +02:00
|
|
|
|
name: 'Discovery',
|
|
|
|
|
|
liquidity: token0isWeth ?
|
2025-08-16 16:45:24 +02:00
|
|
|
|
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
|
|
|
|
|
|
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
|
2025-07-15 11:46:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-16 16:45:24 +02:00
|
|
|
|
|
|
|
|
|
|
// Debug logging for all positions
|
|
|
|
|
|
console.log('Position liquidity values:', {
|
|
|
|
|
|
floor: {
|
|
|
|
|
|
liquidity: positions.floor.liquidity,
|
|
|
|
|
|
eth: floorEth,
|
|
|
|
|
|
kraiken: floorKraiken,
|
|
|
|
|
|
range: [floorTickLower, floorTickUpper]
|
|
|
|
|
|
},
|
|
|
|
|
|
anchor: {
|
|
|
|
|
|
liquidity: positions.anchor.liquidity,
|
|
|
|
|
|
eth: anchorEth,
|
|
|
|
|
|
kraiken: anchorKraiken,
|
|
|
|
|
|
range: [anchorTickLower, anchorTickUpper]
|
|
|
|
|
|
},
|
|
|
|
|
|
discovery: {
|
|
|
|
|
|
liquidity: positions.discovery.liquidity,
|
|
|
|
|
|
eth: discoveryEth,
|
|
|
|
|
|
kraiken: discoveryKraiken,
|
|
|
|
|
|
range: [discoveryTickLower, discoveryTickUpper]
|
|
|
|
|
|
},
|
|
|
|
|
|
currentTick: currentTick,
|
|
|
|
|
|
token0isWeth: token0isWeth
|
|
|
|
|
|
});
|
2025-07-15 11:46:25 +02:00
|
|
|
|
|
|
|
|
|
|
// 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';
|
2025-08-16 16:45:24 +02:00
|
|
|
|
chartTitle.textContent = 'Total Liquidity Distribution (L × Tick Range)';
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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-16 18:22:32 +02:00
|
|
|
|
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth);
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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-07-15 11:46:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-16 18:22:32 +02:00
|
|
|
|
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
const positionKeys = ['floor', 'anchor', 'discovery'];
|
|
|
|
|
|
|
2025-08-16 18:22:32 +02:00
|
|
|
|
// Convert positions to price multiples
|
|
|
|
|
|
const priceMultiplePositions = {};
|
|
|
|
|
|
|
|
|
|
|
|
positionKeys.forEach(key => {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`Position ${key}:`, {
|
|
|
|
|
|
ticks: [pos.tickLower, pos.tickUpper],
|
|
|
|
|
|
currentTick: currentTick,
|
|
|
|
|
|
multiples: [lowerMultiple, upperMultiple],
|
|
|
|
|
|
centerMultiple: centerMultiple,
|
|
|
|
|
|
token0isWeth: token0isWeth
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Warn about extreme positions
|
|
|
|
|
|
if (pos.tickLower > 180000 || pos.tickUpper > 180000) {
|
|
|
|
|
|
console.warn(`EXTREME TICKS: ${key} position has ticks above 180000, which represents extreme price multiples`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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-07-15 11:46:25 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-16 18:22:32 +02:00
|
|
|
|
// Calculate bar positions (centered in price multiple ranges)
|
2025-07-15 11:46:25 +02:00
|
|
|
|
const barPositions = positionKeys.map(key => {
|
2025-08-16 18:22:32 +02:00
|
|
|
|
const pos = priceMultiplePositions[key];
|
|
|
|
|
|
return pos.centerMultiple;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ETH trace (left y-axis)
|
|
|
|
|
|
const ethTrace = {
|
|
|
|
|
|
x: barPositions,
|
2025-08-16 16:45:24 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}),
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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`;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
}),
|
|
|
|
|
|
hoverinfo: 'text'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// KRAIKEN trace (right y-axis)
|
|
|
|
|
|
const kraikenTrace = {
|
|
|
|
|
|
x: barPositions, // Same position as ETH bars
|
2025-08-16 16:45:24 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}),
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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
|
|
|
|
},
|
2025-07-15 11:46:25 +02:00
|
|
|
|
line: {
|
|
|
|
|
|
color: 'white',
|
|
|
|
|
|
width: 2
|
|
|
|
|
|
}
|
2025-07-06 11:20:35 +02:00
|
|
|
|
},
|
2025-07-15 11:46:25 +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`;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
}),
|
|
|
|
|
|
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
|
2025-08-16 16:45:24 +02:00
|
|
|
|
|
|
|
|
|
|
// Debug logging for chart range
|
|
|
|
|
|
console.log('Chart x-axis range:', { xAxisMin, xAxisMax });
|
|
|
|
|
|
console.log('Bar positions:', barPositions);
|
|
|
|
|
|
console.log('Bar widths:', barWidths);
|
|
|
|
|
|
console.log('ETH values:', positionKeys.map(key => positions[key].eth));
|
|
|
|
|
|
console.log('KRAIKEN values:', positionKeys.map(key => positions[key].kraiken));
|
|
|
|
|
|
console.log('ETH trace y values (with min):', ethTrace.y);
|
|
|
|
|
|
console.log('KRAIKEN trace y values (with min):', kraikenTrace.y);
|
2025-07-15 11:46:25 +02:00
|
|
|
|
|
|
|
|
|
|
// 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-07-15 11:46:25 +02:00
|
|
|
|
|
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];
|
2025-08-16 16:45:24 +02:00
|
|
|
|
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 16:45:24 +02:00
|
|
|
|
// Debug logging for very small or large values
|
|
|
|
|
|
if (totalLiquidity < 1 || tickRange > 10000 || pos.tickLower > 180000) {
|
|
|
|
|
|
console.log(`Warning: ${key} position has unusual values:`, {
|
|
|
|
|
|
liquidity: pos.liquidity,
|
|
|
|
|
|
tickRange: tickRange,
|
|
|
|
|
|
totalLiquidity: totalLiquidity,
|
|
|
|
|
|
ticks: [pos.tickLower, pos.tickUpper],
|
2025-08-16 18:22:32 +02:00
|
|
|
|
lowerMultiple: pos.lowerMultiple,
|
|
|
|
|
|
upperMultiple: pos.upperMultiple
|
2025-08-16 16:45:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 16:45:24 +02:00
|
|
|
|
|
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 16:45:24 +02:00
|
|
|
|
|
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 16:45:24 +02:00
|
|
|
|
},
|
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}`,
|
2025-08-16 16:45:24 +02:00
|
|
|
|
hoverinfo: 'text',
|
2025-08-16 18:22:32 +02:00
|
|
|
|
showlegend: true
|
2025-08-16 16:45:24 +02:00
|
|
|
|
};
|
2025-08-16 18:22:32 +02:00
|
|
|
|
|
|
|
|
|
|
liquidityTraces.push(trace);
|
2025-08-16 16:45:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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 16:45:24 +02:00
|
|
|
|
|
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;
|
2025-08-16 16:45:24 +02:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
2025-07-15 11:46:25 +02:00
|
|
|
|
|
|
|
|
|
|
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: {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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',
|
2025-07-15 11:46:25 +02:00
|
|
|
|
showlegend: true
|
|
|
|
|
|
};
|
|
|
|
|
|
data.push(priceLineTrace);
|
|
|
|
|
|
}
|
2025-07-06 11:20:35 +02:00
|
|
|
|
|
2025-07-15 11:46:25 +02:00
|
|
|
|
const layout = {
|
|
|
|
|
|
title: {
|
2025-08-16 18:22:32 +02:00
|
|
|
|
text: `Total Liquidity Distribution (L × Tick Range)`,
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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)',
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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: {
|
2025-08-16 16:45:24 +02:00
|
|
|
|
title: 'Total Liquidity (L × Ticks)',
|
|
|
|
|
|
type: 'log',
|
2025-07-15 11:46:25 +02:00
|
|
|
|
showgrid: true,
|
|
|
|
|
|
gridcolor: '#e0e0e0',
|
2025-08-16 16:45:24 +02:00
|
|
|
|
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)]
|
2025-07-15 11:46:25 +02:00
|
|
|
|
},
|
|
|
|
|
|
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',
|
2025-08-16 16:45:24 +02:00
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-15 11:46:25 +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
|
|
|
|
},
|
2025-07-15 11:46:25 +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: {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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
|
|
|
|
|
2025-07-15 11:46:25 +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: {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
title: 'Price Ticks',
|
|
|
|
|
|
showgrid: true,
|
|
|
|
|
|
gridcolor: '#e0e0e0'
|
2025-07-06 11:20:35 +02:00
|
|
|
|
},
|
|
|
|
|
|
yaxis: {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-15 11:46:25 +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-16 18:22:32 +02:00
|
|
|
|
function createSummaryPanel(positions, currentTick, token0isWeth) {
|
2025-07-15 11:46:25 +02:00
|
|
|
|
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>
|
2025-08-16 16:45:24 +02:00
|
|
|
|
Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
|
|
|
|
|
Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
|
|
|
|
|
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
|
2025-07-15 11:46:25 +02:00
|
|
|
|
`;
|
|
|
|
|
|
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);
|
2025-07-15 11:46:25 +02:00
|
|
|
|
|
|
|
|
|
|
item.innerHTML = `
|
|
|
|
|
|
<strong>${pos.name} Position</strong><br>
|
2025-08-16 16:45:24 +02:00
|
|
|
|
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
|
|
|
|
|
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
|
|
|
|
|
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
|
2025-08-16 18:22:32 +02:00
|
|
|
|
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
|
2025-07-15 11:46:25 +02:00
|
|
|
|
`;
|
|
|
|
|
|
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;
|
2025-07-06 11:20:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
2025-07-15 11:46:25 +02:00
|
|
|
|
</html>
|