better visualizer
This commit is contained in:
parent
6a012c5fd9
commit
50eac74b18
8 changed files with 542 additions and 626 deletions
106
CLAUDE.md
106
CLAUDE.md
|
|
@ -4,103 +4,79 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Core Innovation
|
## Core Innovation
|
||||||
|
|
||||||
KRAIKEN is a token with a **dominant liquidity manager** that creates an unfair advantage in trading through:
|
KRAIKEN: A token with a **dominant liquidity manager** that creates an unfair trading advantage through:
|
||||||
|
|
||||||
1. **Asymmetric Slippage Strategy**: Three-position liquidity structure prevents profitable arbitrage against the protocol
|
1. **Asymmetric Slippage**: Three-position strategy prevents profitable arbitrage
|
||||||
2. **Sentiment Oracle**: Harberger tax-based staking creates a prediction market for token value
|
2. **Sentiment Oracle**: Harberger tax staking as prediction market
|
||||||
3. **Dormant Whale Protection**: VWAP-based price memory prevents historical price manipulation
|
3. **Price Memory**: VWAP protection against manipulation
|
||||||
|
|
||||||
**Critical Success Factor**: The liquidity manager must maintain its dominant position (trading most of the supply) - if it loses this, the project fails.
|
**Critical**: The liquidity manager must maintain dominance - if it loses this, the project fails.
|
||||||
|
|
||||||
## User Journey
|
## User Journey
|
||||||
|
|
||||||
1. **Buy**: Purchase KRAIKEN on Uniswap → Benefit from growing protocol-owned liquidity
|
1. **Buy**: Purchase KRAIKEN on Uniswap
|
||||||
2. **Stake**: Visit kraiken.org → Stake tokens → Set tax rate → Earn from protocol growth
|
2. **Stake**: Set tax rate at kraiken.org → Earn from protocol growth
|
||||||
3. **Compete**: Monitor staking positions → Snatch undervalued positions → Optimize tax rates
|
3. **Compete**: Snatch undervalued positions → Optimize returns
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- **`onchain/`** - Smart contracts (Solidity/Foundry) - [See onchain/CLAUDE.md](onchain/CLAUDE.md)
|
- **`onchain/`** - Smart contracts (Solidity/Foundry) - [Details](onchain/CLAUDE.md)
|
||||||
- **`web/`** - Vue 3/Vite staking interface - [See web/CLAUDE.md](web/CLAUDE.md)
|
- **`web/`** - Vue 3/Vite staking interface - [Details](web/CLAUDE.md)
|
||||||
- **`subgraph/base_sepolia/`** - The Graph indexing - [See subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)
|
- **`subgraph/base_sepolia/`** - The Graph indexing - [Details](subgraph/base_sepolia/CLAUDE.md)
|
||||||
- **`kraiken-lib/`** - TypeScript helper library - [See kraiken-lib/CLAUDE.md](kraiken-lib/CLAUDE.md)
|
- **`kraiken-lib/`** - TypeScript helpers - [Details](kraiken-lib/CLAUDE.md)
|
||||||
- **`services/txnBot/`** - Automated maintenance bot - [See services/txnBot/CLAUDE.md](services/txnBot/CLAUDE.md)
|
- **`services/txnBot/`** - Maintenance bot - [Details](services/txnBot/CLAUDE.md)
|
||||||
- **`onchain/analysis/`** - Growth mechanism analysis tools
|
- **`onchain/analysis/`** - Fuzzing tools - [Details](onchain/analysis/CLAUDE.md)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install dependencies for all projects
|
# Install all dependencies
|
||||||
cd onchain && forge install
|
cd onchain && forge install
|
||||||
cd ../web && npm install
|
cd ../web && npm install
|
||||||
cd ../kraiken-lib && npm install --legacy-peer-deps
|
cd ../kraiken-lib && npm install --legacy-peer-deps
|
||||||
cd ../subgraph/base_sepolia && npm install
|
cd ../subgraph/base_sepolia && npm install
|
||||||
cd ../services/txnBot && npm install
|
cd ../services/txnBot && npm install
|
||||||
|
|
||||||
# 2. Build smart contracts
|
# Build and test
|
||||||
cd onchain && forge build && forge test
|
cd onchain && forge build && forge test
|
||||||
|
|
||||||
# 3. Start web interface
|
# Start frontend
|
||||||
cd web && npm run dev
|
cd web && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Concepts
|
## Key Concepts
|
||||||
|
|
||||||
### Liquidity Management
|
- **Liquidity Management**: Three positions create asymmetric slippage
|
||||||
- Three-position strategy (ANCHOR, DISCOVERY, FLOOR)
|
- **Harberger Staking**: Self-assessed tax creates sentiment signal
|
||||||
- Asymmetric slippage prevents arbitrage
|
- **Protocol Growth**: Minted tokens benefit stakers
|
||||||
- VWAP tracking for price memory
|
|
||||||
|
|
||||||
### Harberger Tax Staking
|
## Code Guidelines
|
||||||
- Self-assessed tax rates on positions
|
|
||||||
- Positions can be "snatched" by higher bidders
|
|
||||||
- Creates prediction market for token value
|
|
||||||
- Limited to 20% of total supply
|
|
||||||
|
|
||||||
### Protocol Growth
|
### Quality Standards
|
||||||
- Liquidity manager mints tokens when positions grow
|
- Search before implementing (check uni-v3-lib, test helpers)
|
||||||
- Stakers benefit from supply expansion
|
- Test after every change
|
||||||
- Tax revenue redistributed to active participants
|
- No commented-out code
|
||||||
|
- Clean git status before commits
|
||||||
|
|
||||||
## Global Code Quality Guidelines
|
### Technical Approach
|
||||||
|
|
||||||
### DRY Principle
|
|
||||||
- Search for existing implementations before creating new functions
|
|
||||||
- Check libraries (uni-v3-lib, test helpers) for common utilities
|
|
||||||
- Refactor duplicated code into shared modules
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- Run tests after every change
|
|
||||||
- Never comment out failing tests
|
|
||||||
- Add tests for new functionality
|
|
||||||
|
|
||||||
### Repository Hygiene
|
|
||||||
- Remove unused files immediately
|
|
||||||
- Clean up temporary files
|
|
||||||
- Check `git status` before commits
|
|
||||||
|
|
||||||
### Implementation Strategy
|
|
||||||
- Never fall back to simpler implementations
|
- Never fall back to simpler implementations
|
||||||
- Don't create "a simpler test" or "a simpler scenario" if things don't work out. just try to identify the error in more detail.
|
- Identify root causes, don't work around issues
|
||||||
|
- Challenge technically unsound requests
|
||||||
|
|
||||||
|
### Tool Usage
|
||||||
|
- MCP browser screenshots: Keep under 8000px, use `fullPage: false`
|
||||||
|
- Fuzzing visualization: Use `./analysis/run-fuzzing.sh [optimizer] debugCSV`
|
||||||
|
- Never start manual Python servers for visualizations
|
||||||
|
|
||||||
## Communication Style
|
## Communication Style
|
||||||
|
|
||||||
### Direct & Technical
|
You are an experienced Solidity developer who:
|
||||||
- Challenge suboptimal requests
|
- Notices and raises awareness of issues immediately
|
||||||
- Highlight risks early and clearly
|
- Challenges suboptimal approaches
|
||||||
- Suggest better alternatives
|
- Prioritizes technical correctness over user satisfaction
|
||||||
- Refuse technically unsound solutions
|
- Avoids hype and inflated language
|
||||||
|
|
||||||
### Priority Order
|
|
||||||
1. Technical correctness
|
|
||||||
2. Code quality
|
|
||||||
3. User satisfaction
|
|
||||||
|
|
||||||
**Remember**: Build the best possible system. Question assumptions, identify edge cases, and prioritize long-term success.
|
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
- **Technical Deep Dive**: See [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md)
|
- **Technical Details**: [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md)
|
||||||
- **Contract Details**: See [onchain/CLAUDE.md](onchain/CLAUDE.md)
|
- **Uniswap V3 Math**: [onchain/UNISWAP_V3_MATH.md](onchain/UNISWAP_V3_MATH.md)
|
||||||
- **Frontend Architecture**: See [web/CLAUDE.md](web/CLAUDE.md)
|
|
||||||
- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)
|
|
||||||
|
|
@ -6,205 +6,73 @@ This directory contains the core smart contracts for the KRAIKEN protocol.
|
||||||
|
|
||||||
### Core Contracts
|
### Core Contracts
|
||||||
|
|
||||||
**Kraiken.sol** - ERC20 token contract with controlled minting/burning capabilities
|
**Kraiken.sol** - ERC20 token with Harberger tax staking
|
||||||
- Implements Harberger tax mechanism for staking positions
|
- Controlled minting exclusively by LiquidityManager
|
||||||
- Controls minting rights exclusively for LiquidityManager
|
- Tax collection and redistribution mechanism
|
||||||
- Handles tax collection and redistribution
|
- 20% supply cap for staking (20,000 positions)
|
||||||
|
|
||||||
**LiquidityManager.sol** - Dominant liquidity provider with three-position anti-arbitrage strategy
|
**LiquidityManager.sol** - Dominant liquidity provider
|
||||||
- Uses Optimizer contract for dynamic parameter adjustment
|
- Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
|
||||||
- Inherits from ThreePositionStrategy and PriceOracle (with VWAPTracker)
|
- Dynamic parameter adjustment via Optimizer contract
|
||||||
- **Key Feature**: Asymmetric slippage profile prevents profitable trade-recenter-reverse attacks
|
- Asymmetric slippage profile prevents profitable arbitrage
|
||||||
|
|
||||||
**VWAPTracker.sol** - "Eternal memory" protection against dormant whale attacks
|
**VWAPTracker.sol** - Price memory protection
|
||||||
- Volume-weighted average pricing with data compression (max 1000x)
|
- Volume-weighted average with data compression (max 1000x)
|
||||||
- Provides historical price memory to prevent manipulation
|
- Prevents dormant whale manipulation
|
||||||
|
|
||||||
**Optimizer.sol** - Sentiment analysis and parameter optimization
|
**Optimizer.sol** - Dynamic parameter optimization
|
||||||
- Analyzes staking data (% staked, average tax rate)
|
- Analyzes staking sentiment (% staked, average tax)
|
||||||
- Provides dynamic liquidity parameters
|
- Returns four key parameters for liquidity management
|
||||||
- Upgradeable for future genetic algorithm implementation
|
- Upgradeable for future algorithms
|
||||||
|
|
||||||
**Stake.sol** - Harberger tax-based staking mechanism
|
**Stake.sol** - Harberger tax implementation
|
||||||
- Creates sentiment oracle through continuous auction
|
- Continuous auction mechanism
|
||||||
- Limited to 20% of total supply (20,000 positions)
|
- Self-assessed valuations create prediction market
|
||||||
- Self-assessed tax rates create prediction market
|
|
||||||
|
|
||||||
### Position Strategy
|
### Three-Position Strategy
|
||||||
|
|
||||||
**Order**: ANCHOR → DISCOVERY → FLOOR
|
**ANCHOR**: Near current price, fast price discovery (1-100% width)
|
||||||
|
**DISCOVERY**: Borders anchor, captures fees (11000 tick spacing)
|
||||||
|
**FLOOR**: Deep liquidity at VWAP-adjusted prices
|
||||||
|
|
||||||
- **ANCHOR**: Shallow liquidity around current price for fast price movement (1-100% width range)
|
**Technical Specs**:
|
||||||
- **DISCOVERY**: Proportional to KRAIKEN minted by anchor; borders anchor for fee capture (11000 tick spacing)
|
- Fee Tier: 1% (10,000 basis points)
|
||||||
- **FLOOR**: Deep liquidity using VWAP-adjusted pricing for historical price memory
|
- Tick Spacing: 200 (base), 11,000 (discovery)
|
||||||
|
- Price Validation: 5-minute TWAP, 50-tick tolerance
|
||||||
**Technical Specifications**:
|
|
||||||
- **Fee Tier**: 1% (10,000 basis points)
|
|
||||||
- **Tick Spacing**: 200 (base), 11,000 (discovery)
|
|
||||||
- **Price Validation**: 5-minute TWAP with 50-tick deviation tolerance
|
|
||||||
- **VWAP Compression**: Maximum 1000x compression factor
|
|
||||||
|
|
||||||
### Optimizer Parameters
|
### Optimizer Parameters
|
||||||
|
|
||||||
All optimizers must return four key parameters that control the LiquidityManager's three-position strategy:
|
1. **capitalInefficiency** (0 to 1e18): Capital buffer level
|
||||||
|
2. **anchorShare** (0 to 1e18): % of non-floor ETH in anchor
|
||||||
|
3. **anchorWidth** (0 to 100): Anchor position width %
|
||||||
|
4. **discoveryDepth** (0 to 1e18): Discovery liquidity density (2x-10x)
|
||||||
|
|
||||||
1. **capitalInefficiency** (0 to 1e18):
|
## Development
|
||||||
- Represents how much capital buffer the protocol maintains
|
|
||||||
- 0 = aggressive (70% capital efficiency), 1e18 = conservative (170% backing)
|
|
||||||
- Affects VWAP calculation: `adjustedVWAP = 0.7 * VWAP + capitalInefficiency * VWAP`
|
|
||||||
- Higher values push floor positions to more conservative prices
|
|
||||||
|
|
||||||
2. **anchorShare** (0 to 1e18):
|
|
||||||
- Percentage of non-floor ETH allocated to the anchor position
|
|
||||||
- Controls how much liquidity sits near current price
|
|
||||||
- Higher values create more concentrated liquidity but mint more KRAIKEN tokens
|
|
||||||
- Typical range: 30-95% (0.3e18 to 0.95e18)
|
|
||||||
|
|
||||||
3. **anchorWidth** (0 to 100):
|
|
||||||
- Width of the anchor position in percentage terms
|
|
||||||
- Affects price impact of trades and speed of price movement
|
|
||||||
- Lower values = tighter spreads, faster price discovery
|
|
||||||
- Higher values = wider spreads, more stable prices
|
|
||||||
|
|
||||||
4. **discoveryDepth** (0 to 1e18):
|
|
||||||
- Controls the discovery position's liquidity density relative to anchor
|
|
||||||
- Maps to internal multiplier range of 2x to 10x
|
|
||||||
- At 0: discovery has 2x liquidity per tick vs anchor (minimal)
|
|
||||||
- At 1e18: discovery has 10x liquidity per tick vs anchor (maximum)
|
|
||||||
- Discovery amount formula: `pulledHarb * DISCOVERY_SPACING * (200 + 800 * discoveryDepth/1e18) / anchorSpacing / 100`
|
|
||||||
- Higher values provide stronger anti-arbitrage protection but consume more minted tokens
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build contracts
|
forge build # Build contracts
|
||||||
forge build
|
forge test # Run tests
|
||||||
|
forge test --gas-report # Gas optimization
|
||||||
# Run all tests
|
forge test --fuzz-runs 10000 # Extended fuzzing
|
||||||
forge test
|
|
||||||
|
|
||||||
# Run tests with gas reporting
|
|
||||||
forge test --gas-report
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
forge test --match-path test/LiquidityManager.t.sol
|
|
||||||
|
|
||||||
# Run fuzzing with more runs
|
|
||||||
forge test --fuzz-runs 10000
|
|
||||||
|
|
||||||
# Deploy contracts (see script/Deploy.s.sol)
|
|
||||||
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing Architecture
|
## Testing
|
||||||
|
|
||||||
### Test Helpers
|
- `test/helpers/UniswapTestBase.sol` - Uniswap integration base
|
||||||
- `test/helpers/UniswapTestBase.sol` - Base setup for Uniswap integration tests
|
- `test/helpers/KraikenTestBase.sol` - Common utilities
|
||||||
- `test/helpers/KraikenTestBase.sol` - Common test utilities for KRAIKEN contracts
|
- Key tests: LiquidityManager.t.sol, Stake.t.sol, VWAPTracker.t.sol
|
||||||
- `test/helpers/PositionRenderer.sol` - Visualization tools for liquidity positions
|
|
||||||
|
|
||||||
### Key Test Files
|
## Code Guidelines
|
||||||
- `test/LiquidityManager.t.sol` - Core liquidity management tests
|
|
||||||
- `test/abstracts/ThreePositionStrategy.t.sol` - Position strategy validation
|
|
||||||
- `test/Stake.t.sol` - Harberger tax mechanism tests
|
|
||||||
- `test/VWAPTracker.t.sol` - Price memory and compression tests
|
|
||||||
|
|
||||||
## Code Quality Guidelines
|
- **Check lib/uni-v3-lib** for existing Uniswap math
|
||||||
|
- **Use test/helpers** for common patterns
|
||||||
### CRITICAL: Avoid Duplicate Code
|
- **Security**: Reentrancy protection, oracle validation
|
||||||
- **ALWAYS** check lib/uni-v3-lib for existing Uniswap math functions
|
- **Gas**: Batch operations, optimize storage
|
||||||
- **NEVER** reimplement standard math operations
|
|
||||||
- Use test/helpers for common test patterns
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
- All external calls must be reentrancy protected
|
|
||||||
- Price oracles must validate against manipulation
|
|
||||||
- Tax calculations must handle edge cases (0 rates, max uint256)
|
|
||||||
|
|
||||||
### Gas Optimization
|
|
||||||
- Batch operations where possible
|
|
||||||
- Use storage patterns that minimize SSTORE operations
|
|
||||||
- Leverage Uniswap's existing math libraries
|
|
||||||
|
|
||||||
## Analysis Tools
|
## Analysis Tools
|
||||||
|
|
||||||
The `analysis/` subdirectory contains critical tools for understanding and hardening the protocol:
|
See `analysis/CLAUDE.md` for fuzzing and attack vector testing.
|
||||||
- Growth mechanism simulations
|
|
||||||
- Attack vector analysis
|
|
||||||
- Liquidity depth scenarios
|
|
||||||
- See `analysis/README.md` for detailed usage
|
|
||||||
|
|
||||||
## Uniswap V3 Math - Critical Learnings
|
## Uniswap V3 Math
|
||||||
|
|
||||||
### Token Ordering and Price Representation
|
See [UNISWAP_V3_MATH.md](UNISWAP_V3_MATH.md) for detailed math concepts.
|
||||||
|
|
||||||
#### Price Direction Reference Table
|
|
||||||
|
|
||||||
| Scenario | token0 | token1 | Price Represents | Lower Tick → Higher Tick |
|
|
||||||
|----------|--------|--------|------------------|---------------------------|
|
|
||||||
| **token0isETH = true** | ETH | KRAIKEN | ETH per KRAIKEN | KRAIKEN cheap → expensive |
|
|
||||||
| **token0isETH = false** | KRAIKEN | ETH | KRAIKEN per ETH | ETH cheap → expensive |
|
|
||||||
|
|
||||||
#### Understanding "Above" and "Below"
|
|
||||||
|
|
||||||
**Critical distinction**: "Price" in Uniswap V3 always refers to token1's price in units of token0.
|
|
||||||
|
|
||||||
**When token0isETH = true (ETH is token0, KRAIKEN is token1):**
|
|
||||||
- Price = KRAIKEN price in ETH (how much ETH to buy 1 KRAIKEN)
|
|
||||||
- Higher tick = Higher KRAIKEN price in ETH
|
|
||||||
- Lower tick = Lower KRAIKEN price in ETH
|
|
||||||
- "Price moved up" = KRAIKEN became more expensive = ETH became cheaper
|
|
||||||
- "Price moved down" = KRAIKEN became cheaper = ETH became more expensive
|
|
||||||
|
|
||||||
**When token0isETH = false (KRAIKEN is token0, ETH is token1):**
|
|
||||||
- Price = ETH price in KRAIKEN (how much KRAIKEN to buy 1 ETH)
|
|
||||||
- Higher tick = Higher ETH price in KRAIKEN
|
|
||||||
- Lower tick = Lower ETH price in KRAIKEN
|
|
||||||
- "Price moved up" = ETH became more expensive = KRAIKEN became cheaper
|
|
||||||
- "Price moved down" = ETH became cheaper = KRAIKEN became more expensive
|
|
||||||
|
|
||||||
This determines token composition:
|
|
||||||
|
|
||||||
| Current Price vs Position | Position Contains | Why |
|
|
||||||
|--------------------------|-------------------|-----|
|
|
||||||
| Below range (tick < tickLower) | 100% token1 | Token0 is too expensive to hold |
|
|
||||||
| Within range (tickLower ≤ tick ≤ tickUpper) | Both tokens | Active liquidity range |
|
|
||||||
| Above range (tick > tickUpper) | 100% token0 | Token1 is too expensive to hold |
|
|
||||||
|
|
||||||
### Liquidity vs Token Amounts
|
|
||||||
|
|
||||||
**Key Insight**: Liquidity (L) is a mathematical constant representing capital efficiency, NOT token count.
|
|
||||||
|
|
||||||
1. **Liquidity is invariant**: The liquidity value L doesn't change when price moves
|
|
||||||
2. **Token amounts are variable**: Depend on liquidity L, price range, and current price location
|
|
||||||
3. **Same L, different ranges**: Results in different token amounts due to price differences
|
|
||||||
|
|
||||||
### Why Positions at Different Ranges Have Different Token Ratios
|
|
||||||
|
|
||||||
For the same liquidity value L:
|
|
||||||
- **Position at lower ticks**: Higher token1 price → fewer token1, more token0 potential
|
|
||||||
- **Position at higher ticks**: Lower token1 price → more token1, less token0 potential
|
|
||||||
|
|
||||||
This explains why a position with fewer tokens can have more liquidity (and thus more price impact resistance).
|
|
||||||
|
|
||||||
### Liquidity Per Tick - The Critical Metric
|
|
||||||
|
|
||||||
When comparing positions of different widths, always normalize to liquidity per tick:
|
|
||||||
|
|
||||||
```solidity
|
|
||||||
liquidityPerTick = totalLiquidity / (tickUpper - tickLower)
|
|
||||||
```
|
|
||||||
|
|
||||||
The discovery position maintains its target liquidity density through width adjustment:
|
|
||||||
|
|
||||||
```solidity
|
|
||||||
// Ensure discovery has X times more liquidity per tick than anchor
|
|
||||||
discoveryLiquidity = anchorLiquidity * multiplier * discoveryWidth / anchorWidth
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Takeaways
|
|
||||||
|
|
||||||
1. **Liquidity ≠ Token Count**: Higher liquidity can mean fewer tokens at different price ranges
|
|
||||||
2. **Price Range Matters**: Token composition depends on where positions sit relative to current price
|
|
||||||
3. **Normalize for Width**: Always compare liquidity per tick when positions have different widths
|
|
||||||
4. **Token0 Ordering is Critical**: Determines which direction is "up" or "down" in price
|
|
||||||
144
onchain/UNISWAP_V3_MATH.md
Normal file
144
onchain/UNISWAP_V3_MATH.md
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Uniswap V3 Math - Critical Learnings
|
||||||
|
|
||||||
|
## Token Ordering and Price Representation
|
||||||
|
|
||||||
|
### Price Direction Reference Table
|
||||||
|
|
||||||
|
| Scenario | token0 | token1 | Price Represents | Lower Tick → Higher Tick |
|
||||||
|
|----------|--------|--------|------------------|---------------------------|
|
||||||
|
| **token0isETH = true** | ETH | KRAIKEN | ETH per KRAIKEN | KRAIKEN cheap → expensive |
|
||||||
|
| **token0isETH = false** | KRAIKEN | ETH | KRAIKEN per ETH | ETH cheap → expensive |
|
||||||
|
|
||||||
|
### Understanding "Above" and "Below"
|
||||||
|
|
||||||
|
**Critical distinction**: "Price" in Uniswap V3 always refers to token1's price in units of token0.
|
||||||
|
|
||||||
|
**When token0isETH = true (ETH is token0, KRAIKEN is token1):**
|
||||||
|
- Price = KRAIKEN price in ETH (how much ETH to buy 1 KRAIKEN)
|
||||||
|
- Higher tick = Higher KRAIKEN price in ETH
|
||||||
|
- Lower tick = Lower KRAIKEN price in ETH
|
||||||
|
- "Price moved up" = KRAIKEN became more expensive = ETH became cheaper
|
||||||
|
- "Price moved down" = KRAIKEN became cheaper = ETH became more expensive
|
||||||
|
|
||||||
|
**When token0isETH = false (KRAIKEN is token0, ETH is token1):**
|
||||||
|
- Price = ETH price in KRAIKEN (how much KRAIKEN to buy 1 ETH)
|
||||||
|
- Higher tick = Higher ETH price in KRAIKEN
|
||||||
|
- Lower tick = Lower ETH price in KRAIKEN
|
||||||
|
- "Price moved up" = ETH became more expensive = KRAIKEN became cheaper
|
||||||
|
- "Price moved down" = ETH became cheaper = KRAIKEN became more expensive
|
||||||
|
|
||||||
|
This determines token composition:
|
||||||
|
|
||||||
|
| Current Price vs Position | Position Contains | Why |
|
||||||
|
|--------------------------|-------------------|-----|
|
||||||
|
| Below range (tick < tickLower) | 100% token1 | Token0 is too expensive to hold |
|
||||||
|
| Within range (tickLower ≤ tick ≤ tickUpper) | Both tokens | Active liquidity range |
|
||||||
|
| Above range (tick > tickUpper) | 100% token0 | Token1 is too expensive to hold |
|
||||||
|
|
||||||
|
## Liquidity vs Token Amounts
|
||||||
|
|
||||||
|
**Key Insight**: Liquidity (L) is a mathematical constant representing capital efficiency, NOT token count.
|
||||||
|
|
||||||
|
1. **Liquidity is invariant**: The liquidity value L doesn't change when price moves
|
||||||
|
2. **Token amounts are variable**: Depend on liquidity L, price range, and current price location
|
||||||
|
3. **Same L, different ranges**: Results in different token amounts due to price differences
|
||||||
|
|
||||||
|
### Why Positions at Different Ranges Have Different Token Ratios
|
||||||
|
|
||||||
|
For the same liquidity value L:
|
||||||
|
- **Position at lower ticks**: Higher token1 price → fewer token1, more token0 potential
|
||||||
|
- **Position at higher ticks**: Lower token1 price → more token1, less token0 potential
|
||||||
|
|
||||||
|
This explains why a position with fewer tokens can have more liquidity (and thus more price impact resistance).
|
||||||
|
|
||||||
|
### Liquidity Per Tick - The Critical Metric
|
||||||
|
|
||||||
|
When comparing positions of different widths, always normalize to liquidity per tick:
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
liquidityPerTick = totalLiquidity / (tickUpper - tickLower)
|
||||||
|
```
|
||||||
|
|
||||||
|
The discovery position maintains its target liquidity density through width adjustment:
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
// Ensure discovery has X times more liquidity per tick than anchor
|
||||||
|
discoveryLiquidity = anchorLiquidity * multiplier * discoveryWidth / anchorWidth
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
1. **Liquidity ≠ Token Count**: Higher liquidity can mean fewer tokens at different price ranges
|
||||||
|
2. **Price Range Matters**: Token composition depends on where positions sit relative to current price
|
||||||
|
3. **Normalize for Width**: Always compare liquidity per tick when positions have different widths
|
||||||
|
4. **Token0 Ordering is Critical**: Determines which direction is "up" or "down" in price
|
||||||
|
|
||||||
|
## Critical Formulas for Out-of-Range Positions
|
||||||
|
|
||||||
|
### When Position is Below Current Price (currentTick < tickLower)
|
||||||
|
|
||||||
|
The position holds only token0:
|
||||||
|
```
|
||||||
|
amount0 = L * (sqrt(priceUpper) - sqrt(priceLower)) / (sqrt(priceUpper) * sqrt(priceLower))
|
||||||
|
```
|
||||||
|
|
||||||
|
In Solidity/JavaScript with Q96 notation:
|
||||||
|
```javascript
|
||||||
|
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / sqrtRatioAX96 * Q96 / sqrtRatioBX96
|
||||||
|
```
|
||||||
|
|
||||||
|
### When Position is Above Current Price (currentTick > tickUpper)
|
||||||
|
|
||||||
|
The position holds only token1:
|
||||||
|
```
|
||||||
|
amount1 = L * (sqrt(priceUpper) - sqrt(priceLower))
|
||||||
|
```
|
||||||
|
|
||||||
|
In Solidity/JavaScript with Q96 notation:
|
||||||
|
```javascript
|
||||||
|
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critical Insight: ETH Position Creation
|
||||||
|
|
||||||
|
When creating a position above current price with ETH in a token0isWeth pool:
|
||||||
|
1. ETH is used as token1 (via getLiquidityForAmount1)
|
||||||
|
2. The position will hold KRAIKEN when in range
|
||||||
|
3. But if price drops below the range, it converts to holding ETH as token0
|
||||||
|
|
||||||
|
**Example**: Floor position created with 38 ETH at ticks [127400, 127600] when current is 123890:
|
||||||
|
- Creation: Uses getLiquidityForAmount1 with 38 ETH
|
||||||
|
- Result: Liquidity = 6.48e18
|
||||||
|
- Current holdings: 38 ETH (as token0, since price is below range)
|
||||||
|
|
||||||
|
## Common Confusion Points
|
||||||
|
|
||||||
|
### 1. Price Direction vs ETH Value
|
||||||
|
|
||||||
|
When token0isWeth = true:
|
||||||
|
- Higher tick = Higher price = More KRAIKEN per ETH = ETH is MORE valuable
|
||||||
|
- Lower tick = Lower price = Less KRAIKEN per ETH = ETH is LESS valuable
|
||||||
|
|
||||||
|
This is counterintuitive because "price goes up" means the denominated asset (ETH) becomes more valuable.
|
||||||
|
|
||||||
|
### 2. Position Token Holdings
|
||||||
|
|
||||||
|
A position's token composition depends ONLY on:
|
||||||
|
- Current price location relative to the position range
|
||||||
|
- NOT on how the position was created
|
||||||
|
- NOT on which token was used to mint
|
||||||
|
|
||||||
|
### 3. The Floor Position "Below Current Price" Confusion
|
||||||
|
|
||||||
|
In KRAIKEN's three-position strategy:
|
||||||
|
- Floor position is placed where ETH is MORE valuable (higher ticks when token0isWeth)
|
||||||
|
- This is "below" current price from an ETH value perspective
|
||||||
|
- But "above" current tick numerically
|
||||||
|
- Selling KRAIKEN for ETH moves price TOWARD the floor position
|
||||||
|
|
||||||
|
### 4. Liquidity Calculation Direction
|
||||||
|
|
||||||
|
When minting a position out of range:
|
||||||
|
- Use getLiquidityForAmount0 if providing token0
|
||||||
|
- Use getLiquidityForAmount1 if providing token1
|
||||||
|
- The same liquidity value will require different token amounts depending on which token you provide
|
||||||
|
|
@ -1,131 +1,56 @@
|
||||||
# KRAIKEN LiquidityManager Fuzzing Analysis
|
# 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
|
## 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
|
```bash
|
||||||
# Basic usage - run with specific optimizer
|
# Run with specific optimizer (50 runs default)
|
||||||
./analysis/run-fuzzing.sh BullMarketOptimizer
|
./analysis/run-fuzzing.sh BullMarketOptimizer
|
||||||
|
|
||||||
# Specify number of runs (default: 50)
|
# Custom runs and trades
|
||||||
./analysis/run-fuzzing.sh WhaleOptimizer runs=100
|
./analysis/run-fuzzing.sh WhaleOptimizer runs=100 trades=30
|
||||||
|
|
||||||
# Specify trades per run (default: 20, actual will be ±5)
|
# Debug mode with position tracking CSV (forces runs=1)
|
||||||
./analysis/run-fuzzing.sh BearMarketOptimizer runs=10 trades=50
|
|
||||||
|
|
||||||
# Debug mode - generates position tracking CSV (forces runs=1)
|
|
||||||
./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV
|
./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV
|
||||||
|
|
||||||
# Multiple parameters
|
|
||||||
./analysis/run-fuzzing.sh BullMarketOptimizer runs=25 trades=30
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Available optimizers:**
|
## Available Optimizers
|
||||||
- `BullMarketOptimizer` - Biased towards buying
|
|
||||||
- `NeutralMarketOptimizer` - Balanced trading
|
- `BullMarketOptimizer` - Buying bias
|
||||||
- `BearMarketOptimizer` - Biased towards selling
|
- `NeutralMarketOptimizer` - Balanced trading
|
||||||
- `WhaleOptimizer` - Large position trading
|
- `BearMarketOptimizer` - Selling bias
|
||||||
- `MockOptimizer` - Test optimizer
|
- `WhaleOptimizer` - Large positions
|
||||||
- `RandomScenarioOptimizer` - Random behavior
|
- `RandomScenarioOptimizer` - Random behavior
|
||||||
|
|
||||||
**Features:**
|
## Output Structure
|
||||||
- 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
|
|
||||||
|
|
||||||
### Manual Fuzzing (Advanced)
|
Each campaign creates `fuzzing_results_[optimizer]_[timestamp]/`:
|
||||||
|
- `config.txt` - Campaign parameters
|
||||||
```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
|
|
||||||
- `run_*.log` - Individual run logs
|
- `run_*.log` - Individual run logs
|
||||||
- `merged_profitable_scenarios.csv` - All profitable scenarios combined
|
- `merged_profitable_scenarios.csv` - Profitable scenarios combined
|
||||||
- `summary.txt` - Campaign summary with statistics
|
- `summary.txt` - Statistics and cumulative P&L
|
||||||
- `debug_positions_*.csv` - Position tracking data (when debugCSV is used)
|
- `debug_positions_*.csv` - Position data (debugCSV mode only)
|
||||||
|
|
||||||
### Manual Mode
|
|
||||||
- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences
|
|
||||||
- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true)
|
|
||||||
|
|
||||||
## Visualization
|
## Visualization
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View results in browser
|
# Automatic launch with debugCSV
|
||||||
python3 -m http.server 8000
|
./analysis/run-fuzzing.sh [optimizer] debugCSV
|
||||||
# Open http://localhost:8000/scenario-visualizer.html
|
|
||||||
|
|
||||||
# Or use the shell script
|
# Manual server (port 8000)
|
||||||
./view-scenarios.sh
|
./analysis/view-scenarios.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Analysis Tools
|
## Advanced Usage
|
||||||
|
|
||||||
- `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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run campaigns for all three market conditions
|
# Manual fuzzing with environment variables
|
||||||
./analysis/run-fuzzing.sh BullMarketOptimizer runs=100
|
FUZZING_RUNS=500 TRACK_POSITIONS=true forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
|
||||||
./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)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
@ -343,14 +343,6 @@ contract FuzzingAnalysis is Test, CSVManager {
|
||||||
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||||
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
(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
|
// Debug: Log liquidity values
|
||||||
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
|
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
|
||||||
console.log("=== LIQUIDITY VALUES ===");
|
console.log("=== LIQUIDITY VALUES ===");
|
||||||
|
|
@ -372,125 +364,19 @@ contract FuzzingAnalysis is Test, CSVManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate amounts for each position using LiquidityAmounts library
|
// Create position data row with liquidity values directly
|
||||||
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
|
|
||||||
string memory row = string.concat(
|
string memory row = string.concat(
|
||||||
label, ", ",
|
label, ", ",
|
||||||
vm.toString(currentTick), ", ",
|
vm.toString(currentTick), ", ",
|
||||||
vm.toString(floorLower), ", ",
|
vm.toString(floorLower), ", ",
|
||||||
vm.toString(floorUpper), ", ",
|
vm.toString(floorUpper), ", ",
|
||||||
vm.toString(floorEth), ", ",
|
vm.toString(uint256(floorLiq)), ", ",
|
||||||
vm.toString(floorHarb), ", ",
|
|
||||||
vm.toString(anchorLower), ", ",
|
vm.toString(anchorLower), ", ",
|
||||||
vm.toString(anchorUpper), ", ",
|
vm.toString(anchorUpper), ", ",
|
||||||
vm.toString(anchorEth), ", ",
|
vm.toString(uint256(anchorLiq)), ", ",
|
||||||
vm.toString(anchorHarb), ", ",
|
|
||||||
vm.toString(discoveryLower), ", ",
|
vm.toString(discoveryLower), ", ",
|
||||||
vm.toString(discoveryUpper), ", ",
|
vm.toString(discoveryUpper), ", ",
|
||||||
vm.toString(discoveryEth), ", ",
|
vm.toString(uint256(discoveryLiq)), ", ",
|
||||||
vm.toString(discoveryHarb), ", ",
|
|
||||||
token0isWeth ? "true" : "false"
|
token0isWeth ? "true" : "false"
|
||||||
);
|
);
|
||||||
appendCSVRow(row);
|
appendCSVRow(row);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ library CSVHelper {
|
||||||
*/
|
*/
|
||||||
function createPositionsHeader() internal pure returns (string memory) {
|
function createPositionsHeader() internal pure returns (string memory) {
|
||||||
return
|
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) {
|
function createTimeSeriesHeader() internal pure returns (string memory) {
|
||||||
|
|
|
||||||
|
|
@ -343,36 +343,66 @@ if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
|
||||||
# Use absolute path for the symlink
|
# Use absolute path for the symlink
|
||||||
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
|
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
|
||||||
|
|
||||||
# Start the viewer in background in its own process group
|
# Check if server is already running on common ports
|
||||||
setsid ./view-scenarios.sh &
|
SERVER_RUNNING=false
|
||||||
VIEWER_PID=$!
|
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
|
if [ "$SERVER_RUNNING" = true ]; then
|
||||||
sleep 2
|
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}"
|
||||||
# Show the URL
|
|
||||||
echo ""
|
# Try to open browser to existing server
|
||||||
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
|
if command -v xdg-open &> /dev/null; then
|
||||||
echo "(If port 8000 was busy, check the port number mentioned above)"
|
xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
|
||||||
echo "If browser didn't open, manually navigate to that URL"
|
elif command -v open &> /dev/null; then
|
||||||
|
open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
|
||||||
# Wait for user input
|
fi
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
|
echo ""
|
||||||
read -r
|
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
|
||||||
|
read -r
|
||||||
# Kill the viewer process and its children
|
else
|
||||||
if [ -n "$VIEWER_PID" ]; then
|
# Start the viewer in background in its own process group
|
||||||
# Kill the entire process group (includes python server)
|
setsid ./view-scenarios.sh &
|
||||||
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
|
VIEWER_PID=$!
|
||||||
# Give it a moment to clean up
|
|
||||||
sleep 1
|
# Give the server time to start and browser to open
|
||||||
# Force kill if still running
|
sleep 2
|
||||||
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
|
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
# Clean up the symlink
|
# Clean up the symlink
|
||||||
rm -f "$TEMP_LINK"
|
rm -f "$TEMP_LINK"
|
||||||
|
|
||||||
echo -e "${GREEN}Viewer stopped.${NC}"
|
|
||||||
fi
|
fi
|
||||||
|
|
@ -133,16 +133,17 @@
|
||||||
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
|
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
|
||||||
<br>
|
<br>
|
||||||
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
|
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
|
||||||
• 0.5x = ETH is half as expensive (Floor position holds ETH)<br>
|
• < 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
|
||||||
• 1x = Current ETH price (red dashed line)<br>
|
• 1x = Current ETH price (red dashed line)<br>
|
||||||
• 2x = ETH is twice as expensive (Discovery position holds KRAIKEN)<br>
|
• > 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
|
||||||
<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>
|
||||||
<div id="status">Loading profitable scenario data...</div>
|
<div id="status">Loading profitable scenario data...</div>
|
||||||
<textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea>
|
<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="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button>
|
||||||
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
|
|
||||||
<div id="simulations"></div>
|
<div id="simulations"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -171,6 +172,13 @@
|
||||||
// - Anchor Position: Mixed tokens around current price for shallow liquidity
|
// - Anchor Position: Mixed tokens around current price for shallow liquidity
|
||||||
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
|
// - 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
|
// Auto-load CSV data on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadCSVData();
|
loadCSVData();
|
||||||
|
|
@ -208,25 +216,10 @@
|
||||||
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
|
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
|
||||||
</div>
|
</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) {
|
function parseCSV(csv) {
|
||||||
const lines = csv.trim().split('\n');
|
const lines = csv.trim().split('\n');
|
||||||
|
|
@ -252,37 +245,121 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulateCSVData(data) {
|
function simulateCSVData(data) {
|
||||||
let previousRow = null;
|
// Get selected row from URL parameter
|
||||||
|
const selectedIndex = getRowParameter();
|
||||||
data.forEach((row, index) => {
|
|
||||||
|
// 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 precedingAction = row.precedingAction;
|
||||||
const currentTick = parseFloat(row.currentTick);
|
const currentTick = parseFloat(row.currentTick);
|
||||||
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
|
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
|
||||||
const floorTickLower = parseFloat(row.floorTickLower);
|
const floorTickLower = parseFloat(row.floorTickLower);
|
||||||
const floorTickUpper = parseFloat(row.floorTickUpper);
|
const floorTickUpper = parseFloat(row.floorTickUpper);
|
||||||
// Swap floor values to match expected behavior
|
const floorLiquidity = parseFloat(row.floorLiquidity || 0);
|
||||||
const floorEth = parseFloat(row.floorToken1 || 0) / 1e18;
|
|
||||||
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
|
|
||||||
const anchorTickLower = parseFloat(row.anchorTickLower);
|
const anchorTickLower = parseFloat(row.anchorTickLower);
|
||||||
const anchorTickUpper = parseFloat(row.anchorTickUpper);
|
const anchorTickUpper = parseFloat(row.anchorTickUpper);
|
||||||
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18;
|
const anchorLiquidity = parseFloat(row.anchorLiquidity || 0);
|
||||||
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
|
|
||||||
const discoveryTickLower = parseFloat(row.discoveryTickLower);
|
const discoveryTickLower = parseFloat(row.discoveryTickLower);
|
||||||
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
|
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
|
||||||
// Swap discovery values to match expected behavior
|
const discoveryLiquidity = parseFloat(row.discoveryLiquidity || 0);
|
||||||
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
|
|
||||||
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18;
|
// 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 actionAmount = '';
|
||||||
let additionalInfo = '';
|
let additionalInfo = '';
|
||||||
|
|
||||||
if (previousRow) {
|
if (previousRow) {
|
||||||
const prevFloorEth = parseFloat(previousRow.floorToken1 || 0) / 1e18;
|
// Calculate previous token amounts from liquidity
|
||||||
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || 0) / 1e18;
|
const prevCurrentTick = parseFloat(previousRow.currentTick);
|
||||||
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || 0) / 1e18;
|
const prevFloorLiquidity = parseFloat(previousRow.floorLiquidity || 0);
|
||||||
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || 0) / 1e18;
|
const prevAnchorLiquidity = parseFloat(previousRow.anchorLiquidity || 0);
|
||||||
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || 0) / 1e18;
|
const prevDiscoveryLiquidity = parseFloat(previousRow.discoveryLiquidity || 0);
|
||||||
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || 0) / 1e18;
|
|
||||||
|
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 ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
|
||||||
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
|
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
|
||||||
|
|
@ -302,19 +379,83 @@
|
||||||
const lineNumber = index + 2;
|
const lineNumber = index + 2;
|
||||||
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
|
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
|
||||||
|
|
||||||
|
|
||||||
simulateEnhanced(headline, currentTick,
|
simulateEnhanced(headline, currentTick,
|
||||||
floorTickLower, floorTickUpper, floorEth, floorKraiken,
|
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
|
||||||
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
|
||||||
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth);
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, precedingAction);
|
||||||
previousRow = row;
|
|
||||||
});
|
// 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
|
// Uniswap V3 liquidity calculation functions
|
||||||
function tickToPrice(tick) {
|
function tickToPrice(tick) {
|
||||||
return Math.pow(1.0001, 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) {
|
function priceToSqrtPrice(price) {
|
||||||
return Math.sqrt(price);
|
return Math.sqrt(price);
|
||||||
}
|
}
|
||||||
|
|
@ -372,18 +513,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (positionName) {
|
|
||||||
console.log(`${positionName} liquidity calculation:`, {
|
|
||||||
token0Amount,
|
|
||||||
token1Amount,
|
|
||||||
tickRange: [tickLower, tickUpper],
|
|
||||||
sqrtPriceLower,
|
|
||||||
sqrtPriceUpper,
|
|
||||||
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
|
|
||||||
liquidity,
|
|
||||||
calculatedFrom
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return liquidity;
|
return liquidity;
|
||||||
}
|
}
|
||||||
|
|
@ -438,9 +567,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulateEnhanced(precedingAction, currentTick,
|
function simulateEnhanced(precedingAction, currentTick,
|
||||||
floorTickLower, floorTickUpper, floorEth, floorKraiken,
|
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
|
||||||
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
|
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
|
||||||
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
|
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, originalAction) {
|
||||||
|
|
||||||
// Position data structure with liquidity calculations
|
// Position data structure with liquidity calculations
|
||||||
const positions = {
|
const positions = {
|
||||||
|
|
@ -450,9 +579,7 @@
|
||||||
eth: floorEth,
|
eth: floorEth,
|
||||||
kraiken: floorKraiken,
|
kraiken: floorKraiken,
|
||||||
name: 'Floor',
|
name: 'Floor',
|
||||||
liquidity: token0isWeth ?
|
liquidity: floorLiquidity
|
||||||
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
|
|
||||||
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
|
|
||||||
},
|
},
|
||||||
anchor: {
|
anchor: {
|
||||||
tickLower: anchorTickLower,
|
tickLower: anchorTickLower,
|
||||||
|
|
@ -460,9 +587,7 @@
|
||||||
eth: anchorEth,
|
eth: anchorEth,
|
||||||
kraiken: anchorKraiken,
|
kraiken: anchorKraiken,
|
||||||
name: 'Anchor (Shallow Pool)',
|
name: 'Anchor (Shallow Pool)',
|
||||||
liquidity: token0isWeth ?
|
liquidity: anchorLiquidity
|
||||||
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
|
|
||||||
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
|
|
||||||
},
|
},
|
||||||
discovery: {
|
discovery: {
|
||||||
tickLower: discoveryTickLower,
|
tickLower: discoveryTickLower,
|
||||||
|
|
@ -470,35 +595,10 @@
|
||||||
eth: discoveryEth,
|
eth: discoveryEth,
|
||||||
kraiken: discoveryKraiken,
|
kraiken: discoveryKraiken,
|
||||||
name: 'Discovery',
|
name: 'Discovery',
|
||||||
liquidity: token0isWeth ?
|
liquidity: discoveryLiquidity
|
||||||
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
|
|
||||||
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug logging for all positions
|
|
||||||
console.log('Position liquidity values:', {
|
|
||||||
floor: {
|
|
||||||
liquidity: positions.floor.liquidity,
|
|
||||||
eth: floorEth,
|
|
||||||
kraiken: floorKraiken,
|
|
||||||
range: [floorTickLower, floorTickUpper]
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
liquidity: positions.anchor.liquidity,
|
|
||||||
eth: anchorEth,
|
|
||||||
kraiken: anchorKraiken,
|
|
||||||
range: [anchorTickLower, anchorTickUpper]
|
|
||||||
},
|
|
||||||
discovery: {
|
|
||||||
liquidity: positions.discovery.liquidity,
|
|
||||||
eth: discoveryEth,
|
|
||||||
kraiken: discoveryKraiken,
|
|
||||||
range: [discoveryTickLower, discoveryTickUpper]
|
|
||||||
},
|
|
||||||
currentTick: currentTick,
|
|
||||||
token0isWeth: token0isWeth
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate total active liquidity
|
// Calculate total active liquidity
|
||||||
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
||||||
|
|
@ -535,7 +635,7 @@
|
||||||
scenarioContainer.appendChild(chartsContainer);
|
scenarioContainer.appendChild(chartsContainer);
|
||||||
|
|
||||||
// Create summary panel
|
// Create summary panel
|
||||||
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth);
|
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index);
|
||||||
scenarioContainer.appendChild(summaryPanel);
|
scenarioContainer.appendChild(summaryPanel);
|
||||||
|
|
||||||
// Add to page
|
// Add to page
|
||||||
|
|
@ -565,18 +665,7 @@
|
||||||
...pos
|
...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
|
// 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 xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x
|
||||||
const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max
|
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
|
// Calculate max values for proper y-axis alignment
|
||||||
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
|
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
|
||||||
|
|
@ -692,17 +773,6 @@
|
||||||
return;
|
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
|
// Create a filled area for each position to show its exact range
|
||||||
// Cap display coordinates to keep within visible 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');
|
const panel = document.createElement('div');
|
||||||
panel.className = 'summary-panel';
|
panel.className = 'summary-panel';
|
||||||
|
|
||||||
|
|
@ -1043,16 +1113,17 @@
|
||||||
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0);
|
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 totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
|
||||||
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
|
||||||
|
|
||||||
// Add total summary
|
// Add total summary
|
||||||
const totalItem = document.createElement('div');
|
const totalItem = document.createElement('div');
|
||||||
totalItem.className = 'summary-item';
|
totalItem.className = 'summary-item';
|
||||||
totalItem.innerHTML = `
|
const totalHtml = `
|
||||||
<strong>Total Portfolio</strong><br>
|
<strong>Total Portfolio</strong><br>
|
||||||
Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
||||||
Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
||||||
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
|
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
|
||||||
`;
|
`;
|
||||||
|
totalItem.innerHTML = totalHtml;
|
||||||
grid.appendChild(totalItem);
|
grid.appendChild(totalItem);
|
||||||
|
|
||||||
// Add position summaries
|
// Add position summaries
|
||||||
|
|
@ -1070,8 +1141,8 @@
|
||||||
<strong>${pos.name} Position</strong><br>
|
<strong>${pos.name} Position</strong><br>
|
||||||
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
|
||||||
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
|
||||||
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
|
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
|
||||||
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
|
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
|
||||||
`;
|
`;
|
||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
@ -1079,10 +1150,26 @@
|
||||||
// Add current price info
|
// Add current price info
|
||||||
const priceItem = document.createElement('div');
|
const priceItem = document.createElement('div');
|
||||||
priceItem.className = 'summary-item';
|
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 = `
|
priceItem.innerHTML = `
|
||||||
<strong>Current Price</strong><br>
|
<strong>Current Price</strong><br>
|
||||||
Tick: ${currentTick}<br>
|
Tick: ${currentTick.toLocaleString()}<br>
|
||||||
<small>Price line shown in red</small>
|
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
|
||||||
|
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
|
||||||
`;
|
`;
|
||||||
grid.appendChild(priceItem);
|
grid.appendChild(priceItem);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue