856 lines
No EOL
35 KiB
HTML
856 lines
No EOL
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Kraiken Liquidity Position Simulator</title>
|
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f5f5f5;
|
|
}
|
|
textarea {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
height: 100px;
|
|
}
|
|
button {
|
|
margin-top: 10px;
|
|
padding: 8px 16px;
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
.scenario-container {
|
|
margin-bottom: 40px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.scenario-header {
|
|
margin: 0 0 20px 0;
|
|
padding: 10px;
|
|
background-color: #f8f9fa;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
.charts-container {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.chart-wrapper {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.chart-title {
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
.chart-div {
|
|
width: 100%;
|
|
height: 500px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
.summary-panel {
|
|
background-color: #f8f9fa;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 10px;
|
|
}
|
|
.summary-item {
|
|
background: white;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
.summary-item.floor {
|
|
border-left-color: #1f77b4;
|
|
}
|
|
.summary-item.anchor {
|
|
border-left-color: #ff7f0e;
|
|
}
|
|
.summary-item.discovery {
|
|
border-left-color: #2ca02c;
|
|
}
|
|
.legend {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
margin-bottom: 20px;
|
|
font-size: 14px;
|
|
}
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.legend-color {
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 3px;
|
|
}
|
|
.legend-color.floor {
|
|
background-color: #1f77b4;
|
|
}
|
|
.legend-color.anchor {
|
|
background-color: #ff7f0e;
|
|
}
|
|
.legend-color.discovery {
|
|
background-color: #2ca02c;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.charts-container {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>Kraiken Liquidity Position Simulator</h2>
|
|
<div style="background-color: #e3f2fd; border-radius: 4px; padding: 15px; margin-bottom: 20px; border-left: 4px solid #2196f3;">
|
|
<strong>📊 Anti-Arbitrage Three-Position Strategy</strong><br>
|
|
<em>Floor</em>: Deep liquidity position - contains ETH when below current price, KRAIKEN when above<br>
|
|
<em>Anchor</em>: Shallow liquidity around current price for fast slippage<br>
|
|
<em>Discovery</em>: Edge liquidity position - contains KRAIKEN when below current price, ETH when above
|
|
</div>
|
|
<div id="status">Loading profitable scenario data...</div>
|
|
<textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea>
|
|
<button onclick="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button>
|
|
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
|
|
<div id="simulations"></div>
|
|
|
|
<script>
|
|
// Position color scheme
|
|
const POSITION_COLORS = {
|
|
floor: '#1f77b4', // Dark Blue - Foundation/Stability
|
|
anchor: '#ff7f0e', // Orange - Current Price/Center
|
|
discovery: '#2ca02c' // Green - Growth/Expansion
|
|
};
|
|
|
|
// Position names for display
|
|
const POSITION_NAMES = {
|
|
floor: 'Floor',
|
|
anchor: 'Anchor',
|
|
discovery: 'Discovery'
|
|
};
|
|
|
|
// Token ordering configuration - set this based on your deployment
|
|
// If ethIsToken0 = true: ETH is token0, KRAIKEN is token1
|
|
// If ethIsToken0 = false: KRAIKEN is token0, ETH is token1
|
|
// Default matches test setup: DEFAULT_TOKEN0_IS_WETH = false
|
|
const ethIsToken0 = false;
|
|
|
|
// Position Economic Model:
|
|
// - Floor Position: Deep liquidity - holds KRAIKEN above price, ETH below price
|
|
// - Anchor Position: Mixed tokens around current price for shallow liquidity
|
|
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
|
|
|
|
// Auto-load CSV data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadCSVData();
|
|
});
|
|
|
|
function loadCSVData() {
|
|
const statusDiv = document.getElementById('status');
|
|
statusDiv.textContent = 'Loading profitable scenario data...';
|
|
|
|
// Try to load the CSV file generated by the analysis script
|
|
fetch('./profitable_scenario.csv')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('CSV file not found');
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(csvText => {
|
|
statusDiv.textContent = 'Profitable scenario data loaded successfully!';
|
|
statusDiv.style.color = 'green';
|
|
const data = parseCSV(csvText);
|
|
simulateCSVData(data);
|
|
})
|
|
.catch(error => {
|
|
statusDiv.innerHTML = `
|
|
<div style="color: orange;">
|
|
<strong>Cannot load CSV automatically due to browser security restrictions.</strong><br>
|
|
<br>
|
|
<strong>Solution 1:</strong> Run a local server:<br>
|
|
<code>cd analysis && python3 -m http.server 8000</code><br>
|
|
Then open: <a href="http://localhost:8000/scenario-visualizer.html">http://localhost:8000/scenario-visualizer.html</a><br>
|
|
<br>
|
|
<strong>Solution 2:</strong> Use manual input mode below<br>
|
|
<br>
|
|
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
|
|
</div>
|
|
`;
|
|
console.log('CSV load error:', error);
|
|
});
|
|
}
|
|
|
|
function toggleManualInput() {
|
|
const csvInput = document.getElementById('csvInput');
|
|
const button = document.getElementById('manualButton');
|
|
const parseButton = document.querySelector('button[onclick="parseAndSimulateCSV()"]');
|
|
|
|
if (csvInput.style.display === 'none') {
|
|
csvInput.style.display = 'block';
|
|
parseButton.style.display = 'inline-block';
|
|
button.textContent = 'Hide Manual Input';
|
|
} else {
|
|
csvInput.style.display = 'none';
|
|
parseButton.style.display = 'none';
|
|
button.textContent = 'Manual Input Mode';
|
|
}
|
|
}
|
|
|
|
function parseCSV(csv) {
|
|
const lines = csv.trim().split('\n');
|
|
const headers = lines[0].split(',').map(h => h.trim());
|
|
const data = lines.slice(1).map(line => {
|
|
const values = line.split(',').map(v => v.trim());
|
|
const entry = {};
|
|
headers.forEach((header, index) => {
|
|
entry[header] = values[index];
|
|
});
|
|
return entry;
|
|
});
|
|
return data;
|
|
}
|
|
|
|
function parseAndSimulateCSV() {
|
|
const csvInput = document.getElementById('csvInput').value;
|
|
const data = parseCSV(csvInput);
|
|
simulateCSVData(data);
|
|
|
|
// Clear input field after processing
|
|
document.getElementById('csvInput').value = '';
|
|
}
|
|
|
|
function simulateCSVData(data) {
|
|
let previousRow = null;
|
|
|
|
data.forEach((row, 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 || row.floorHarb) / 1e18;
|
|
const floorKraiken = parseFloat(row.floorToken0 || row.floorEth) / 1e18;
|
|
const anchorTickLower = parseFloat(row.anchorTickLower);
|
|
const anchorTickUpper = parseFloat(row.anchorTickUpper);
|
|
const anchorEth = parseFloat(row.anchorToken0 || row.anchorEth) / 1e18;
|
|
const anchorKraiken = parseFloat(row.anchorToken1 || row.anchorHarb) / 1e18;
|
|
const discoveryTickLower = parseFloat(row.discoveryTickLower);
|
|
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
|
|
// Swap discovery values to match expected behavior
|
|
const discoveryEth = parseFloat(row.discoveryToken1 || row.discoveryHarb) / 1e18;
|
|
const discoveryKraiken = parseFloat(row.discoveryToken0 || row.discoveryEth) / 1e18;
|
|
|
|
let actionAmount = '';
|
|
let additionalInfo = '';
|
|
|
|
if (previousRow) {
|
|
const prevFloorEth = parseFloat(previousRow.floorToken1 || previousRow.floorHarb) / 1e18;
|
|
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || previousRow.floorEth) / 1e18;
|
|
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || previousRow.anchorEth) / 1e18;
|
|
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || previousRow.anchorHarb) / 1e18;
|
|
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || previousRow.discoveryHarb) / 1e18;
|
|
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || previousRow.discoveryEth) / 1e18;
|
|
|
|
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
|
|
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
|
|
|
|
if (precedingAction.toLowerCase().includes('buy')) {
|
|
actionAmount = `${precedingAction} ETH`;
|
|
additionalInfo = `(${Math.abs(kraikenDifference).toFixed(6)} KRAIKEN bought)`;
|
|
} else if (precedingAction.toLowerCase().includes('sell')) {
|
|
actionAmount = `${precedingAction} KRAIKEN`;
|
|
additionalInfo = `(${Math.abs(ethDifference).toFixed(6)} ETH bought)`;
|
|
} else {
|
|
actionAmount = precedingAction;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
function calculateUniV3Liquidity(token0Amount, token1Amount, tickLower, tickUpper, currentTick) {
|
|
const priceLower = tickToPrice(tickLower);
|
|
const priceUpper = tickToPrice(tickUpper);
|
|
const priceCurrent = tickToPrice(currentTick);
|
|
|
|
const sqrtPriceLower = priceToSqrtPrice(priceLower);
|
|
const sqrtPriceUpper = priceToSqrtPrice(priceUpper);
|
|
const sqrtPriceCurrent = priceToSqrtPrice(priceCurrent);
|
|
|
|
// Handle edge cases where denominators would be zero
|
|
if (sqrtPriceUpper === sqrtPriceLower) {
|
|
return 0; // Invalid range
|
|
}
|
|
|
|
if (priceCurrent <= priceLower) {
|
|
// Price below range - position holds only token0 (ETH)
|
|
// Calculate liquidity from ETH amount using the full price range
|
|
if (token0Amount > 0) {
|
|
return token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower);
|
|
}
|
|
return 0;
|
|
} else if (priceCurrent >= priceUpper) {
|
|
// Price above range - position holds only token1 (KRAIKEN)
|
|
// Calculate liquidity from KRAIKEN amount using the full price range
|
|
if (token1Amount > 0) {
|
|
return token1Amount / (sqrtPriceUpper - sqrtPriceLower);
|
|
}
|
|
return 0;
|
|
} else {
|
|
// Price within range - calculate from both tokens and take minimum
|
|
let liquidityFromToken0 = 0;
|
|
let liquidityFromToken1 = 0;
|
|
|
|
if (token0Amount > 0 && sqrtPriceUpper > sqrtPriceCurrent) {
|
|
liquidityFromToken0 = token0Amount * (sqrtPriceUpper * sqrtPriceCurrent) / (sqrtPriceUpper - sqrtPriceCurrent);
|
|
}
|
|
|
|
if (token1Amount > 0 && sqrtPriceCurrent > sqrtPriceLower) {
|
|
liquidityFromToken1 = token1Amount / (sqrtPriceCurrent - sqrtPriceLower);
|
|
}
|
|
|
|
// Return the non-zero value, or minimum if both are present
|
|
if (liquidityFromToken0 > 0 && liquidityFromToken1 > 0) {
|
|
return Math.min(liquidityFromToken0, liquidityFromToken1);
|
|
}
|
|
return Math.max(liquidityFromToken0, liquidityFromToken1);
|
|
}
|
|
}
|
|
|
|
function simulateEnhanced(precedingAction, currentTick,
|
|
floorTickLower, floorTickUpper, floorEth, floorKraiken,
|
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
|
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
|
|
|
|
// Position data structure with liquidity calculations
|
|
const positions = {
|
|
floor: {
|
|
tickLower: floorTickLower,
|
|
tickUpper: floorTickUpper,
|
|
eth: floorEth,
|
|
kraiken: floorKraiken,
|
|
name: 'Floor',
|
|
liquidity: token0isWeth ?
|
|
calculateUniV3Liquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, currentTick) :
|
|
calculateUniV3Liquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, currentTick)
|
|
},
|
|
anchor: {
|
|
tickLower: anchorTickLower,
|
|
tickUpper: anchorTickUpper,
|
|
eth: anchorEth,
|
|
kraiken: anchorKraiken,
|
|
name: 'Anchor (Shallow Pool)',
|
|
liquidity: token0isWeth ?
|
|
calculateUniV3Liquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, currentTick) :
|
|
calculateUniV3Liquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, currentTick)
|
|
},
|
|
discovery: {
|
|
tickLower: discoveryTickLower,
|
|
tickUpper: discoveryTickUpper,
|
|
eth: discoveryEth,
|
|
kraiken: discoveryKraiken,
|
|
name: 'Discovery',
|
|
liquidity: token0isWeth ?
|
|
calculateUniV3Liquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, currentTick) :
|
|
calculateUniV3Liquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, currentTick)
|
|
}
|
|
};
|
|
|
|
// Calculate total active liquidity
|
|
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
|
|
|
// Create container for this scenario
|
|
const scenarioContainer = document.createElement('div');
|
|
scenarioContainer.className = 'scenario-container';
|
|
|
|
// Create header
|
|
const header = document.createElement('div');
|
|
header.className = 'scenario-header';
|
|
header.innerHTML = `<h3>${precedingAction}</h3>`;
|
|
scenarioContainer.appendChild(header);
|
|
|
|
// Create legend
|
|
const legend = document.createElement('div');
|
|
legend.className = 'legend';
|
|
legend.innerHTML = `
|
|
<div class="legend-item">
|
|
<div class="legend-color floor"></div>
|
|
<span>Floor Position (Foundation)</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color anchor"></div>
|
|
<span>Anchor Position (Current Price)</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color discovery"></div>
|
|
<span>Discovery Position (Growth)</span>
|
|
</div>
|
|
`;
|
|
scenarioContainer.appendChild(legend);
|
|
|
|
// Create charts container
|
|
const chartsContainer = document.createElement('div');
|
|
chartsContainer.className = 'charts-container';
|
|
|
|
// Create combined chart
|
|
const chartWrapper = document.createElement('div');
|
|
chartWrapper.className = 'chart-wrapper';
|
|
chartWrapper.style.width = '100%'; // Full width for single chart
|
|
const chartTitle = document.createElement('div');
|
|
chartTitle.className = 'chart-title';
|
|
chartTitle.textContent = 'Token Distribution by Position';
|
|
const combinedChart = document.createElement('div');
|
|
combinedChart.className = 'chart-div';
|
|
combinedChart.id = `combined-chart-${Date.now()}-${Math.random()}`;
|
|
chartWrapper.appendChild(chartTitle);
|
|
chartWrapper.appendChild(combinedChart);
|
|
|
|
chartsContainer.appendChild(chartWrapper);
|
|
scenarioContainer.appendChild(chartsContainer);
|
|
|
|
// Create summary panel
|
|
const summaryPanel = createSummaryPanel(positions, currentTick);
|
|
scenarioContainer.appendChild(summaryPanel);
|
|
|
|
// Add to page
|
|
document.getElementById('simulations').appendChild(scenarioContainer);
|
|
|
|
// Create the combined chart
|
|
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity);
|
|
}
|
|
|
|
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity) {
|
|
const positionKeys = ['floor', 'anchor', 'discovery'];
|
|
|
|
// Calculate bar widths to represent actual tick ranges
|
|
const barWidths = positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
return pos.tickUpper - pos.tickLower; // Width = actual tick range
|
|
});
|
|
|
|
// Calculate bar positions (centered in tick ranges)
|
|
const barPositions = positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2;
|
|
});
|
|
|
|
// ETH trace (left y-axis)
|
|
const ethTrace = {
|
|
x: barPositions,
|
|
y: positionKeys.map(key => positions[key].eth),
|
|
width: barWidths,
|
|
type: 'bar',
|
|
name: 'ETH',
|
|
yaxis: 'y',
|
|
marker: {
|
|
color: positionKeys.map(key => POSITION_COLORS[key]),
|
|
opacity: 0.7,
|
|
line: {
|
|
color: 'white',
|
|
width: 2
|
|
}
|
|
},
|
|
text: positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
return `${pos.name}<br>ETH: ${pos.eth.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
|
}),
|
|
hoverinfo: 'text'
|
|
};
|
|
|
|
// KRAIKEN trace (right y-axis)
|
|
const kraikenTrace = {
|
|
x: barPositions, // Same position as ETH bars
|
|
y: positionKeys.map(key => positions[key].kraiken),
|
|
width: barWidths,
|
|
type: 'bar',
|
|
name: 'KRAIKEN',
|
|
yaxis: 'y2',
|
|
marker: {
|
|
color: positionKeys.map(key => POSITION_COLORS[key]),
|
|
opacity: 0.4,
|
|
pattern: {
|
|
shape: '/',
|
|
size: 8
|
|
},
|
|
line: {
|
|
color: 'white',
|
|
width: 2
|
|
}
|
|
},
|
|
text: positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
return `${pos.name}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
|
}),
|
|
hoverinfo: 'text'
|
|
};
|
|
|
|
// Calculate x-axis range based on position ranges with some padding
|
|
const allTicks = positionKeys.flatMap(key => [positions[key].tickLower, positions[key].tickUpper]);
|
|
const minTick = Math.min(...allTicks);
|
|
const maxTick = Math.max(...allTicks);
|
|
const tickRange = maxTick - minTick;
|
|
const padding = tickRange * 0.1; // 10% padding on each side
|
|
const xAxisMin = minTick - padding;
|
|
const xAxisMax = maxTick + padding;
|
|
|
|
// Calculate max values for proper y-axis alignment
|
|
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
|
|
const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken));
|
|
const showPriceLine = currentTick >= xAxisMin && currentTick <= xAxisMax;
|
|
|
|
const data = [ethTrace, kraikenTrace];
|
|
|
|
if (showPriceLine) {
|
|
const priceLineTrace = {
|
|
x: [currentTick, currentTick],
|
|
y: [0, maxEth * 1.1],
|
|
mode: 'lines',
|
|
line: {
|
|
color: 'red',
|
|
width: 3,
|
|
dash: 'dash'
|
|
},
|
|
name: 'Current Price',
|
|
yaxis: 'y',
|
|
hoverinfo: 'x',
|
|
text: [`Current Price: ${currentTick}`],
|
|
showlegend: true
|
|
};
|
|
data.push(priceLineTrace);
|
|
}
|
|
|
|
const layout = {
|
|
title: {
|
|
text: `Token Distribution by Position (Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'})`,
|
|
font: { size: 16 }
|
|
},
|
|
xaxis: {
|
|
title: 'Price Ticks',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0',
|
|
range: [xAxisMin, xAxisMax]
|
|
},
|
|
yaxis: {
|
|
title: 'ETH Amount',
|
|
side: 'left',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0',
|
|
titlefont: { color: '#1f77b4' },
|
|
tickfont: { color: '#1f77b4' },
|
|
range: [0, maxEth * 1.1] // Start from 0, add 10% padding
|
|
},
|
|
yaxis2: {
|
|
title: 'KRAIKEN Amount',
|
|
side: 'right',
|
|
overlaying: 'y',
|
|
showgrid: false,
|
|
titlefont: { color: '#ff7f0e' },
|
|
tickfont: { color: '#ff7f0e' },
|
|
range: [0, maxKraiken * 1.1] // Start from 0, add 10% padding
|
|
},
|
|
showlegend: true,
|
|
legend: {
|
|
x: 0.02,
|
|
y: 0.98,
|
|
bgcolor: 'rgba(255,255,255,0.8)',
|
|
bordercolor: '#ccc',
|
|
borderwidth: 1
|
|
},
|
|
plot_bgcolor: 'white',
|
|
paper_bgcolor: 'white',
|
|
margin: { l: 60, r: 60, t: 60, b: 50 }
|
|
};
|
|
|
|
Plotly.newPlot(chartDiv, data, layout, {responsive: true});
|
|
}
|
|
|
|
function createDualCharts(ethChartDiv, kraikenChartDiv, positions, currentTick, totalLiquidity) {
|
|
const positionKeys = ['floor', 'anchor', 'discovery'];
|
|
|
|
// Calculate bar widths proportional to actual Uniswap V3 liquidity
|
|
const baseWidth = 50; // Base width for tick ranges
|
|
const barWidths = positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
const liquidityRatio = totalLiquidity > 0 ? pos.liquidity / totalLiquidity : 0;
|
|
return Math.max(baseWidth * 0.3, baseWidth * liquidityRatio * 3); // Scale for visibility
|
|
});
|
|
|
|
// Calculate bar positions (centered in tick ranges)
|
|
const barPositions = positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
return pos.tickLower + (pos.tickUpper - pos.tickLower) / 2;
|
|
});
|
|
|
|
// ETH Chart Data
|
|
const ethData = [{
|
|
x: barPositions,
|
|
y: positionKeys.map(key => positions[key].eth),
|
|
width: barWidths,
|
|
type: 'bar',
|
|
marker: {
|
|
color: positionKeys.map(key => POSITION_COLORS[key]),
|
|
opacity: 0.8,
|
|
line: {
|
|
color: 'white',
|
|
width: 2
|
|
}
|
|
},
|
|
text: positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
let tooltip = `${pos.name} Position<br>`;
|
|
|
|
// Show token amounts and actual Uniswap V3 liquidity
|
|
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
|
|
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
|
|
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
|
return tooltip;
|
|
}),
|
|
hoverinfo: 'text',
|
|
name: 'ETH Liquidity'
|
|
}];
|
|
|
|
// Kraiken Chart Data
|
|
const kraikenData = [{
|
|
x: barPositions,
|
|
y: positionKeys.map(key => positions[key].kraiken),
|
|
width: barWidths,
|
|
type: 'bar',
|
|
marker: {
|
|
color: positionKeys.map(key => POSITION_COLORS[key]),
|
|
opacity: 0.8,
|
|
line: {
|
|
color: 'white',
|
|
width: 2
|
|
}
|
|
},
|
|
text: positionKeys.map(key => {
|
|
const pos = positions[key];
|
|
let tooltip = `${pos.name} Position<br>`;
|
|
|
|
// Show token amounts and actual Uniswap V3 liquidity
|
|
tooltip += `ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toFixed(6)}`;
|
|
tooltip += `<br>Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)}`;
|
|
tooltip += `<br>Range: [${pos.tickLower}, ${pos.tickUpper}]`;
|
|
return tooltip;
|
|
}),
|
|
hoverinfo: 'text',
|
|
name: 'KRAIKEN Liquidity'
|
|
}];
|
|
|
|
// Add current price line to both charts
|
|
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
|
|
const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken));
|
|
|
|
const priceLineEth = {
|
|
x: [currentTick, currentTick],
|
|
y: [0, maxEth * 1.1],
|
|
mode: 'lines',
|
|
line: {
|
|
color: 'red',
|
|
width: 3,
|
|
dash: 'dash'
|
|
},
|
|
name: 'Current Price',
|
|
hoverinfo: 'x',
|
|
text: [`Current Price: ${currentTick}`]
|
|
};
|
|
|
|
const priceLineKraiken = {
|
|
x: [currentTick, currentTick],
|
|
y: [0, maxKraiken * 1.1],
|
|
mode: 'lines',
|
|
line: {
|
|
color: 'red',
|
|
width: 3,
|
|
dash: 'dash'
|
|
},
|
|
name: 'Current Price',
|
|
hoverinfo: 'x',
|
|
text: [`Current Price: ${currentTick}`]
|
|
};
|
|
|
|
ethData.push(priceLineEth);
|
|
kraikenData.push(priceLineKraiken);
|
|
|
|
// Create synchronized layouts
|
|
const ethLayout = {
|
|
title: {
|
|
text: 'ETH Liquidity by Position',
|
|
font: { size: 16 }
|
|
},
|
|
xaxis: {
|
|
title: 'Price Ticks',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0'
|
|
},
|
|
yaxis: {
|
|
title: 'ETH Amount',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0'
|
|
},
|
|
showlegend: false,
|
|
plot_bgcolor: 'white',
|
|
paper_bgcolor: 'white',
|
|
margin: { l: 60, r: 30, t: 60, b: 50 }
|
|
};
|
|
|
|
const kraikenLayout = {
|
|
title: {
|
|
text: 'KRAIKEN Liquidity by Position',
|
|
font: { size: 16 }
|
|
},
|
|
xaxis: {
|
|
title: 'Price Ticks',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0'
|
|
},
|
|
yaxis: {
|
|
title: 'KRAIKEN Amount',
|
|
showgrid: true,
|
|
gridcolor: '#e0e0e0'
|
|
},
|
|
showlegend: false,
|
|
plot_bgcolor: 'white',
|
|
paper_bgcolor: 'white',
|
|
margin: { l: 60, r: 30, t: 60, b: 50 }
|
|
};
|
|
|
|
// Plot both charts
|
|
Plotly.newPlot(ethChartDiv, ethData, ethLayout, {responsive: true});
|
|
Plotly.newPlot(kraikenChartDiv, kraikenData, kraikenLayout, {responsive: true});
|
|
|
|
// Add synchronized interactions
|
|
synchronizeCharts(ethChartDiv, kraikenChartDiv);
|
|
}
|
|
|
|
function synchronizeCharts(chart1, chart2) {
|
|
// Synchronize hover events
|
|
chart1.on('plotly_hover', function(data) {
|
|
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
|
|
const pointIndex = data.points[0].pointNumber;
|
|
Plotly.Fx.hover(chart2, [{curveNumber: 0, pointNumber: pointIndex}]);
|
|
}
|
|
});
|
|
|
|
chart2.on('plotly_hover', function(data) {
|
|
if (data.points && data.points[0] && data.points[0].pointNumber !== undefined) {
|
|
const pointIndex = data.points[0].pointNumber;
|
|
Plotly.Fx.hover(chart1, [{curveNumber: 0, pointNumber: pointIndex}]);
|
|
}
|
|
});
|
|
|
|
// Synchronize unhover events
|
|
chart1.on('plotly_unhover', function() {
|
|
Plotly.Fx.unhover(chart2);
|
|
});
|
|
|
|
chart2.on('plotly_unhover', function() {
|
|
Plotly.Fx.unhover(chart1);
|
|
});
|
|
}
|
|
|
|
function createSummaryPanel(positions, currentTick) {
|
|
const panel = document.createElement('div');
|
|
panel.className = 'summary-panel';
|
|
|
|
const title = document.createElement('h4');
|
|
title.textContent = 'Position Summary';
|
|
title.style.margin = '0 0 15px 0';
|
|
panel.appendChild(title);
|
|
|
|
const grid = document.createElement('div');
|
|
grid.className = 'summary-grid';
|
|
|
|
// Calculate totals
|
|
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0);
|
|
const totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
|
|
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
|
|
|
// Add total summary
|
|
const totalItem = document.createElement('div');
|
|
totalItem.className = 'summary-item';
|
|
totalItem.innerHTML = `
|
|
<strong>Total Portfolio</strong><br>
|
|
Token ETH: ${totalEth.toFixed(6)}<br>
|
|
Token KRAIKEN: ${totalKraiken.toFixed(6)}<br>
|
|
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toFixed(2)}
|
|
`;
|
|
grid.appendChild(totalItem);
|
|
|
|
// Add position summaries
|
|
Object.entries(positions).forEach(([key, pos]) => {
|
|
const item = document.createElement('div');
|
|
item.className = `summary-item ${key}`;
|
|
|
|
// Calculate position-specific liquidity percentage
|
|
const liquidityPercent = totalUniV3Liquidity > 0 ? (pos.liquidity / totalUniV3Liquidity * 100).toFixed(1) : '0.0';
|
|
const tickRange = pos.tickUpper - pos.tickLower;
|
|
|
|
item.innerHTML = `
|
|
<strong>${pos.name} Position</strong><br>
|
|
ETH: ${pos.eth.toFixed(6)}<br>
|
|
KRAIKEN: ${pos.kraiken.toFixed(6)}<br>
|
|
Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)} (${liquidityPercent}%)<br>
|
|
Range: ${tickRange} ticks
|
|
`;
|
|
grid.appendChild(item);
|
|
});
|
|
|
|
// Add current price info
|
|
const priceItem = document.createElement('div');
|
|
priceItem.className = 'summary-item';
|
|
priceItem.innerHTML = `
|
|
<strong>Current Price</strong><br>
|
|
Tick: ${currentTick}<br>
|
|
<small>Price line shown in red</small>
|
|
`;
|
|
grid.appendChild(priceItem);
|
|
|
|
panel.appendChild(grid);
|
|
return panel;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |