1094 lines
No EOL
48 KiB
HTML
1094 lines
No EOL
48 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 (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>
|
||
</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, index) => {
|
||
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 || 0) / 1e18;
|
||
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
|
||
const anchorTickLower = parseFloat(row.anchorTickLower);
|
||
const anchorTickUpper = parseFloat(row.anchorTickUpper);
|
||
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18;
|
||
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
|
||
const discoveryTickLower = parseFloat(row.discoveryTickLower);
|
||
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
|
||
// Swap discovery values to match expected behavior
|
||
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
|
||
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18;
|
||
|
||
let actionAmount = '';
|
||
let additionalInfo = '';
|
||
|
||
if (previousRow) {
|
||
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;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// Calculate CSV line number (index + 2 to account for header line)
|
||
const lineNumber = index + 2;
|
||
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
|
||
|
||
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);
|
||
}
|
||
|
||
// 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
|
||
if (positionName) {
|
||
console.log(`${positionName} liquidity calculation:`, {
|
||
token0Amount,
|
||
token1Amount,
|
||
tickRange: [tickLower, tickUpper],
|
||
sqrtPriceLower,
|
||
sqrtPriceUpper,
|
||
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
|
||
liquidity,
|
||
calculatedFrom
|
||
});
|
||
}
|
||
|
||
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,
|
||
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 ?
|
||
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
|
||
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
|
||
},
|
||
anchor: {
|
||
tickLower: anchorTickLower,
|
||
tickUpper: anchorTickUpper,
|
||
eth: anchorEth,
|
||
kraiken: anchorKraiken,
|
||
name: 'Anchor (Shallow Pool)',
|
||
liquidity: token0isWeth ?
|
||
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
|
||
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
|
||
},
|
||
discovery: {
|
||
tickLower: discoveryTickLower,
|
||
tickUpper: discoveryTickUpper,
|
||
eth: discoveryEth,
|
||
kraiken: discoveryKraiken,
|
||
name: 'Discovery',
|
||
liquidity: token0isWeth ?
|
||
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
|
||
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
|
||
}
|
||
};
|
||
|
||
// 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
|
||
});
|
||
|
||
// 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
|
||
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth);
|
||
scenarioContainer.appendChild(summaryPanel);
|
||
|
||
// Add to page
|
||
document.getElementById('simulations').appendChild(scenarioContainer);
|
||
|
||
// Create the combined chart
|
||
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity, token0isWeth);
|
||
}
|
||
|
||
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) {
|
||
const positionKeys = ['floor', 'anchor', 'discovery'];
|
||
|
||
// Convert positions to price multiples
|
||
const priceMultiplePositions = {};
|
||
|
||
positionKeys.forEach(key => {
|
||
const pos = positions[key];
|
||
const lowerMultiple = tickToPriceMultiple(pos.tickLower, currentTick, token0isWeth);
|
||
const upperMultiple = tickToPriceMultiple(pos.tickUpper, currentTick, token0isWeth);
|
||
const centerMultiple = tickToPriceMultiple(pos.tickLower + (pos.tickUpper - pos.tickLower) / 2, currentTick, token0isWeth);
|
||
|
||
priceMultiplePositions[key] = {
|
||
lowerMultiple: lowerMultiple,
|
||
upperMultiple: upperMultiple,
|
||
centerMultiple: centerMultiple,
|
||
width: upperMultiple - lowerMultiple,
|
||
...pos
|
||
};
|
||
|
||
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
|
||
});
|
||
|
||
// Calculate bar positions (centered in price multiple ranges)
|
||
const barPositions = positionKeys.map(key => {
|
||
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 => {
|
||
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
|
||
},
|
||
line: {
|
||
color: 'white',
|
||
width: 2
|
||
}
|
||
},
|
||
text: positionKeys.map(key => {
|
||
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
|
||
// 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
|
||
|
||
// 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);
|
||
|
||
// 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 = true; // Always show price line at 1x
|
||
|
||
// 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;
|
||
|
||
// Skip positions with zero liquidity
|
||
if (pos.liquidity === 0 || totalLiquidity === 0) {
|
||
console.warn(`Skipping ${key} position: zero liquidity`);
|
||
return;
|
||
}
|
||
|
||
// 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],
|
||
lowerMultiple: pos.lowerMultiple,
|
||
upperMultiple: pos.upperMultiple
|
||
});
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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>';
|
||
}
|
||
}
|
||
|
||
const trace = {
|
||
x: [displayLower, displayLower, displayUpper, displayUpper],
|
||
y: [0, totalLiquidity, totalLiquidity, 0],
|
||
fill: 'toself',
|
||
fillcolor: POSITION_COLORS[key],
|
||
opacity: extendsBeyond ? 0.5 : 0.7,
|
||
type: 'scatter',
|
||
mode: 'lines',
|
||
line: {
|
||
color: POSITION_COLORS[key],
|
||
width: 2,
|
||
dash: extendsBeyond ? 'dash' : 'solid'
|
||
},
|
||
name: pos.name,
|
||
text: `${pos.name}<br>Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>Tick Range: ${tickRange.toLocaleString()}<br>Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}<br>ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>${rangeText}`,
|
||
hoverinfo: 'text',
|
||
showlegend: true
|
||
};
|
||
|
||
liquidityTraces.push(trace);
|
||
});
|
||
|
||
const data = liquidityTraces;
|
||
|
||
// Calculate max and min total liquidity (L × ticks) for y-axis scaling
|
||
const totalLiquidities = positionKeys.map(key => {
|
||
const pos = priceMultiplePositions[key];
|
||
const tickRange = pos.tickUpper - pos.tickLower;
|
||
return pos.liquidity * tickRange;
|
||
}).filter(l => l > 0); // Only consider non-zero liquidities
|
||
|
||
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
|
||
const yMin = Math.max(1e-10, minTotalLiquidity / 100);
|
||
const yMax = maxTotalLiquidity * 10;
|
||
|
||
if (showPriceLine) {
|
||
const priceLineTrace = {
|
||
x: [1, 1], // Current price is always at 1x
|
||
y: [yMin, yMax], // Use calculated y range
|
||
mode: 'lines',
|
||
line: {
|
||
color: 'red',
|
||
width: 3,
|
||
dash: 'dash'
|
||
},
|
||
name: 'Current Price (1x)',
|
||
hoverinfo: 'name',
|
||
showlegend: true
|
||
};
|
||
data.push(priceLineTrace);
|
||
}
|
||
|
||
const layout = {
|
||
title: {
|
||
text: `Total Liquidity Distribution (L × Tick Range)`,
|
||
font: { size: 16 }
|
||
},
|
||
xaxis: {
|
||
title: 'Price Multiple (relative to current price)',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0',
|
||
range: [Math.log10(xAxisMin), Math.log10(xAxisMax)],
|
||
tickformat: '.2f',
|
||
ticksuffix: 'x',
|
||
type: 'log' // Use log scale for better visualization of price multiples
|
||
},
|
||
yaxis: {
|
||
title: 'Total Liquidity (L × Ticks)',
|
||
type: 'log',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0',
|
||
dtick: 1, // Major gridlines at powers of 10
|
||
tickformat: '.0e', // Scientific notation
|
||
range: [Math.log10(yMin), Math.log10(yMax)]
|
||
},
|
||
showlegend: true,
|
||
legend: {
|
||
x: 0.02,
|
||
y: 0.98,
|
||
bgcolor: 'rgba(255,255,255,0.8)',
|
||
bordercolor: '#ccc',
|
||
borderwidth: 1
|
||
},
|
||
plot_bgcolor: 'white',
|
||
paper_bgcolor: 'white',
|
||
margin: { l: 60, r: 60, t: 60, b: 50 },
|
||
hovermode: 'closest'
|
||
};
|
||
|
||
Plotly.newPlot(chartDiv, data, layout, {responsive: true});
|
||
}
|
||
|
||
function 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, token0isWeth) {
|
||
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.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})}
|
||
`;
|
||
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;
|
||
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>
|
||
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
|
||
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
|
||
`;
|
||
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> |