2025-08-23 22:32:41 +02:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>Kraiken Fuzzing Run Visualizer</title>
|
|
|
|
|
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.file-selector {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
select {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border: 2px solid #ddd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
select:hover {
|
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
button {
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
button:hover {
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-panel {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border-left: 4px solid #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
.navigation {
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.navigation button {
|
|
|
|
|
|
margin: 0 10px;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.row-info {
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.instructions {
|
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
border-left: 4px solid #2196f3;
|
|
|
|
|
|
}
|
|
|
|
|
|
.error {
|
|
|
|
|
|
background-color: #ffebee;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
border-left: 4px solid #f44336;
|
|
|
|
|
|
color: #c62828;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.charts-container {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1>🐙 Kraiken Fuzzing Run Visualizer</h1>
|
|
|
|
|
|
<p>Analyze individual trades from fuzzing runs</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="instructions">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
• < 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
|
|
|
|
|
|
• 1x = Current ETH price (red dashed line)<br>
|
|
|
|
|
|
• > 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
|
|
|
|
|
|
<br>
|
|
|
|
|
|
<strong>Usage:</strong> Select a CSV file from the dropdown and use Previous/Next buttons to navigate through trades
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<div class="file-selector">
|
|
|
|
|
|
<label for="csvSelect">Select CSV File:</label>
|
|
|
|
|
|
<select id="csvSelect">
|
|
|
|
|
|
<option value="">Loading files...</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button onclick="refreshFileList()">Refresh List</button>
|
|
|
|
|
|
<button onclick="loadSelectedFile()">Load File</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-panel" id="fileInfo" style="display: none;">
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<div class="info-label">File</div>
|
|
|
|
|
|
<div class="info-value" id="fileName">-</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<div class="info-label">Total Rows</div>
|
|
|
|
|
|
<div class="info-value" id="totalRows">-</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<div class="info-label">Scenario ID</div>
|
|
|
|
|
|
<div class="info-value" id="scenarioId">-</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<div class="info-label">P&L</div>
|
|
|
|
|
|
<div class="info-value" id="pnl">-</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="errorMessage" class="error" style="display: none;"></div>
|
|
|
|
|
|
<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'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Global data storage
|
|
|
|
|
|
let currentData = null;
|
|
|
|
|
|
let currentFile = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Token ordering configuration
|
|
|
|
|
|
const ethIsToken0 = false; // Default matches test setup
|
|
|
|
|
|
|
|
|
|
|
|
// Get row parameter from URL
|
|
|
|
|
|
function getRowParameter() {
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const row = urlParams.get('row');
|
|
|
|
|
|
return row ? parseInt(row) - 2 : 0; // Convert CSV line number to array index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set row parameter in URL
|
|
|
|
|
|
function setRowParameter(lineNumber) {
|
|
|
|
|
|
const url = new URL(window.location);
|
|
|
|
|
|
url.searchParams.set('row', lineNumber);
|
|
|
|
|
|
window.history.pushState({}, '', url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-load on page load
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
refreshFileList();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshFileList() {
|
|
|
|
|
|
const select = document.getElementById('csvSelect');
|
|
|
|
|
|
select.innerHTML = '<option value="">Loading files...</option>';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Fetch list of CSV files from the server
|
|
|
|
|
|
const response = await fetch('./');
|
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
|
|
|
|
|
|
|
// Parse the directory listing to find CSV files
|
|
|
|
|
|
const parser = new DOMParser();
|
|
|
|
|
|
const doc = parser.parseFromString(text, 'text/html');
|
|
|
|
|
|
const links = doc.querySelectorAll('a');
|
|
|
|
|
|
|
|
|
|
|
|
const csvFiles = [];
|
|
|
|
|
|
links.forEach(link => {
|
|
|
|
|
|
const href = link.getAttribute('href');
|
|
|
|
|
|
if (href && href.endsWith('.csv')) {
|
|
|
|
|
|
// Filter for fuzz-XXXX-NNN.csv pattern
|
|
|
|
|
|
if (href.match(/fuzz-[A-Z0-9]{4}-\d{3}\.csv/)) {
|
|
|
|
|
|
csvFiles.push(href);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Sort files by name
|
|
|
|
|
|
csvFiles.sort();
|
|
|
|
|
|
|
|
|
|
|
|
// Populate select
|
|
|
|
|
|
select.innerHTML = '<option value="">-- Select a file --</option>';
|
|
|
|
|
|
csvFiles.forEach(file => {
|
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
|
option.value = file;
|
|
|
|
|
|
option.textContent = file;
|
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (csvFiles.length === 0) {
|
|
|
|
|
|
select.innerHTML = '<option value="">No CSV files found</option>';
|
|
|
|
|
|
showError('No fuzzing CSV files found. Run ./run-fuzzing.sh to generate data.');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error fetching file list:', error);
|
|
|
|
|
|
select.innerHTML = '<option value="">Error loading files</option>';
|
|
|
|
|
|
showError('Failed to load file list. Make sure the server is running from the analysis folder.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSelectedFile() {
|
|
|
|
|
|
const select = document.getElementById('csvSelect');
|
|
|
|
|
|
const fileName = select.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!fileName) {
|
|
|
|
|
|
showError('Please select a CSV file');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`./${fileName}`);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Failed to load ${fileName}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const csvText = await response.text();
|
|
|
|
|
|
currentFile = fileName;
|
|
|
|
|
|
loadCSVData(csvText, fileName);
|
|
|
|
|
|
hideError();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error loading file:', error);
|
|
|
|
|
|
showError(`Failed to load ${fileName}. Make sure the file exists and the server has access.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showError(message) {
|
|
|
|
|
|
const errorDiv = document.getElementById('errorMessage');
|
|
|
|
|
|
errorDiv.textContent = message;
|
|
|
|
|
|
errorDiv.style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hideError() {
|
|
|
|
|
|
document.getElementById('errorMessage').style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadCSVData(csvText, fileName) {
|
|
|
|
|
|
const data = parseCSV(csvText);
|
|
|
|
|
|
currentData = data;
|
|
|
|
|
|
|
|
|
|
|
|
// Update file info
|
|
|
|
|
|
const fileInfo = document.getElementById('fileInfo');
|
|
|
|
|
|
fileInfo.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('fileName').textContent = fileName;
|
|
|
|
|
|
document.getElementById('totalRows').textContent = data.length;
|
|
|
|
|
|
|
|
|
|
|
|
// Extract scenario ID from filename
|
|
|
|
|
|
const scenarioMatch = fileName.match(/fuzz-([A-Z0-9]{4})-/);
|
|
|
|
|
|
if (scenarioMatch) {
|
|
|
|
|
|
document.getElementById('scenarioId').textContent = scenarioMatch[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate P&L
|
|
|
|
|
|
const initRow = data.find(row => (row.action === 'INIT' || row.precedingAction === 'INIT'));
|
|
|
|
|
|
const finalRow = data.find(row => (row.action === 'FINAL' || row.precedingAction === 'FINAL'));
|
|
|
|
|
|
if (initRow && finalRow) {
|
|
|
|
|
|
const initEth = parseFloat(initRow.eth_balance || initRow.ethBalance) || 0;
|
|
|
|
|
|
const finalEth = parseFloat(finalRow.eth_balance || finalRow.ethBalance) || 0;
|
|
|
|
|
|
if (initEth > 0) {
|
|
|
|
|
|
const pnl = ((finalEth - initEth) / initEth * 100).toFixed(2);
|
|
|
|
|
|
const pnlElement = document.getElementById('pnl');
|
|
|
|
|
|
pnlElement.textContent = `${pnl > 0 ? '+' : ''}${pnl}%`;
|
|
|
|
|
|
pnlElement.style.color = pnl > 0 ? '#4CAF50' : '#f44336';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
simulateCSVData(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 simulateCSVData(data) {
|
|
|
|
|
|
// Clear previous simulations
|
|
|
|
|
|
document.getElementById('simulations').innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// Get selected row from URL parameter
|
|
|
|
|
|
const selectedIndex = getRowParameter();
|
|
|
|
|
|
|
|
|
|
|
|
// Add current row info and navigation
|
|
|
|
|
|
const currentRowInfo = document.createElement('div');
|
|
|
|
|
|
currentRowInfo.className = 'row-info';
|
|
|
|
|
|
currentRowInfo.innerHTML = `
|
|
|
|
|
|
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex]?.action || data[selectedIndex]?.precedingAction || 'Unknown'}</strong><br>
|
|
|
|
|
|
<span style="color: #666; font-size: 14px;">Total rows: ${data.length} (Lines 2-${data.length + 1})</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('simulations').appendChild(currentRowInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// Add navigation buttons above
|
|
|
|
|
|
if (selectedIndex > 0 || selectedIndex < data.length - 1) {
|
|
|
|
|
|
const topNavDiv = document.createElement('div');
|
|
|
|
|
|
topNavDiv.className = 'navigation';
|
|
|
|
|
|
topNavDiv.innerHTML = `
|
|
|
|
|
|
${selectedIndex > 0 ? `
|
|
|
|
|
|
<button onclick="navigateRow(-1)">
|
|
|
|
|
|
← Previous (Line ${selectedIndex + 1})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${selectedIndex < data.length - 1 ? `
|
|
|
|
|
|
<button onclick="navigateRow(1)">
|
|
|
|
|
|
Next (Line ${selectedIndex + 3}) →
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('simulations').appendChild(topNavDiv);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Process selected row
|
|
|
|
|
|
if (selectedIndex >= 0 && selectedIndex < data.length) {
|
|
|
|
|
|
const row = data[selectedIndex];
|
|
|
|
|
|
const previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
|
|
|
|
|
|
processRow(row, selectedIndex, previousRow);
|
|
|
|
|
|
|
|
|
|
|
|
// Add navigation buttons below
|
|
|
|
|
|
const bottomNavDiv = document.createElement('div');
|
|
|
|
|
|
bottomNavDiv.className = 'navigation';
|
|
|
|
|
|
bottomNavDiv.innerHTML = `
|
|
|
|
|
|
${selectedIndex > 0 ? `
|
|
|
|
|
|
<button onclick="navigateRow(-1)">
|
|
|
|
|
|
← Previous (Line ${selectedIndex + 1})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${selectedIndex < data.length - 1 ? `
|
|
|
|
|
|
<button onclick="navigateRow(1)">
|
|
|
|
|
|
Next (Line ${selectedIndex + 3}) →
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('simulations').appendChild(bottomNavDiv);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function navigateRow(direction) {
|
|
|
|
|
|
const currentIndex = getRowParameter();
|
|
|
|
|
|
const newIndex = currentIndex + direction;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentData && newIndex >= 0 && newIndex < currentData.length) {
|
|
|
|
|
|
setRowParameter(newIndex + 2); // Convert to CSV line number
|
|
|
|
|
|
simulateCSVData(currentData);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function processRow(row, index, previousRow) {
|
|
|
|
|
|
// Map column names from actual CSV format
|
|
|
|
|
|
const precedingAction = row.action || row.precedingAction;
|
|
|
|
|
|
const currentTick = parseFloat(row.tick || row.currentTick);
|
|
|
|
|
|
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true || false;
|
|
|
|
|
|
|
|
|
|
|
|
const floorTickLower = parseFloat(row.floor_lower || row.floorTickLower);
|
|
|
|
|
|
const floorTickUpper = parseFloat(row.floor_upper || row.floorTickUpper);
|
|
|
|
|
|
const floorLiquidity = parseFloat(row.floor_liq || row.floorLiquidity || 0);
|
|
|
|
|
|
|
|
|
|
|
|
const anchorTickLower = parseFloat(row.anchor_lower || row.anchorTickLower);
|
|
|
|
|
|
const anchorTickUpper = parseFloat(row.anchor_upper || row.anchorTickUpper);
|
|
|
|
|
|
const anchorLiquidity = parseFloat(row.anchor_liq || row.anchorLiquidity || 0);
|
|
|
|
|
|
|
|
|
|
|
|
const discoveryTickLower = parseFloat(row.discovery_lower || row.discoveryTickLower);
|
|
|
|
|
|
const discoveryTickUpper = parseFloat(row.discovery_upper || row.discoveryTickUpper);
|
|
|
|
|
|
const discoveryLiquidity = parseFloat(row.discovery_liq || row.discoveryLiquidity || 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Extract staking metrics if available
|
|
|
|
|
|
const percentageStaked = row.percentageStaked ? parseFloat(row.percentageStaked) : 0;
|
|
|
|
|
|
const avgTaxRate = row.avgTaxRate ? parseFloat(row.avgTaxRate) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate token amounts
|
|
|
|
|
|
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
|
|
|
|
|
|
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
|
|
|
|
|
|
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
|
|
|
|
|
|
|
|
|
|
|
|
let floorEth, anchorEth, discoveryEth;
|
|
|
|
|
|
let floorKraiken, anchorKraiken, discoveryKraiken;
|
|
|
|
|
|
|
2025-08-24 18:38:48 +02:00
|
|
|
|
// Note: In KRAIKEN protocol, positions are named opposite to their price location:
|
|
|
|
|
|
// - "Floor" position is actually at high ticks (above current price)
|
|
|
|
|
|
// - "Discovery" position is at low ticks (below current price)
|
|
|
|
|
|
// This is counterintuitive but matches the contract implementation
|
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
|
if (token0isWeth) {
|
2025-08-24 18:38:48 +02:00
|
|
|
|
// Floor position (high ticks, above current) holds token0 (ETH) when above price
|
2025-08-23 22:32:41 +02:00
|
|
|
|
floorEth = floorAmounts.amount0 / 1e18;
|
|
|
|
|
|
floorKraiken = floorAmounts.amount1 / 1e18;
|
|
|
|
|
|
anchorEth = anchorAmounts.amount0 / 1e18;
|
|
|
|
|
|
anchorKraiken = anchorAmounts.amount1 / 1e18;
|
2025-08-24 18:38:48 +02:00
|
|
|
|
// Discovery position (low ticks, below current) holds token1 (KRAIKEN) when below price
|
2025-08-23 22:32:41 +02:00
|
|
|
|
discoveryEth = discoveryAmounts.amount0 / 1e18;
|
|
|
|
|
|
discoveryKraiken = discoveryAmounts.amount1 / 1e18;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
floorEth = floorAmounts.amount1 / 1e18;
|
|
|
|
|
|
floorKraiken = floorAmounts.amount0 / 1e18;
|
|
|
|
|
|
anchorEth = anchorAmounts.amount1 / 1e18;
|
|
|
|
|
|
anchorKraiken = anchorAmounts.amount0 / 1e18;
|
|
|
|
|
|
discoveryEth = discoveryAmounts.amount1 / 1e18;
|
|
|
|
|
|
discoveryKraiken = discoveryAmounts.amount0 / 1e18;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create headline
|
|
|
|
|
|
const lineNumber = index + 2;
|
|
|
|
|
|
const headline = `Line ${lineNumber}: ${precedingAction}`;
|
|
|
|
|
|
|
|
|
|
|
|
simulateEnhanced(headline, currentTick,
|
|
|
|
|
|
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
|
|
|
|
|
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
|
|
|
|
|
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
|
|
|
|
|
|
token0isWeth, index, precedingAction, percentageStaked, avgTaxRate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Uniswap V3 calculation functions
|
|
|
|
|
|
function tickToPrice(tick) {
|
|
|
|
|
|
return Math.pow(1.0001, tick);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tickToSqrtPriceX96(tick) {
|
|
|
|
|
|
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
|
|
|
|
|
|
if (tickLower > tickUpper) {
|
|
|
|
|
|
[tickLower, tickUpper] = [tickUpper, tickLower];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sqrtRatioAX96 = tickToSqrtPriceX96(tickLower);
|
|
|
|
|
|
const sqrtRatioBX96 = tickToSqrtPriceX96(tickUpper);
|
|
|
|
|
|
const sqrtRatioX96 = tickToSqrtPriceX96(currentTick);
|
|
|
|
|
|
|
|
|
|
|
|
let amount0 = 0;
|
|
|
|
|
|
let amount1 = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const Q96 = 2 ** 96;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentTick < tickLower) {
|
|
|
|
|
|
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
|
|
|
|
|
|
} else if (currentTick >= tickUpper) {
|
|
|
|
|
|
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
|
|
|
|
|
|
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { amount0, amount1 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tickToPriceMultiple(tick, currentTick, token0isWeth) {
|
|
|
|
|
|
const price = tickToPrice(tick);
|
|
|
|
|
|
const currentPrice = tickToPrice(currentTick);
|
|
|
|
|
|
|
|
|
|
|
|
if (token0isWeth) {
|
|
|
|
|
|
return currentPrice / price;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return price / currentPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function simulateEnhanced(precedingAction, currentTick,
|
|
|
|
|
|
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
|
|
|
|
|
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
|
|
|
|
|
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
|
|
|
|
|
|
token0isWeth, index, originalAction, percentageStaked = 0, avgTaxRate = 0) {
|
|
|
|
|
|
|
|
|
|
|
|
const positions = {
|
|
|
|
|
|
floor: {
|
|
|
|
|
|
tickLower: floorTickLower,
|
|
|
|
|
|
tickUpper: floorTickUpper,
|
|
|
|
|
|
eth: floorEth,
|
|
|
|
|
|
kraiken: floorKraiken,
|
|
|
|
|
|
name: 'Floor',
|
|
|
|
|
|
liquidity: floorLiquidity
|
|
|
|
|
|
},
|
|
|
|
|
|
anchor: {
|
|
|
|
|
|
tickLower: anchorTickLower,
|
|
|
|
|
|
tickUpper: anchorTickUpper,
|
|
|
|
|
|
eth: anchorEth,
|
|
|
|
|
|
kraiken: anchorKraiken,
|
|
|
|
|
|
name: 'Anchor (Shallow Pool)',
|
|
|
|
|
|
liquidity: anchorLiquidity
|
|
|
|
|
|
},
|
|
|
|
|
|
discovery: {
|
|
|
|
|
|
tickLower: discoveryTickLower,
|
|
|
|
|
|
tickUpper: discoveryTickUpper,
|
|
|
|
|
|
eth: discoveryEth,
|
|
|
|
|
|
kraiken: discoveryKraiken,
|
|
|
|
|
|
name: 'Discovery',
|
|
|
|
|
|
liquidity: discoveryLiquidity
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Create container
|
|
|
|
|
|
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%';
|
|
|
|
|
|
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, originalAction || precedingAction, index, percentageStaked, avgTaxRate);
|
|
|
|
|
|
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 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
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Create liquidity traces
|
|
|
|
|
|
const liquidityTraces = [];
|
|
|
|
|
|
const MAX_REASONABLE_MULTIPLE = 50;
|
|
|
|
|
|
const MIN_REASONABLE_MULTIPLE = 0.02;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
const xAxisMin = Math.max(0.01, minMultiple - padding);
|
|
|
|
|
|
const xAxisMax = Math.min(100, maxMultiple + padding);
|
|
|
|
|
|
|
|
|
|
|
|
positionKeys.forEach(key => {
|
|
|
|
|
|
const pos = priceMultiplePositions[key];
|
|
|
|
|
|
const tickRange = pos.tickUpper - pos.tickLower;
|
|
|
|
|
|
const totalLiquidity = pos.liquidity * tickRange;
|
|
|
|
|
|
|
|
|
|
|
|
if (pos.liquidity === 0 || totalLiquidity === 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let displayLower = pos.lowerMultiple;
|
|
|
|
|
|
let displayUpper = pos.upperMultiple;
|
|
|
|
|
|
|
|
|
|
|
|
if (pos.upperMultiple < 0.01) {
|
|
|
|
|
|
displayLower = xAxisMin;
|
|
|
|
|
|
displayUpper = xAxisMin * 1.5;
|
|
|
|
|
|
} else if (pos.lowerMultiple > 50) {
|
|
|
|
|
|
displayLower = xAxisMax * 0.8;
|
|
|
|
|
|
displayUpper = xAxisMax;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
displayLower = Math.max(xAxisMin, Math.min(xAxisMax, pos.lowerMultiple));
|
|
|
|
|
|
displayUpper = Math.max(xAxisMin, Math.min(xAxisMax, pos.upperMultiple));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
|
|
|
|
|
|
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 y-axis range
|
|
|
|
|
|
const totalLiquidities = positionKeys.map(key => {
|
|
|
|
|
|
const pos = priceMultiplePositions[key];
|
|
|
|
|
|
const tickRange = pos.tickUpper - pos.tickLower;
|
|
|
|
|
|
return pos.liquidity * tickRange;
|
|
|
|
|
|
}).filter(l => l > 0);
|
|
|
|
|
|
|
|
|
|
|
|
const maxTotalLiquidity = totalLiquidities.length > 0 ? Math.max(...totalLiquidities) : 1;
|
|
|
|
|
|
const minTotalLiquidity = totalLiquidities.length > 0 ? Math.min(...totalLiquidities) : 0.1;
|
|
|
|
|
|
|
|
|
|
|
|
const yMin = Math.max(1e-10, minTotalLiquidity / 100);
|
|
|
|
|
|
const yMax = maxTotalLiquidity * 10;
|
|
|
|
|
|
|
|
|
|
|
|
// Add price line
|
|
|
|
|
|
const priceLineTrace = {
|
|
|
|
|
|
x: [1, 1],
|
|
|
|
|
|
y: [yMin, yMax],
|
|
|
|
|
|
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'
|
|
|
|
|
|
},
|
|
|
|
|
|
yaxis: {
|
|
|
|
|
|
title: 'Total Liquidity (L × Ticks)',
|
|
|
|
|
|
type: 'log',
|
|
|
|
|
|
showgrid: true,
|
|
|
|
|
|
gridcolor: '#e0e0e0',
|
|
|
|
|
|
dtick: 1,
|
|
|
|
|
|
tickformat: '.0e',
|
|
|
|
|
|
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 createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index, percentageStaked = 0, avgTaxRate = 0) {
|
|
|
|
|
|
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>
|
|
|
|
|
|
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
|
|
|
|
|
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
|
|
|
|
|
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
|
|
|
|
|
|
`;
|
|
|
|
|
|
grid.appendChild(totalItem);
|
|
|
|
|
|
|
|
|
|
|
|
// Add position summaries
|
|
|
|
|
|
Object.entries(positions).forEach(([key, pos]) => {
|
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
|
item.className = `summary-item ${key}`;
|
|
|
|
|
|
|
|
|
|
|
|
const liquidityPercent = totalUniV3Liquidity > 0 ? (pos.liquidity / totalUniV3Liquidity * 100).toFixed(1) : '0.0';
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
|
|
|
|
|
|
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
|
|
|
|
|
|
`;
|
|
|
|
|
|
grid.appendChild(item);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add current price info
|
|
|
|
|
|
const priceItem = document.createElement('div');
|
|
|
|
|
|
priceItem.className = 'summary-item';
|
|
|
|
|
|
|
|
|
|
|
|
const currentPrice = tickToPrice(currentTick);
|
|
|
|
|
|
let ethPriceInKraiken, kraikenPriceInEth;
|
|
|
|
|
|
|
|
|
|
|
|
if (token0isWeth) {
|
|
|
|
|
|
ethPriceInKraiken = currentPrice;
|
|
|
|
|
|
kraikenPriceInEth = 1 / currentPrice;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
kraikenPriceInEth = currentPrice;
|
|
|
|
|
|
ethPriceInKraiken = 1 / currentPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
priceItem.innerHTML = `
|
|
|
|
|
|
<strong>Current Price</strong><br>
|
|
|
|
|
|
Tick: ${currentTick.toLocaleString()}<br>
|
|
|
|
|
|
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
|
|
|
|
|
|
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
|
|
|
|
|
|
`;
|
|
|
|
|
|
grid.appendChild(priceItem);
|
|
|
|
|
|
|
|
|
|
|
|
// Add staking metrics if available
|
|
|
|
|
|
if (percentageStaked > 0 || avgTaxRate > 0) {
|
|
|
|
|
|
const stakingItem = document.createElement('div');
|
|
|
|
|
|
stakingItem.className = 'summary-item';
|
|
|
|
|
|
|
|
|
|
|
|
const stakingPercent = (percentageStaked / 1e16).toFixed(2);
|
|
|
|
|
|
const taxRatePercent = (avgTaxRate / 1e16).toFixed(2);
|
|
|
|
|
|
|
|
|
|
|
|
stakingItem.innerHTML = `
|
|
|
|
|
|
<strong>Staking Metrics</strong><br>
|
|
|
|
|
|
Staked: ${stakingPercent}%<br>
|
|
|
|
|
|
Avg Tax Rate: ${taxRatePercent}%
|
|
|
|
|
|
`;
|
|
|
|
|
|
grid.appendChild(stakingItem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
panel.appendChild(grid);
|
|
|
|
|
|
return panel;
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|