better visualizer

This commit is contained in:
johba 2025-08-17 15:09:41 +02:00
parent 6a012c5fd9
commit 50eac74b18
8 changed files with 542 additions and 626 deletions

View file

@ -1,131 +1,56 @@
# KRAIKEN LiquidityManager Fuzzing Analysis
Tools for testing the KRAIKEN LiquidityManager's resilience against various trading strategies to identify scenarios where traders can profit.
Tools for testing KRAIKEN's three-position strategy resilience against various market conditions and trading patterns.
## Quick Start
### Using run-fuzzing.sh (Recommended)
The `run-fuzzing.sh` script provides an easy way to run fuzzing campaigns with different market optimizers:
```bash
# Basic usage - run with specific optimizer
# Run with specific optimizer (50 runs default)
./analysis/run-fuzzing.sh BullMarketOptimizer
# Specify number of runs (default: 50)
./analysis/run-fuzzing.sh WhaleOptimizer runs=100
# Custom runs and trades
./analysis/run-fuzzing.sh WhaleOptimizer runs=100 trades=30
# Specify trades per run (default: 20, actual will be ±5)
./analysis/run-fuzzing.sh BearMarketOptimizer runs=10 trades=50
# Debug mode - generates position tracking CSV (forces runs=1)
# Debug mode with position tracking CSV (forces runs=1)
./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV
# Multiple parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=25 trades=30
```
**Available optimizers:**
- `BullMarketOptimizer` - Biased towards buying
- `NeutralMarketOptimizer` - Balanced trading
- `BearMarketOptimizer` - Biased towards selling
- `WhaleOptimizer` - Large position trading
- `MockOptimizer` - Test optimizer
## Available Optimizers
- `BullMarketOptimizer` - Buying bias
- `NeutralMarketOptimizer` - Balanced trading
- `BearMarketOptimizer` - Selling bias
- `WhaleOptimizer` - Large positions
- `RandomScenarioOptimizer` - Random behavior
**Features:**
- Automatic results aggregation and summary generation
- Progress tracking with colored output
- Cumulative P&L calculation across all runs
- Automatic visualization launch for profitable scenarios
- Organized output in timestamped directories
## Output Structure
### Manual Fuzzing (Advanced)
```bash
# Run fuzzing analysis with default settings (100 runs per market)
forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
# Custom configuration
FUZZING_RUNS=500 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
# With position tracking (generates detailed CSV for each scenario)
TRACK_POSITIONS=true FUZZING_RUNS=50 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
```
## Configuration
### run-fuzzing.sh Parameters
- **optimizer_class**: Required. The optimizer class to use (e.g., BullMarketOptimizer)
- **runs=N**: Optional. Number of fuzzing runs (default: 50)
- **trades=N**: Optional. Trades per run (default: 20, actual will be ±5)
- **debugCSV**: Optional. Enable debug mode with position tracking CSV (forces runs=1)
### Environment Variables (Manual Mode)
- **FUZZING_RUNS**: Number of random trading scenarios per market type (default: 100)
- **TRACK_POSITIONS**: Enable detailed position tracking CSV output (default: false)
- **OPTIMIZER_CLASS**: The optimizer to use (default: BullMarketOptimizer)
- **TRADES_PER_RUN**: Number of trades per run (default: 20)
- **SEED_OFFSET**: Starting seed for random number generation (default: 0)
## How It Works
1. **Real Deployments**: Deploys actual Uniswap V3 factory, pool, and LiquidityManager
2. **Random Trading**: Generates random buy/sell patterns with varying amounts and timing
3. **Recenter Calls**: Triggers `lm.recenter()` at random intervals
4. **Profit Detection**: Identifies scenarios where traders end with more ETH than they started
5. **CSV Export**: Saves all profitable scenarios to `profitable_scenarios_[timestamp].csv`
## Output Files
### Using run-fuzzing.sh
Each campaign creates a timestamped directory: `fuzzing_results_[optimizer]_[timestamp]/`
- `config.txt` - Campaign configuration
Each campaign creates `fuzzing_results_[optimizer]_[timestamp]/`:
- `config.txt` - Campaign parameters
- `run_*.log` - Individual run logs
- `merged_profitable_scenarios.csv` - All profitable scenarios combined
- `summary.txt` - Campaign summary with statistics
- `debug_positions_*.csv` - Position tracking data (when debugCSV is used)
### Manual Mode
- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences
- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true)
- `merged_profitable_scenarios.csv` - Profitable scenarios combined
- `summary.txt` - Statistics and cumulative P&L
- `debug_positions_*.csv` - Position data (debugCSV mode only)
## Visualization
```bash
# View results in browser
python3 -m http.server 8000
# Open http://localhost:8000/scenario-visualizer.html
# Automatic launch with debugCSV
./analysis/run-fuzzing.sh [optimizer] debugCSV
# Or use the shell script
./view-scenarios.sh
# Manual server (port 8000)
./analysis/view-scenarios.sh
```
## Analysis Tools
- `AnalysisVisualizer.py` - Generates charts from CSV data
- `scenario-visualizer.html` - Interactive web visualization
- `RISK_ANALYSIS_FINDINGS.md` - Summary of discovered vulnerabilities
## Components
- `run-fuzzing.sh` - Main campaign runner with automatic visualization
- `FuzzingAnalysis.s.sol` - Core fuzzing script
- `helpers/SwapExecutor.sol` - Shared swap execution logic
- `helpers/CSVManager.sol` - CSV generation utilities
- `helpers/CSVHelper.sol` - CSV formatting helpers
## Example Campaign Comparison
To run fuzzing campaigns comparing different market optimizers:
## Advanced Usage
```bash
# Run campaigns for all three market conditions
./analysis/run-fuzzing.sh BullMarketOptimizer runs=100
./analysis/run-fuzzing.sh NeutralMarketOptimizer runs=100
./analysis/run-fuzzing.sh BearMarketOptimizer runs=100
# Check results
cat fuzzing_results_*/summary.txt | grep -E "(Optimizer:|Success rate:|Average P&L)"
# Manual fuzzing with environment variables
FUZZING_RUNS=500 TRACK_POSITIONS=true forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
```
Environment variables:
- `FUZZING_RUNS` - Scenarios per market (default: 100)
- `TRACK_POSITIONS` - Enable position CSV (default: false)
- `OPTIMIZER_CLASS` - Optimizer to use
- `TRADES_PER_RUN` - Trades per run (default: 20)

View file

@ -343,14 +343,6 @@ contract FuzzingAnalysis is Test, CSVManager {
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Calculate ETH and HARB amounts in each position using proper Uniswap math
uint256 floorEth = 0;
uint256 floorHarb = 0;
uint256 anchorEth = 0;
uint256 anchorHarb = 0;
uint256 discoveryEth = 0;
uint256 discoveryHarb = 0;
// Debug: Log liquidity values
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
console.log("=== LIQUIDITY VALUES ===");
@ -372,125 +364,19 @@ contract FuzzingAnalysis is Test, CSVManager {
}
}
// Calculate amounts for each position using LiquidityAmounts library
if (floorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorUpper);
// Calculate actual deposited amounts based on position relative to current price
if (token0isWeth) {
if (currentTick < floorLower) {
// Position is above current price - contains only token1 (KRAIKEN)
floorEth = 0;
// Use position's lower tick for actual deposited amount
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else if (currentTick >= floorUpper) {
// Position is below current price - contains only token0 (WETH)
// Use position's upper tick for actual deposited amount
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorHarb = 0;
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
} else {
if (currentTick < floorLower) {
// Position is above current price
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorEth = 0;
} else if (currentTick >= floorUpper) {
// Position is below current price
floorHarb = 0;
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
}
}
if (anchorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchorUpper);
if (token0isWeth) {
if (currentTick < anchorLower) {
anchorEth = 0;
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else if (currentTick >= anchorUpper) {
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
} else {
if (currentTick < anchorLower) {
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorEth = 0;
} else if (currentTick >= anchorUpper) {
anchorHarb = 0;
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
}
}
if (discoveryLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discoveryLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discoveryUpper);
if (token0isWeth) {
if (currentTick < discoveryLower) {
discoveryEth = 0;
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else if (currentTick >= discoveryUpper) {
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
} else {
if (currentTick < discoveryLower) {
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = 0;
} else if (currentTick >= discoveryUpper) {
discoveryHarb = 0;
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
}
}
// Create position data row matching the expected CSV format
// Create position data row with liquidity values directly
string memory row = string.concat(
label, ", ",
vm.toString(currentTick), ", ",
vm.toString(floorLower), ", ",
vm.toString(floorUpper), ", ",
vm.toString(floorEth), ", ",
vm.toString(floorHarb), ", ",
vm.toString(uint256(floorLiq)), ", ",
vm.toString(anchorLower), ", ",
vm.toString(anchorUpper), ", ",
vm.toString(anchorEth), ", ",
vm.toString(anchorHarb), ", ",
vm.toString(uint256(anchorLiq)), ", ",
vm.toString(discoveryLower), ", ",
vm.toString(discoveryUpper), ", ",
vm.toString(discoveryEth), ", ",
vm.toString(discoveryHarb), ", ",
vm.toString(uint256(discoveryLiq)), ", ",
token0isWeth ? "true" : "false"
);
appendCSVRow(row);

View file

@ -15,7 +15,7 @@ library CSVHelper {
*/
function createPositionsHeader() internal pure returns (string memory) {
return
"precedingAction, currentTick, floorTickLower, floorTickUpper, floorToken0, floorToken1, anchorTickLower, anchorTickUpper, anchorToken0, anchorToken1, discoveryTickLower, discoveryTickUpper, discoveryToken0, discoveryToken1, token0isWeth";
"precedingAction, currentTick, floorTickLower, floorTickUpper, floorLiquidity, anchorTickLower, anchorTickUpper, anchorLiquidity, discoveryTickLower, discoveryTickUpper, discoveryLiquidity, token0isWeth";
}
function createTimeSeriesHeader() internal pure returns (string memory) {

View file

@ -343,36 +343,66 @@ if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
# Use absolute path for the symlink
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
# Start the viewer in background in its own process group
setsid ./view-scenarios.sh &
VIEWER_PID=$!
# Check if server is already running on common ports
SERVER_RUNNING=false
EXISTING_PORT=""
for PORT in 8000 8001 8002; do
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
# Check if it's a python http server in our analysis directory
if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then
SERVER_RUNNING=true
EXISTING_PORT=$PORT
break
fi
fi
done
# Give the server time to start and browser to open
sleep 2
# Show the URL
echo ""
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
echo "(If port 8000 was busy, check the port number mentioned above)"
echo "If browser didn't open, manually navigate to that URL"
# Wait for user input
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
# Kill the viewer process and its children
if [ -n "$VIEWER_PID" ]; then
# Kill the entire process group (includes python server)
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
# Give it a moment to clean up
sleep 1
# Force kill if still running
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
if [ "$SERVER_RUNNING" = true ]; then
echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}"
echo -e "${GREEN}Browser should open to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}"
# Try to open browser to existing server
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
elif command -v open &> /dev/null; then
open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
fi
echo ""
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
read -r
else
# Start the viewer in background in its own process group
setsid ./view-scenarios.sh &
VIEWER_PID=$!
# Give the server time to start and browser to open
sleep 2
# Show the URL
echo ""
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
echo "(If port 8000 was busy, check the port number mentioned above)"
echo "If browser didn't open, manually navigate to that URL"
# Wait for user input
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
# Kill the viewer process and its children
if [ -n "$VIEWER_PID" ]; then
# Kill the entire process group (includes python server)
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
# Give it a moment to clean up
sleep 1
# Force kill if still running
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
fi
echo -e "${GREEN}Viewer stopped.${NC}"
fi
# Clean up the symlink
rm -f "$TEMP_LINK"
echo -e "${GREEN}Viewer stopped.${NC}"
fi

View file

@ -133,16 +133,17 @@
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
<br>
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
0.5x = ETH is half as expensive (Floor position holds ETH)<br>
&lt; 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
• 1x = Current ETH price (red dashed line)<br>
2x = ETH is twice as expensive (Discovery position holds KRAIKEN)<br>
&gt; 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
<br>
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em>
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em><br>
<br>
<strong>Navigation:</strong> Use the Previous/Next buttons or URL parameter <code>?row=N</code> to view specific CSV rows
</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>
@ -171,6 +172,13 @@
// - Anchor Position: Mixed tokens around current price for shallow liquidity
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
// 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
}
// Auto-load CSV data on page load
document.addEventListener('DOMContentLoaded', function() {
loadCSVData();
@ -208,25 +216,10 @@
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
</div>
`;
console.log('CSV load error:', error);
// CSV load error - handled by status message above
});
}
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');
@ -252,37 +245,121 @@
}
function simulateCSVData(data) {
let previousRow = null;
data.forEach((row, index) => {
// Get selected row from URL parameter
const selectedIndex = getRowParameter();
// Add current row info and navigation
const currentRowInfo = document.createElement('div');
currentRowInfo.style.cssText = 'margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 4px; text-align: center;';
currentRowInfo.innerHTML = `
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex].precedingAction}</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 not first row
if (selectedIndex > 0) {
const topNavDiv = document.createElement('div');
topNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
topNavDiv.innerHTML = `
<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px;">
← Previous Row (Line ${selectedIndex + 1})
</button>
`;
document.getElementById('simulations').appendChild(topNavDiv);
}
// Process only the selected row
let previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
if (selectedIndex >= 0 && selectedIndex < data.length) {
const row = data[selectedIndex];
const index = selectedIndex;
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 floorLiquidity = parseFloat(row.floorLiquidity || 0);
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 anchorLiquidity = parseFloat(row.anchorLiquidity || 0);
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;
const discoveryLiquidity = parseFloat(row.discoveryLiquidity || 0);
// Calculate token amounts from liquidity
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
// FIXED calculation - properly determine ETH amounts based on token ordering
let floorEthAmount, anchorEthAmount, discoveryEthAmount;
let floorKraikenAmount, anchorKraikenAmount, discoveryKraikenAmount;
// Simply use the amounts from getAmountsForLiquidity and determine which is ETH
if (token0isWeth) {
// token0 is WETH, token1 is KRAIKEN
floorEthAmount = floorAmounts.amount0 / 1e18;
floorKraikenAmount = floorAmounts.amount1 / 1e18;
anchorEthAmount = anchorAmounts.amount0 / 1e18;
anchorKraikenAmount = anchorAmounts.amount1 / 1e18;
discoveryEthAmount = discoveryAmounts.amount0 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount1 / 1e18;
} else {
// token0 is KRAIKEN, token1 is WETH
floorEthAmount = floorAmounts.amount1 / 1e18;
floorKraikenAmount = floorAmounts.amount0 / 1e18;
anchorEthAmount = anchorAmounts.amount1 / 1e18;
anchorKraikenAmount = anchorAmounts.amount0 / 1e18;
discoveryEthAmount = discoveryAmounts.amount1 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount0 / 1e18;
}
const totalEth = floorEthAmount + anchorEthAmount + discoveryEthAmount;
// Use the already calculated ETH and KRAIKEN amounts from above
let floorEth = floorEthAmount;
let floorKraiken = floorKraikenAmount;
let anchorEth = anchorEthAmount;
let anchorKraiken = anchorKraikenAmount;
let discoveryEth = discoveryEthAmount;
let discoveryKraiken = discoveryKraikenAmount;
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;
// Calculate previous token amounts from liquidity
const prevCurrentTick = parseFloat(previousRow.currentTick);
const prevFloorLiquidity = parseFloat(previousRow.floorLiquidity || 0);
const prevAnchorLiquidity = parseFloat(previousRow.anchorLiquidity || 0);
const prevDiscoveryLiquidity = parseFloat(previousRow.discoveryLiquidity || 0);
const prevFloorAmounts = getAmountsForLiquidity(prevFloorLiquidity, floorTickLower, floorTickUpper, prevCurrentTick);
const prevAnchorAmounts = getAmountsForLiquidity(prevAnchorLiquidity, anchorTickLower, anchorTickUpper, prevCurrentTick);
const prevDiscoveryAmounts = getAmountsForLiquidity(prevDiscoveryLiquidity, discoveryTickLower, discoveryTickUpper, prevCurrentTick);
let prevFloorEth, prevFloorKraiken, prevAnchorEth, prevAnchorKraiken, prevDiscoveryEth, prevDiscoveryKraiken;
if (token0isWeth === true) {
prevFloorEth = prevFloorAmounts.amount0 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount1 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount0 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount1 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount0 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount1 / 1e18;
} else {
prevFloorEth = prevFloorAmounts.amount1 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount0 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount1 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount0 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount1 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount0 / 1e18;
}
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
@ -302,19 +379,83 @@
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;
});
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, precedingAction);
// Add navigation buttons below
const bottomNavDiv = document.createElement('div');
bottomNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
const prevButton = selectedIndex > 0 ?
`<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px; margin-right: 20px;">
← Previous Row (Line ${selectedIndex + 1})
</button>` : '';
const nextButton = selectedIndex < data.length - 1 ?
`<button onclick="navigateRow(1)" style="padding: 10px 20px; font-size: 16px;">
Next Row (Line ${selectedIndex + 3}) →
</button>` : '';
bottomNavDiv.innerHTML = prevButton + nextButton;
document.getElementById('simulations').appendChild(bottomNavDiv);
}
}
// Function to navigate between rows
function navigateRow(direction) {
const currentIndex = getRowParameter();
const newLineNumber = currentIndex + direction + 2; // Convert back to CSV line number
const url = new URL(window.location);
url.searchParams.set('row', newLineNumber);
window.location = url;
}
// Uniswap V3 liquidity calculation functions
function tickToPrice(tick) {
return Math.pow(1.0001, tick);
}
function tickToSqrtPriceX96(tick) {
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
}
// Calculate token amounts from liquidity for a position
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
// Sort ticks to ensure tickLower < tickUpper
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) {
// Current price is below the range, position holds only token0
// When position was created above current price, ETH was used as amount1
// Use the amount1 formula to get the ETH that was deposited
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else if (currentTick >= tickUpper) {
// Current price is above the range, position holds only token1
// amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else {
// Current price is within the range
// amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioX96) / (sqrtRatioX96 * sqrtRatioBX96) * Q96
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
// amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
}
return { amount0, amount1 };
}
function priceToSqrtPrice(price) {
return Math.sqrt(price);
}
@ -372,18 +513,6 @@
}
// Debug logging
if (positionName) {
console.log(`${positionName} liquidity calculation:`, {
token0Amount,
token1Amount,
tickRange: [tickLower, tickUpper],
sqrtPriceLower,
sqrtPriceUpper,
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
liquidity,
calculatedFrom
});
}
return liquidity;
}
@ -438,9 +567,9 @@
}
function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, originalAction) {
// Position data structure with liquidity calculations
const positions = {
@ -450,9 +579,7 @@
eth: floorEth,
kraiken: floorKraiken,
name: 'Floor',
liquidity: token0isWeth ?
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
liquidity: floorLiquidity
},
anchor: {
tickLower: anchorTickLower,
@ -460,9 +587,7 @@
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: token0isWeth ?
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
liquidity: anchorLiquidity
},
discovery: {
tickLower: discoveryTickLower,
@ -470,35 +595,10 @@
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: token0isWeth ?
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
liquidity: discoveryLiquidity
}
};
// 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);
@ -535,7 +635,7 @@
scenarioContainer.appendChild(chartsContainer);
// Create summary panel
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth);
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index);
scenarioContainer.appendChild(summaryPanel);
// Add to page
@ -565,18 +665,7 @@
...pos
};
console.log(`Position ${key}:`, {
ticks: [pos.tickLower, pos.tickUpper],
currentTick: currentTick,
multiples: [lowerMultiple, upperMultiple],
centerMultiple: centerMultiple,
token0isWeth: token0isWeth
});
// Warn about extreme positions
if (pos.tickLower > 180000 || pos.tickUpper > 180000) {
console.warn(`EXTREME TICKS: ${key} position has ticks above 180000, which represents extreme price multiples`);
}
});
// Calculate bar widths to represent actual price multiple ranges
@ -663,14 +752,6 @@
const xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x
const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max
// Debug logging for chart range
console.log('Chart x-axis range:', { xAxisMin, xAxisMax });
console.log('Bar positions:', barPositions);
console.log('Bar widths:', barWidths);
console.log('ETH values:', positionKeys.map(key => positions[key].eth));
console.log('KRAIKEN values:', positionKeys.map(key => positions[key].kraiken));
console.log('ETH trace y values (with min):', ethTrace.y);
console.log('KRAIKEN trace y values (with min):', kraikenTrace.y);
// Calculate max values for proper y-axis alignment
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
@ -692,17 +773,6 @@
return;
}
// Debug logging for very small or large values
if (totalLiquidity < 1 || tickRange > 10000 || pos.tickLower > 180000) {
console.log(`Warning: ${key} position has unusual values:`, {
liquidity: pos.liquidity,
tickRange: tickRange,
totalLiquidity: totalLiquidity,
ticks: [pos.tickLower, pos.tickUpper],
lowerMultiple: pos.lowerMultiple,
upperMultiple: pos.upperMultiple
});
}
// Create a filled area for each position to show its exact range
// Cap display coordinates to keep within visible range
@ -1027,7 +1097,7 @@
});
}
function createSummaryPanel(positions, currentTick, token0isWeth) {
function createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index) {
const panel = document.createElement('div');
panel.className = 'summary-panel';
@ -1043,16 +1113,17 @@
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 = `
const totalHtml = `
<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})}
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`;
totalItem.innerHTML = totalHtml;
grid.appendChild(totalItem);
// Add position summaries
@ -1070,8 +1141,8 @@
<strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
`;
grid.appendChild(item);
});
@ -1079,10 +1150,26 @@
// Add current price info
const priceItem = document.createElement('div');
priceItem.className = 'summary-item';
// Calculate current price
const currentPrice = tickToPrice(currentTick);
let ethPriceInKraiken, kraikenPriceInEth;
if (token0isWeth) {
// price = KRAIKEN/ETH
ethPriceInKraiken = currentPrice;
kraikenPriceInEth = 1 / currentPrice;
} else {
// price = ETH/KRAIKEN
kraikenPriceInEth = currentPrice;
ethPriceInKraiken = 1 / currentPrice;
}
priceItem.innerHTML = `
<strong>Current Price</strong><br>
Tick: ${currentTick}<br>
<small>Price line shown in red</small>
Tick: ${currentTick.toLocaleString()}<br>
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`;
grid.appendChild(priceItem);