- Implement dynamic discovery depth based on anchor position share - Add configurable discovery_max_multiple (1.5-4x) for flexible adjustment - Update BullMarketOptimizer with new depth calculation logic - Fix scenario visualizer floor position visibility - Add comprehensive tests for discovery depth behavior The discovery position now dynamically adjusts its depth based on the anchor position's share of total liquidity, allowing for more effective price discovery while maintaining protection against manipulation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1039 lines
No EOL
45 KiB
HTML
1039 lines
No EOL
45 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Kraiken Liquidity Position Simulator</title>
|
||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
background-color: #f5f5f5;
|
||
}
|
||
textarea {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
height: 100px;
|
||
}
|
||
button {
|
||
margin-top: 10px;
|
||
padding: 8px 16px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
button:hover {
|
||
background-color: #0056b3;
|
||
}
|
||
.scenario-container {
|
||
margin-bottom: 40px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
.scenario-header {
|
||
margin: 0 0 20px 0;
|
||
padding: 10px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
border-left: 4px solid #007bff;
|
||
}
|
||
.charts-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.chart-wrapper {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.chart-title {
|
||
text-align: center;
|
||
margin-bottom: 10px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
.chart-div {
|
||
width: 100%;
|
||
height: 500px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
.summary-panel {
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
padding: 15px;
|
||
margin-top: 20px;
|
||
}
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin-top: 10px;
|
||
}
|
||
.summary-item {
|
||
background: white;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
border-left: 4px solid #007bff;
|
||
}
|
||
.summary-item.floor {
|
||
border-left-color: #1f77b4;
|
||
}
|
||
.summary-item.anchor {
|
||
border-left-color: #ff7f0e;
|
||
}
|
||
.summary-item.discovery {
|
||
border-left-color: #2ca02c;
|
||
}
|
||
.legend {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 30px;
|
||
margin-bottom: 20px;
|
||
font-size: 14px;
|
||
}
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.legend-color {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 3px;
|
||
}
|
||
.legend-color.floor {
|
||
background-color: #1f77b4;
|
||
}
|
||
.legend-color.anchor {
|
||
background-color: #ff7f0e;
|
||
}
|
||
.legend-color.discovery {
|
||
background-color: #2ca02c;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.charts-container {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h2>Kraiken Liquidity Position Simulator</h2>
|
||
<div style="background-color: #e3f2fd; border-radius: 4px; padding: 15px; margin-bottom: 20px; border-left: 4px solid #2196f3;">
|
||
<strong>📊 Anti-Arbitrage Three-Position Strategy</strong><br>
|
||
<em>Floor</em>: Deep liquidity position - contains ETH when below current price, KRAIKEN when above<br>
|
||
<em>Anchor</em>: Shallow liquidity around current price for fast slippage<br>
|
||
<em>Discovery</em>: Edge liquidity position - contains KRAIKEN when below current price, ETH when above
|
||
</div>
|
||
<div id="status">Loading profitable scenario data...</div>
|
||
<textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea>
|
||
<button onclick="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button>
|
||
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
|
||
<div id="simulations"></div>
|
||
|
||
<script>
|
||
// Position color scheme
|
||
const POSITION_COLORS = {
|
||
floor: '#1f77b4', // Dark Blue - Foundation/Stability
|
||
anchor: '#ff7f0e', // Orange - Current Price/Center
|
||
discovery: '#2ca02c' // Green - Growth/Expansion
|
||
};
|
||
|
||
// Position names for display
|
||
const POSITION_NAMES = {
|
||
floor: 'Floor',
|
||
anchor: 'Anchor',
|
||
discovery: 'Discovery'
|
||
};
|
||
|
||
// Token ordering configuration - set this based on your deployment
|
||
// If ethIsToken0 = true: ETH is token0, KRAIKEN is token1
|
||
// If ethIsToken0 = false: KRAIKEN is token0, ETH is token1
|
||
// Default matches test setup: DEFAULT_TOKEN0_IS_WETH = false
|
||
const ethIsToken0 = false;
|
||
|
||
// Position Economic Model:
|
||
// - Floor Position: Deep liquidity - holds KRAIKEN above price, ETH below price
|
||
// - Anchor Position: Mixed tokens around current price for shallow liquidity
|
||
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
|
||
|
||
// Auto-load CSV data on page load
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadCSVData();
|
||
});
|
||
|
||
function loadCSVData() {
|
||
const statusDiv = document.getElementById('status');
|
||
statusDiv.textContent = 'Loading profitable scenario data...';
|
||
|
||
// Try to load the CSV file generated by the analysis script
|
||
fetch('./profitable_scenario.csv')
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('CSV file not found');
|
||
}
|
||
return response.text();
|
||
})
|
||
.then(csvText => {
|
||
statusDiv.textContent = 'Profitable scenario data loaded successfully!';
|
||
statusDiv.style.color = 'green';
|
||
const data = parseCSV(csvText);
|
||
simulateCSVData(data);
|
||
})
|
||
.catch(error => {
|
||
statusDiv.innerHTML = `
|
||
<div style="color: orange;">
|
||
<strong>Cannot load CSV automatically due to browser security restrictions.</strong><br>
|
||
<br>
|
||
<strong>Solution 1:</strong> Run a local server:<br>
|
||
<code>cd analysis && python3 -m http.server 8000</code><br>
|
||
Then open: <a href="http://localhost:8000/scenario-visualizer.html">http://localhost:8000/scenario-visualizer.html</a><br>
|
||
<br>
|
||
<strong>Solution 2:</strong> Use manual input mode below<br>
|
||
<br>
|
||
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
|
||
</div>
|
||
`;
|
||
console.log('CSV load error:', error);
|
||
});
|
||
}
|
||
|
||
function toggleManualInput() {
|
||
const csvInput = document.getElementById('csvInput');
|
||
const button = document.getElementById('manualButton');
|
||
const parseButton = document.querySelector('button[onclick="parseAndSimulateCSV()"]');
|
||
|
||
if (csvInput.style.display === 'none') {
|
||
csvInput.style.display = 'block';
|
||
parseButton.style.display = 'inline-block';
|
||
button.textContent = 'Hide Manual Input';
|
||
} else {
|
||
csvInput.style.display = 'none';
|
||
parseButton.style.display = 'none';
|
||
button.textContent = 'Manual Input Mode';
|
||
}
|
||
}
|
||
|
||
function parseCSV(csv) {
|
||
const lines = csv.trim().split('\n');
|
||
const headers = lines[0].split(',').map(h => h.trim());
|
||
const data = lines.slice(1).map(line => {
|
||
const values = line.split(',').map(v => v.trim());
|
||
const entry = {};
|
||
headers.forEach((header, index) => {
|
||
entry[header] = values[index];
|
||
});
|
||
return entry;
|
||
});
|
||
return data;
|
||
}
|
||
|
||
function parseAndSimulateCSV() {
|
||
const csvInput = document.getElementById('csvInput').value;
|
||
const data = parseCSV(csvInput);
|
||
simulateCSVData(data);
|
||
|
||
// Clear input field after processing
|
||
document.getElementById('csvInput').value = '';
|
||
}
|
||
|
||
function simulateCSVData(data) {
|
||
let previousRow = null;
|
||
|
||
data.forEach((row, 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);
|
||
}
|
||
|
||
// 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 legend
|
||
const legend = document.createElement('div');
|
||
legend.className = 'legend';
|
||
legend.innerHTML = `
|
||
<div class="legend-item">
|
||
<div class="legend-color floor"></div>
|
||
<span>Floor Position (Foundation)</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color anchor"></div>
|
||
<span>Anchor Position (Current Price)</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color discovery"></div>
|
||
<span>Discovery Position (Growth)</span>
|
||
</div>
|
||
`;
|
||
scenarioContainer.appendChild(legend);
|
||
|
||
// Create charts container
|
||
const chartsContainer = document.createElement('div');
|
||
chartsContainer.className = 'charts-container';
|
||
|
||
// Create combined chart
|
||
const chartWrapper = document.createElement('div');
|
||
chartWrapper.className = 'chart-wrapper';
|
||
chartWrapper.style.width = '100%'; // Full width for single chart
|
||
const chartTitle = document.createElement('div');
|
||
chartTitle.className = 'chart-title';
|
||
chartTitle.textContent = '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);
|
||
scenarioContainer.appendChild(summaryPanel);
|
||
|
||
// Add to page
|
||
document.getElementById('simulations').appendChild(scenarioContainer);
|
||
|
||
// Create the combined chart
|
||
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity);
|
||
}
|
||
|
||
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity) {
|
||
const positionKeys = ['floor', 'anchor', 'discovery'];
|
||
|
||
// Calculate bar widths to represent actual tick ranges
|
||
const barWidths = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
return pos.tickUpper - pos.tickLower; // Width = actual tick range
|
||
});
|
||
|
||
// Calculate bar positions (centered in tick ranges)
|
||
const barPositions = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2;
|
||
});
|
||
|
||
// ETH trace (left y-axis)
|
||
const ethTrace = {
|
||
x: barPositions,
|
||
y: positionKeys.map(key => {
|
||
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 = positions[key];
|
||
return `${pos.name}<br>ETH: ${pos.eth.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
||
}),
|
||
hoverinfo: 'text'
|
||
};
|
||
|
||
// KRAIKEN trace (right y-axis)
|
||
const kraikenTrace = {
|
||
x: barPositions, // Same position as ETH bars
|
||
y: positionKeys.map(key => {
|
||
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 = positions[key];
|
||
return `${pos.name}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
||
}),
|
||
hoverinfo: 'text'
|
||
};
|
||
|
||
// Calculate x-axis range based on position ranges with some padding
|
||
const allTicks = positionKeys.flatMap(key => [positions[key].tickLower, positions[key].tickUpper]);
|
||
const minTick = Math.min(...allTicks);
|
||
const maxTick = Math.max(...allTicks);
|
||
const tickRange = maxTick - minTick;
|
||
const padding = tickRange * 0.1; // 10% padding on each side
|
||
const xAxisMin = minTick - padding;
|
||
const xAxisMax = maxTick + padding;
|
||
|
||
// 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 = currentTick >= xAxisMin && currentTick <= xAxisMax;
|
||
|
||
// Create liquidity × ticks traces for each position
|
||
const liquidityTraces = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
const tickRange = pos.tickUpper - pos.tickLower;
|
||
const totalLiquidity = pos.liquidity * tickRange;
|
||
|
||
// 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],
|
||
tickCenter: pos.tickLower + (pos.tickUpper - pos.tickLower) / 2
|
||
});
|
||
}
|
||
|
||
// Note: minVisibleLiquidity will be calculated after all positions are processed
|
||
|
||
// Note: Width adjustment will be done after x-axis range is calculated
|
||
const visibleWidth = tickRange;
|
||
|
||
return {
|
||
x: [pos.tickLower + (pos.tickUpper - pos.tickLower) / 2],
|
||
y: [totalLiquidity], // Will be adjusted later
|
||
width: visibleWidth,
|
||
type: 'bar',
|
||
name: `${pos.name} Total Liquidity`,
|
||
marker: {
|
||
color: POSITION_COLORS[key],
|
||
opacity: 0.8,
|
||
line: {
|
||
color: 'white',
|
||
width: 2
|
||
}
|
||
},
|
||
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>Range: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]`,
|
||
hoverinfo: 'text',
|
||
showlegend: false
|
||
};
|
||
});
|
||
|
||
const data = liquidityTraces;
|
||
|
||
// Calculate max and min total liquidity (L × ticks) for y-axis scaling
|
||
const totalLiquidities = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
return pos.liquidity * (pos.tickUpper - pos.tickLower);
|
||
});
|
||
const maxTotalLiquidity = Math.max(...totalLiquidities);
|
||
const minTotalLiquidity = Math.min(...totalLiquidities.filter(l => l > 0));
|
||
|
||
// Calculate x-axis range first
|
||
const allTicksForRange = [];
|
||
positionKeys.forEach(key => {
|
||
allTicksForRange.push(positions[key].tickLower, positions[key].tickUpper);
|
||
});
|
||
allTicksForRange.push(currentTick);
|
||
|
||
const minTickForWidth = Math.min(...allTicksForRange);
|
||
const maxTickForWidth = Math.max(...allTicksForRange);
|
||
const tickRangeTotal = maxTickForWidth - minTickForWidth;
|
||
const paddingForWidth = tickRangeTotal * 0.1;
|
||
const xAxisMinForWidth = minTickForWidth - paddingForWidth;
|
||
const xAxisMaxForWidth = maxTickForWidth + paddingForWidth;
|
||
const xRange = xAxisMaxForWidth - xAxisMinForWidth;
|
||
|
||
// Calculate minimum visible width as 2% of x-axis range
|
||
const minVisibleWidth = xRange * 0.02;
|
||
|
||
// Adjust bar widths to ensure visibility
|
||
liquidityTraces.forEach((trace, index) => {
|
||
const pos = positions[positionKeys[index]];
|
||
const actualWidth = pos.tickUpper - pos.tickLower;
|
||
if (actualWidth < minVisibleWidth) {
|
||
trace.width = minVisibleWidth;
|
||
// Add note about width adjustment
|
||
trace.text += `<br><i>(Width expanded for visibility - actual: ${actualWidth} ticks)</i>`;
|
||
}
|
||
});
|
||
|
||
// Sort liquidity values to find appropriate thresholds
|
||
const sortedLiquidities = [...totalLiquidities].sort((a, b) => a - b);
|
||
|
||
// Ensure all bars are visible on the log scale
|
||
// Set minimum height to be 2% of the median value, which should make all bars clearly visible
|
||
const medianLiquidity = sortedLiquidities[Math.floor(sortedLiquidities.length / 2)];
|
||
const minVisibleLiquidity = medianLiquidity * 0.02;
|
||
|
||
// Adjust y values to ensure minimum visibility
|
||
liquidityTraces.forEach((trace, index) => {
|
||
const actualValue = totalLiquidities[index];
|
||
if (actualValue < minVisibleLiquidity) {
|
||
trace.y[0] = minVisibleLiquidity;
|
||
// Add a note to the hover text that this value was adjusted for visibility
|
||
trace.text += `<br><i>(Height adjusted for visibility - actual: ${actualValue.toExponential(2)})</i>`;
|
||
}
|
||
});
|
||
|
||
// Ensure minimum is at least 1e-10 for log scale
|
||
const yMin = Math.max(1e-10, Math.min(minTotalLiquidity / 100, minVisibleLiquidity / 10));
|
||
|
||
if (showPriceLine) {
|
||
const priceLineTrace = {
|
||
x: [currentTick, currentTick],
|
||
y: [yMin, maxTotalLiquidity * 10], // Use log scale range
|
||
mode: 'lines',
|
||
line: {
|
||
color: 'red',
|
||
width: 3,
|
||
dash: 'dash'
|
||
},
|
||
name: 'Current Price',
|
||
hoverinfo: 'x',
|
||
text: [`Current Price: ${currentTick}`],
|
||
showlegend: true
|
||
};
|
||
data.push(priceLineTrace);
|
||
}
|
||
|
||
const layout = {
|
||
title: {
|
||
text: `Total Liquidity Distribution (L × Tick Range) - Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'}`,
|
||
font: { size: 16 }
|
||
},
|
||
xaxis: {
|
||
title: 'Price Ticks',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0',
|
||
range: [xAxisMin, xAxisMax]
|
||
},
|
||
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(maxTotalLiquidity * 10)]
|
||
},
|
||
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 },
|
||
barmode: 'group'
|
||
};
|
||
|
||
Plotly.newPlot(chartDiv, data, layout, {responsive: true});
|
||
}
|
||
|
||
function createDualCharts(ethChartDiv, kraikenChartDiv, positions, currentTick, totalLiquidity) {
|
||
const positionKeys = ['floor', 'anchor', 'discovery'];
|
||
|
||
// Calculate bar widths proportional to actual Uniswap V3 liquidity
|
||
const baseWidth = 50; // Base width for tick ranges
|
||
const barWidths = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
const liquidityRatio = totalLiquidity > 0 ? pos.liquidity / totalLiquidity : 0;
|
||
return Math.max(baseWidth * 0.3, baseWidth * liquidityRatio * 3); // Scale for visibility
|
||
});
|
||
|
||
// Calculate bar positions (centered in tick ranges)
|
||
const barPositions = positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2;
|
||
});
|
||
|
||
// ETH Chart Data
|
||
const ethData = [{
|
||
x: barPositions,
|
||
y: positionKeys.map(key => positions[key].eth),
|
||
width: barWidths,
|
||
type: 'bar',
|
||
marker: {
|
||
color: positionKeys.map(key => POSITION_COLORS[key]),
|
||
opacity: 0.8,
|
||
line: {
|
||
color: 'white',
|
||
width: 2
|
||
}
|
||
},
|
||
text: positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
let tooltip = `${pos.name} Position<br>`;
|
||
|
||
// Show token amounts and actual Uniswap V3 liquidity
|
||
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
|
||
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
|
||
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
||
return tooltip;
|
||
}),
|
||
hoverinfo: 'text',
|
||
name: 'ETH Liquidity'
|
||
}];
|
||
|
||
// Kraiken Chart Data
|
||
const kraikenData = [{
|
||
x: barPositions,
|
||
y: positionKeys.map(key => positions[key].kraiken),
|
||
width: barWidths,
|
||
type: 'bar',
|
||
marker: {
|
||
color: positionKeys.map(key => POSITION_COLORS[key]),
|
||
opacity: 0.8,
|
||
line: {
|
||
color: 'white',
|
||
width: 2
|
||
}
|
||
},
|
||
text: positionKeys.map(key => {
|
||
const pos = positions[key];
|
||
let tooltip = `${pos.name} Position<br>`;
|
||
|
||
// Show token amounts and actual Uniswap V3 liquidity
|
||
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
|
||
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
|
||
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
||
return tooltip;
|
||
}),
|
||
hoverinfo: 'text',
|
||
name: 'KRAIKEN Liquidity'
|
||
}];
|
||
|
||
// Add current price line to both charts
|
||
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
|
||
const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken));
|
||
|
||
const priceLineEth = {
|
||
x: [currentTick, currentTick],
|
||
y: [0, maxEth * 1.1],
|
||
mode: 'lines',
|
||
line: {
|
||
color: 'red',
|
||
width: 3,
|
||
dash: 'dash'
|
||
},
|
||
name: 'Current Price',
|
||
hoverinfo: 'x',
|
||
text: [`Current Price: ${currentTick}`]
|
||
};
|
||
|
||
const priceLineKraiken = {
|
||
x: [currentTick, currentTick],
|
||
y: [0, maxKraiken * 1.1],
|
||
mode: 'lines',
|
||
line: {
|
||
color: 'red',
|
||
width: 3,
|
||
dash: 'dash'
|
||
},
|
||
name: 'Current Price',
|
||
hoverinfo: 'x',
|
||
text: [`Current Price: ${currentTick}`]
|
||
};
|
||
|
||
ethData.push(priceLineEth);
|
||
kraikenData.push(priceLineKraiken);
|
||
|
||
// Create synchronized layouts
|
||
const ethLayout = {
|
||
title: {
|
||
text: 'ETH Liquidity by Position',
|
||
font: { size: 16 }
|
||
},
|
||
xaxis: {
|
||
title: 'Price Ticks',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0'
|
||
},
|
||
yaxis: {
|
||
title: 'ETH Amount',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0'
|
||
},
|
||
showlegend: false,
|
||
plot_bgcolor: 'white',
|
||
paper_bgcolor: 'white',
|
||
margin: { l: 60, r: 30, t: 60, b: 50 }
|
||
};
|
||
|
||
const kraikenLayout = {
|
||
title: {
|
||
text: 'KRAIKEN Liquidity by Position',
|
||
font: { size: 16 }
|
||
},
|
||
xaxis: {
|
||
title: 'Price Ticks',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0'
|
||
},
|
||
yaxis: {
|
||
title: 'KRAIKEN Amount',
|
||
showgrid: true,
|
||
gridcolor: '#e0e0e0'
|
||
},
|
||
showlegend: false,
|
||
plot_bgcolor: 'white',
|
||
paper_bgcolor: 'white',
|
||
margin: { l: 60, r: 30, t: 60, b: 50 }
|
||
};
|
||
|
||
// Plot both charts
|
||
Plotly.newPlot(ethChartDiv, ethData, ethLayout, {responsive: true});
|
||
Plotly.newPlot(kraikenChartDiv, kraikenData, kraikenLayout, {responsive: true});
|
||
|
||
// Add synchronized interactions
|
||
synchronizeCharts(ethChartDiv, kraikenChartDiv);
|
||
}
|
||
|
||
function synchronizeCharts(chart1, chart2) {
|
||
// Synchronize hover events
|
||
chart1.on('plotly_hover', function(data) {
|
||
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
|
||
const pointIndex = data.points[0].pointNumber;
|
||
Plotly.Fx.hover(chart2, [{curveNumber: 0, pointNumber: pointIndex}]);
|
||
}
|
||
});
|
||
|
||
chart2.on('plotly_hover', function(data) {
|
||
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
|
||
const pointIndex = data.points[0].pointNumber;
|
||
Plotly.Fx.hover(chart1, [{curveNumber: 0, pointNumber: pointIndex}]);
|
||
}
|
||
});
|
||
|
||
// Synchronize unhover events
|
||
chart1.on('plotly_unhover', function() {
|
||
Plotly.Fx.unhover(chart2);
|
||
});
|
||
|
||
chart2.on('plotly_unhover', function() {
|
||
Plotly.Fx.unhover(chart1);
|
||
});
|
||
}
|
||
|
||
function createSummaryPanel(positions, currentTick) {
|
||
const panel = document.createElement('div');
|
||
panel.className = 'summary-panel';
|
||
|
||
const title = document.createElement('h4');
|
||
title.textContent = 'Position Summary';
|
||
title.style.margin = '0 0 15px 0';
|
||
panel.appendChild(title);
|
||
|
||
const grid = document.createElement('div');
|
||
grid.className = 'summary-grid';
|
||
|
||
// Calculate totals
|
||
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0);
|
||
const totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
|
||
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
||
|
||
// Add total summary
|
||
const totalItem = document.createElement('div');
|
||
totalItem.className = 'summary-item';
|
||
totalItem.innerHTML = `
|
||
<strong>Total Portfolio</strong><br>
|
||
Token ETH: ${totalEth.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;
|
||
|
||
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: ${tickRange} ticks
|
||
`;
|
||
grid.appendChild(item);
|
||
});
|
||
|
||
// Add current price info
|
||
const priceItem = document.createElement('div');
|
||
priceItem.className = 'summary-item';
|
||
priceItem.innerHTML = `
|
||
<strong>Current Price</strong><br>
|
||
Tick: ${currentTick}<br>
|
||
<small>Price line shown in red</small>
|
||
`;
|
||
grid.appendChild(priceItem);
|
||
|
||
panel.appendChild(grid);
|
||
return panel;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |