14 Commits

44 changed files with 6675 additions and 773 deletions

95
GEMINI.md Normal file
View File

@ -0,0 +1,95 @@
# GEMINI Project Context: Uniswap Auto CLP & Hedger
**Last Updated:** January 6, 2026
**Project:** Uniswap V3 Automated Concentrated Liquidity (CLP) & Delta-Neutral Hedger
**Operating System:** Windows (win32)
## 1. Project Overview
This project is an automated high-frequency trading system designed to:
1. **Provide Concentrated Liquidity (CLP):** Automatically manages positions on Uniswap V3 and its forks (Aerodrome, PancakeSwap) to capture trading fees.
2. **Hedge Delta Exposure:** Simultaneously executes delta-neutral hedges on Hyperliquid (Perp DEX) to neutralize the price risk of the underlying assets (Impermanent Loss protection).
The system operates as a **Delta-Zero Yield Farmer**, effectively capturing LP fees with reduced market exposure.
## 2. System Architecture
The system consists of two main independent Python processes that coordinate via a shared JSON state file.
```mermaid
graph TD
A[clp_manager.py] -- Reads/Writes --> C{status.json}
B[clp_hedger.py] -- Reads --> C
A -- On-Chain Tx --> D[EVM Chain (Base/Arb)]
B -- API Calls --> E[Hyperliquid Perp]
F[telegram_monitor.py] -- Reads --> C
F -- Alerts --> G[Telegram]
```
### Components
* **`clp_manager.py` (The Strategist):**
* **Role:** Active Liquidity Provision on EVM chains.
* **Actions:** Mints new positions, monitors ranges, auto-closes out-of-range positions, collects fees, and handles auto-wrapping/swapping.
* **State:** Updates `{TARGET_DEX}_status.json` with entry price, token amounts, and range details.
* **`clp_hedger.py` (The Guardian):**
* **Role:** Delta Neutralization on Hyperliquid.
* **Actions:** Calculates the LP's real-time delta. Executes short/long trades to maintain `Net Delta ≈ 0`.
* **Features:**
* **Dynamic Thresholds:** Adjusts rebalance sensitivity based on market volatility (StdDev).
* **Fishing Orders:** Places passive maker orders to capture spread and rebates.
* **Edge Protection:** Reduces hedging logic near range edges to prevent "buying high/selling low".
* **`clp_config.py`:** Centralized configuration. Uses `TARGET_DEX` env var to switch between profiles (e.g., `UNISWAP_V3`, `AERODROME_BASE_CL`).
## 3. Key Files & Directories
| File/Directory | Description |
| :--- | :--- |
| **`clp_manager.py`** | Main entry point for the LP Manager. |
| **`clp_hedger.py`** | Main entry point for the Delta Hedger. |
| **`clp_config.py`** | Configuration profiles and strategy settings. |
| **`requirements.txt`** | Python dependencies (`web3`, `hyperliquid-python-sdk`, `dotenv`). |
| **`doc/`** | Extensive documentation (`CHANGELOG.md`, `UNISWAP_MANAGER_WORKFLOW.md`). |
| **`florida/`** | A secondary deployment or research directory containing specific strategy configurations. |
| **`tools/`** | Utility scripts (e.g., `git_agent.py`, `universal_swapper.py`). |
| **`logs/`** | Runtime logs. |
| **`*_status.json`** | The shared state file (IPC). Specific name depends on `TARGET_DEX`. |
## 4. Setup & Usage
### Prerequisites
* Python 3.10+
* Hyperliquid Account & API Key (in `.env`)
* EVM Wallet Private Key (in `.env`)
* RPC URLs (in `.env`)
### Installation
```bash
pip install -r requirements.txt
```
### Configuration
1. Create a `.env` file (see `.env.example` in `tools/`).
2. Edit `clp_config.py` or set `TARGET_DEX` environment variable to choose a profile (e.g., `AERODROME_WETH-USDC_008`).
### Running the System
You typically run the Manager and Hedger in separate terminals:
**Terminal 1 (Manager):**
```bash
python clp_manager.py
```
**Terminal 2 (Hedger):**
```bash
python clp_hedger.py
```
## 5. Recent Developments (Jan 2026)
* **Low Volatility Optimization:** Increased base rebalance thresholds (from 5% to 8%) to reduce churn in sideways markets.
* **Fishing Orders:** Implemented passive limit orders to profit from rebalancing.
* **Shadow Logging:** The system now logs "Shadow" trades to simulate maker order performance vs taker execution.
* **Dynamic Edge Proximity:** Safety buffers now scale with position size.
## 6. Active Context
* **Current Profile:** The root directory contains `AERODROME_WETH-USDC_008_status.json`, indicating an active or recent session on **Aerodrome (Base Chain)**.
* **Workflows:** Refer to `doc/UNISWAP_MANAGER_WORKFLOW.md` for detailed state machine logic of the Manager.

82
clp_abis.py Normal file
View File

@ -0,0 +1,82 @@
import json
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
UNISWAP_V3_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
UNISWAP_V3_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_NPM_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
AERODROME_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
SWAP_ROUTER_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
WETH9_ABI = json.loads('''
[
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
]
''')

View File

@ -8,7 +8,9 @@ STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
# --- DEFAULT STRATEGY ---
DEFAULT_STRATEGY = {
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
"MONITOR_INTERVAL_SECONDS": 300, # Manager loop & sync interval
"LOG_INTERVAL_SECONDS": 300, # Hedger console logging interval
"RANGE_MODE": "AUTO", # Options: "AUTO" (BB-based), "FIXED" (RANGE_WIDTH_PCT)
"CLOSE_POSITION_ENABLED": True, # Allow the bot to automatically close out-of-range positions
"OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior
@ -19,21 +21,22 @@ DEFAULT_STRATEGY = {
"VALUE_REFERENCE": "USD", # Base currency for all calculations
# Range Settings
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting
"RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.05"), # Max allowed slippage for swaps and minting
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction), "FIXED" (Initial Delta)
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
# Unified Hedger Settings
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
"LEVERAGE": 5, # Leverage to use on Hyperliquid
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range
"ZONE_CLOSE_START": Decimal("10.0"), # Distance (pct) from edge to start closing logic
"ZONE_CLOSE_END": Decimal("11.0"), # Distance (pct) from edge to finish closing logic
"ZONE_TOP_HEDGE_START": Decimal("10.0"), # Distance (pct) from top edge to adjust hedging
"PRICE_BUFFER_PCT": Decimal("0.0015"), # Buffer for limit order pricing (0.15%)
"PRICE_BUFFER_PCT": Decimal("0.0025"), # Buffer for limit order pricing (0.15%)
"MIN_ORDER_VALUE_USD": Decimal("10.0"), # Minimum order size allowed by Hyperliquid
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.2"), # Expansion factor for thresholds
"MIN_TIME_BETWEEN_TRADES": 60, # Cooldown (seconds) between rebalance trades
@ -45,7 +48,7 @@ DEFAULT_STRATEGY = {
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
"ENABLE_EDGE_CLEANUP": True, # Force rebalances when price is at range boundaries
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.02"), # % of range width used for edge detection
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.03"), # % of range width used for edge detection
"MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
@ -72,6 +75,9 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500,
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
"RANGE_WIDTH_PCT": Decimal("0.0075"),
},
"UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
@ -98,6 +104,7 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100,
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.1875"), # 0.1875 only for asymmetric shedge % of range width used for edge detection
"RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
@ -123,6 +130,34 @@ CLP_PROFILES = {
"TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10")
},
"AERODROME_BASE_CL": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 100, # TickSpacing 100 pool (0xb2cc...)
"RANGE_WIDTH_PCT": Decimal("0.075"),
"TARGET_INVESTMENT_AMOUNT": 200,
"HEDGE_STRATEGY": "FIXED",
},
"AERODROME_WETH-USDC_008": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC Stable",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 1, # TickSpacing 1 pool (0xdbc6...)
"RANGE_WIDTH_PCT": Decimal("0.0075"),
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
}
}

View File

@ -14,20 +14,15 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(project_root)
# Import local modules
try:
from logging_utils import setup_logging
except ImportError:
setup_logging = None
# Ensure root logger is clean if we can't use setup_logging
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
# Ensure root logger is clean
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY, TARGET_DEX
# Load environment variables
dotenv_path = os.path.join(current_dir, '.env')
@ -213,7 +208,7 @@ class HyperliquidStrategy:
else: # >=5% range
return Decimal("0.075") # Standard for wide ranges
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict:
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal, strategy_type: str = "ASYMMETRIC") -> Dict:
# Note: current_short_size here is virtual (just for this specific strategy),
# but the unified hedger will use the 'target_short' output primarily.
@ -221,17 +216,33 @@ class HyperliquidStrategy:
# --- ASYMMETRIC COMPENSATION ---
adj_pct = Decimal("0.0")
range_width = self.high_range - self.low_range
if range_width > 0:
dist = current_price - self.entry_price
half_width = range_width / Decimal("2")
norm_dist = dist / half_width
max_boost = self.get_compensation_boost()
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
if strategy_type == "ASYMMETRIC":
range_width = self.high_range - self.low_range
if range_width > 0:
dist = current_price - self.entry_price
half_width = range_width / Decimal("2")
norm_dist = dist / half_width
max_boost = self.get_compensation_boost()
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
# --- FIXED STRATEGY LOGIC ---
if strategy_type == "FIXED":
# Target is exactly the pool delta at entry price
raw_target_short = self.get_pool_delta(self.entry_price)
adj_pct = Decimal("0")
elif strategy_type == "BOTTOM":
if current_price > self.entry_price:
# Disable hedging in upper half
raw_target_short = Decimal("0")
adj_pct = Decimal("0")
else:
# Enable hedging in lower half (standard delta)
# No asymmetric boost applied
adj_pct = Decimal("0")
raw_target_short = pool_delta
adjusted_target_short = raw_target_short * (Decimal("1.0") + adj_pct)
diff = adjusted_target_short - abs(current_short_size)
@ -273,12 +284,23 @@ class UnifiedHedger:
# Market Data Cache
self.last_prices = {}
self.price_history = {} # Symbol -> List[Decimal]
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
self.last_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List)
self.shadow_orders = []
# State: Emergency Close Hysteresis
# Map: (file_path, token_id) -> bool
self.emergency_close_active = {}
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
self.custom_fixed_targets = {}
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
self.hedge_entry_prices = {}
self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -286,6 +308,7 @@ class UnifiedHedger:
def _init_coin_configs(self):
"""Pre-load configuration for known coins from CLP_PROFILES."""
# 1. Load all profiles (order depends on dict iteration)
for profile_key, profile_data in CLP_PROFILES.items():
symbol = profile_data.get("COIN_SYMBOL")
if symbol:
@ -297,6 +320,18 @@ class UnifiedHedger:
# Update with Profile Specifics
self.coin_configs[symbol].update(profile_data)
# 2. Force overwrite with TARGET_DEX profile to ensure precedence
target_profile = CLP_PROFILES.get(TARGET_DEX)
if target_profile:
symbol = target_profile.get("COIN_SYMBOL")
if symbol:
if symbol not in self.coin_configs:
self.coin_configs[symbol] = DEFAULT_STRATEGY.copy()
self.coin_configs[symbol]["sz_decimals"] = 4
logger.info(f"Overwriting config for {symbol} using TARGET_DEX: {TARGET_DEX}")
self.coin_configs[symbol].update(target_profile)
def _get_sz_decimals(self, coin: str) -> int:
try:
meta = self.info.meta()
@ -427,6 +462,7 @@ class UnifiedHedger:
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -478,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN')
}
# Initial hedge entry price is the CLP entry price
self.hedge_entry_prices[key] = entry_price
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
# Ensure JSON has these fields initialized
update_position_stats(key[0], key[1], {
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
})
except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -652,7 +699,7 @@ class UnifiedHedger:
price = to_decimal(mids[coin])
self.last_prices[coin] = price
# Update Price History
# Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
@ -663,19 +710,60 @@ class UnifiedHedger:
if coin not in self.last_prices: continue
price = self.last_prices[coin]
# Get Config & Strategy Type
config = self.coin_configs.get(coin, {})
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
# Calc Logic
calc = strat.calculate_rebalance(price, Decimal("0"))
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates:
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
if status == 'CLOSING':
# If Closing, we want target to be 0 for this strategy
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
is_active_hysteresis = self.emergency_close_active.get(key, False)
if is_active_hysteresis:
# CHECK RESET CONDITION
if strategy_type == "FIXED":
# Reset at 75% of range (from Bottom)
range_width = strat.high_range - strat.low_range
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
else:
reset_threshold = strat.high_range * Decimal("0.999")
if price < reset_threshold:
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
self.emergency_close_active[key] = False
is_active_hysteresis = False
# Capture NEW Dynamic Fixed Target and Entry Price
if strategy_type == "FIXED":
dynamic_delta = strat.get_pool_delta(price)
self.custom_fixed_targets[key] = dynamic_delta
self.hedge_entry_prices[key] = price
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
if not is_active_hysteresis:
# CHECK TRIGGER CONDITION
if price >= strat.high_range:
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
self.emergency_close_active[key] = True
is_active_hysteresis = True
# Reset entry price when closed
self.hedge_entry_prices[key] = Decimal("0")
if status == 'CLOSING' or is_active_hysteresis:
# If Closing OR Hysteresis Active, target is 0
aggregates[coin]['is_closing'] = True
# Do not add to target_short
else:
aggregates[coin]['target_short'] += calc['target_short']
# Use custom fixed target if exists, else standard calc
if strategy_type == "FIXED" and key in self.custom_fixed_targets:
aggregates[coin]['target_short'] += self.custom_fixed_targets[key]
else:
aggregates[coin]['target_short'] += calc['target_short']
aggregates[coin]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct']
@ -693,6 +781,8 @@ class UnifiedHedger:
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True
if dist_bottom_pct < safety_margin_pct:
aggregates[coin]['is_at_bottom_edge'] = True
# Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots)
@ -706,255 +796,256 @@ class UnifiedHedger:
for coin in coins_to_process:
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
price = self.last_prices.get(coin, Decimal("0")) # FIX: Explicitly get price for this coin
price = self.last_prices.get(coin, Decimal("0"))
if price == 0: continue
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short)
target_position = -target_short_abs # We want to be Short, so negative size
target_short_abs = data['target_short']
target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
diff = target_position - current_pos
diff_abs = abs(diff)
# Thresholds
config = self.coin_configs.get(coin, {})
min_thresh = config.get("min_threshold", Decimal("0.008"))
# Volatility Multiplier
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005")
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
# FORCE EDGE CLEANUP
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
if data['is_at_edge'] and enable_edge_cleanup:
if dynamic_thresh > min_thresh:
# logger.info(f"[EDGE] {coin} forced to min threshold.")
dynamic_thresh = min_thresh
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
# Check Trigger
action_needed = diff_abs > dynamic_thresh
# Determine Intent (Moved UP for Order Logic)
is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders
existing_orders = orders_map.get(coin, [])
force_taker_retry = False
# Fishing Config
enable_fishing = config.get("ENABLE_FISHING", False)
fishing_timeout = config.get("FISHING_TIMEOUT_FALLBACK", 30)
# Check Existing Orders for compatibility
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side'] # 'B' or 'A'
o_side = o['side']
o_timestamp = o.get('timestamp', int(time.time()*1000))
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
# Price Check (within buffer)
dist_pct = abs(price - o_price) / price
# Maker Timeout Check (General)
maker_timeout = config.get("MAKER_ORDER_TIMEOUT", 300)
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > maker_timeout:
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.")
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid)
continue
# Fishing Timeout Check
if enable_fishing and is_same_side and order_age_sec > fishing_timeout:
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
self.cancel_order(coin, o_oid)
force_taker_retry = True
continue # Do not mark matched, let it flow to execution
continue
if is_same_side and dist_pct < price_buffer_pct:
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
order_matched = True
if int(time.time()) % 10 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%) | Age: {order_age_sec:.1f}s")
if int(time.time()) % 15 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
# --- EXECUTION LOGIC ---
if not order_matched:
if action_needed or force_taker_retry:
# Determine Urgency / Bypass Cooldown
bypass_cooldown = False
force_maker = False
if not order_matched and (action_needed or force_taker_retry):
if force_taker_retry: bypass_cooldown = True
elif data.get('is_closing', False): bypass_cooldown = True
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5: force_maker = True
else: continue # Skip startup ghost positions
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
# Prevent IOC for BUYs at bottom edge
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
bypass_cooldown = True
# --- BOTTOM STRATEGY SAFEGUARDS ---
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
if strategy_type == "BOTTOM":
# strict: "do not use taker orders... except only on very bottom"
if not data.get('is_at_bottom_edge', False):
bypass_cooldown = False
force_maker = False
force_taker_retry = False # Disable taker retry from fishing
# 0. Forced Taker Retry (Fishing Timeout)
if force_taker_retry:
bypass_cooldown = True
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker")
# --- ASYMMETRIC HEDGE CHECK ---
is_asymmetric_blocked = False
p_mid_asym = Decimal("0")
# strategy_type already fetched above
# 1. Urgent Closing -> Taker
elif data.get('is_closing', False):
bypass_cooldown = True
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit")
if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
total_L_asym = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
# 2. Ghost/Cleanup -> Maker
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5:
force_maker = True
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce")
else:
logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)")
continue # Skip execution for this coin
# Large Hedge Check (Only Force Taker if AT EDGE)
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
bypass_cooldown = True
logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
# Large hedge but safe zone -> Maker is fine, but maybe log it
logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.")
gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
if gamma_asym > 0:
p_mid_asym = price - (diff_abs / gamma_asym)
if not data.get('is_at_edge', False) and price >= p_mid_asym:
is_asymmetric_blocked = True
# --- EXECUTION ---
if not order_matched and not is_asymmetric_blocked:
if action_needed or force_taker_retry:
last_trade = self.last_trade_times.get(coin, 0)
min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60)
min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60)
can_trade = False
if bypass_cooldown:
can_trade = True
elif time.time() - last_trade > min_time_trade:
can_trade = True
if can_trade:
# Get Orderbook for Price
if coin not in l2_snapshots:
l2_snapshots[coin] = self.info.l2_snapshot(coin)
if bypass_cooldown or (time.time() - last_trade > min_time):
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
levels = l2_snapshots[coin]['levels']
if not levels[0] or not levels[1]: continue
if levels[0] and levels[1]:
bid, ask = to_decimal(levels[0][0]['px']), to_decimal(levels[1][0]['px'])
if bypass_cooldown and not force_maker:
exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999")
order_type = "Ioc"
else:
exec_price = bid if is_buy_bool else ask
order_type = "Alo"
bid = to_decimal(levels[0][0]['px'])
ask = to_decimal(levels[1][0]['px'])
logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
if order_type == "Ioc":
shadow_price = bid if is_buy_bool else ask
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
# Price logic
create_shadow = False
logger.info("Sleeping 10s for position update...")
time.sleep(10)
self._update_closed_pnl(coin)
else:
# Idle Cleanup
if existing_orders and not order_matched:
for o in existing_orders: self.cancel_order(coin, o['oid'])
# Decide Order Type: Taker (Ioc) or Maker (Alo)
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo)
# --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
total_L_log = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_log += strat_inst.L
# Logic:
# If Force Maker -> Alo
# Else if Urgent -> Ioc
# Else if Enable Fishing -> Alo
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
# Update all active strategies for this coin in JSON
if total_L_log > 0 and price > 0:
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
# Let's clarify:
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo.
# New logic:
# If bypass_cooldown -> Ioc
# Else -> Alo (Fishing)
# CLP Value Calc
def get_clp_value(p, s):
if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
if bypass_cooldown and not force_maker:
exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999")
order_type = "Ioc"
create_shadow = True
else:
# Fishing / Standard Maker
exec_price = bid if is_buy_bool else ask
order_type = "Alo"
clp_curr_val = get_clp_value(price, strat_inst)
logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f} | Type: {order_type}")
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
# Shadow Order
if create_shadow:
shadow_price = bid if is_buy_bool else ask
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
self.shadow_orders.append({
'coin': coin,
'side': side_str,
'price': shadow_price,
'expires_at': time.time() + shadow_timeout
})
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
# UPDATED: Sleep for API Lag (Phase 5.1)
logger.info("Sleeping 10s to allow position update...")
time.sleep(10)
# --- UPDATE CLOSED PnL FROM API ---
self._update_closed_pnl(coin)
# USE TRACKED HEDGE ENTRY PRICE
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
else:
# Cooldown log
pass
hedge_pnl_curr = Decimal("0")
else:
# Action NOT needed
# Cleanup any dangling orders
if existing_orders:
for o in existing_orders:
logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})")
self.cancel_order(coin, o['oid'])
fee_close_curr = (target_size * price) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# --- IDLE LOGGING (Restored Format) ---
# Calculate aggregate Gamma to estimate triggers
# Gamma = 0.5 * Sum(L) * P^-1.5
# We need Sum(L) for this coin.
total_L = Decimal("0")
# We need to re-iterate or cache L.
# Simpler: Just re-sum L from active strats for this coin.
for key, strat in self.strategies.items():
if self.strategy_states[key]['coin'] == coin:
total_L += strat.L
# Retrieve Realized PnL & Fees from State
realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
if total_L > 0 and price > 0:
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5")))
if gamma > 0:
# Equilibrium Price (Diff = 0)
p_mid = price + (diff / gamma)
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
# Triggers
p_buy = price + (dynamic_thresh + diff) / gamma
p_sell = price - (dynamic_thresh - diff) / gamma
cur_hl_cost = realized_fees + fee_close_curr
if int(time.time()) % 30 == 0:
# Sync to JSON every 1s
update_position_stats(k_strat[0], k_strat[1], {
"combined_TotPnL": round(float(tot_curr), 2),
"hedge_HL_cost_est": round(float(cur_hl_cost), 2),
"hedge_pnl_unrealized": round(float(hedge_pnl_curr), 2),
"last_sync_hl": int(time.time())
})
# --- THROTTLED STATUS LOGGING (300s) ---
now = time.time()
last_log = self.last_idle_log_times.get(coin, 0)
log_interval = config.get("LOG_INTERVAL_SECONDS", 300)
if now - last_log >= log_interval:
self.last_idle_log_times[coin] = now
if is_asymmetric_blocked:
logger.info(f"[ASYMMETRIC] Blocking BUY. Px ({price:.2f}) >= Eq ({p_mid_asym:.2f}) & Not Edge")
if total_L_log > 0 and price > 0:
gamma_log = (Decimal("0.5") * total_L_log * (price ** Decimal("-1.5")))
if gamma_log > 0:
p_mid_log = price - (diff / gamma_log)
p_buy = price + (dynamic_thresh + diff) / gamma_log
p_sell = price - (dynamic_thresh - diff) / gamma_log
pad = " " if coin == "BNB" else ""
adj_val = data.get('adj_pct', Decimal("0")) * 100
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl_total = Decimal("0")
fees_total = Decimal("0")
for k, s_state in self.strategy_states.items():
if s_state['coin'] == coin:
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
total_pnl = (closed_pnl - fees) + unrealized
total_pnl = (closed_pnl_total - fees_total) + unrealized
# Log individual strategy PnL
if strategy_type == "FIXED":
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
pnl_pad = " " if unrealized >= 0 else ""
tot_pnl_pad = " " if total_pnl >= 0 else ""
# Recalculate for logging (including bounds)
clp_curr_val = get_clp_value(price, strat_inst)
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {adj_val:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f}{pnl_pad} | TotPnL: {total_pnl:.2f}{tot_pnl_pad}")
else:
if int(time.time()) % 30 == 0:
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
hedge_pnl_low = (h_entry_px - strat_inst.low_range) * target_size
hedge_pnl_high = (h_entry_px - strat_inst.high_range) * target_size
fee_open = (target_size * h_entry_px) * Decimal("0.000144")
else:
hedge_pnl_curr = hedge_pnl_low = hedge_pnl_high = Decimal("0")
fee_open = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
fee_close_low = (target_size * strat_inst.low_range) * Decimal("0.000432")
fee_close_high = (target_size * strat_inst.high_range) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr - (fee_open + fee_close_curr) + uni_fees
tot_low = (clp_low_val - strat_inst.target_value) + hedge_pnl_low - (fee_open + fee_close_low) + uni_fees
tot_high = (clp_high_val - strat_inst.target_value) + hedge_pnl_high - (fee_open + fee_close_high) + uni_fees
cur_hl_cost = fee_open + fee_close_curr
# ID or Range to distinguish
strat_id = str(k_strat[1]) # Token ID
logger.info(f"[FIXED] {coin} #{strat_id} | TotPnL: {tot_curr:+.2f} | Down: {tot_low:+.2f} | Up: {tot_high:+.2f} (Inc: Fees ${uni_fees:.2f}, HL Cost ${cur_hl_cost:.2f})")
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid_log:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {data.get('adj_pct',0)*100:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f} | HedgePnL: {total_pnl:.2f}")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
if int(time.time()) % 30 == 0:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))

View File

@ -63,65 +63,34 @@ formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# --- ABIs ---
# (Kept minimal for brevity, normally would load from files)
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
UNISWAP_V3_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
UNISWAP_V3_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
SWAP_ROUTER_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
WETH9_ABI = json.loads('''
[
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
]
''')
from clp_abis import (
NONFUNGIBLE_POSITION_MANAGER_ABI,
UNISWAP_V3_POOL_ABI,
ERC20_ABI,
UNISWAP_V3_FACTORY_ABI,
AERODROME_FACTORY_ABI,
AERODROME_POOL_ABI,
AERODROME_NPM_ABI,
SWAP_ROUTER_ABI,
WETH9_ABI
)
from clp_config import get_current_config, STATUS_FILE
from tools.universal_swapper import execute_swap
# --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_config()
DEX_TO_CHAIN = {
"UNISWAP_V3": "ARBITRUM",
"UNISWAP_wide": "ARBITRUM",
"PANCAKESWAP_BNB": "BSC",
"WETH_CBBTC_BASE": "BASE",
"UNISWAP_BASE_CL": "BASE",
"AERODROME_BASE_CL": "BASE",
"AERODROME_WETH-USDC_008": "BASE"
}
# --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
@ -130,11 +99,77 @@ REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", Tr
TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01"))
RANGE_MODE = CONFIG.get("RANGE_MODE", "FIXED")
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
# --- AUTO RANGE HELPERS ---
def get_market_indicators() -> Optional[Dict]:
file_path = os.path.join("market_data", "indicators.json")
if not os.path.exists(file_path):
return None
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Check Freshness (5m)
last_updated_str = data.get("last_updated")
if not last_updated_str: return None
last_updated = datetime.fromisoformat(last_updated_str)
if (datetime.now() - last_updated).total_seconds() > 300:
logger.warning("⚠️ Market indicators file is stale (>5m).")
return None
return data
except Exception as e:
logger.error(f"Error reading indicators: {e}")
return None
def calculate_dynamic_range_pct(coin: str) -> Optional[Decimal]:
indicators = get_market_indicators()
if not indicators: return None
# Normalize symbols (Hyperliquid uses ETH, BNB while DEX uses WETH, WBNB)
symbol_map = {"WETH": "ETH", "WBNB": "BNB"}
lookup_coin = symbol_map.get(coin.upper(), coin.upper())
coin_data = indicators.get("data", {}).get(lookup_coin)
if not coin_data: return None
try:
price = Decimal(str(coin_data["current_price"]))
bb12 = coin_data["bb"]["12h"]
bb_low = Decimal(str(bb12["lower"]))
bb_high = Decimal(str(bb12["upper"]))
ma88 = Decimal(str(coin_data["ma"]["88"]))
# Condition 2: Price inside BB 12h
if not (bb_low <= price <= bb_high):
logger.warning(f"⚖️ AUTO: Price {price:.2f} is outside BB 12h ({bb_low:.2f} - {bb_high:.2f}). Skipping AUTO.")
return None
# Condition 3: MA 88 inside BB 12h
if not (bb_low <= ma88 <= bb_high):
logger.warning(f"⚖️ AUTO: MA 88 {ma88:.2f} is outside BB 12h. Skipping AUTO.")
return None
# Calculation: Max distance to BB edge
dist_low = abs(price - bb_low)
dist_high = abs(price - bb_high)
max_dist = max(dist_low, dist_high)
range_pct = max_dist / price
return range_pct
except (KeyError, TypeError, ValueError) as e:
logger.error(f"Error in dynamic range calc: {e}")
return None
# --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
# Router address not strictly needed for Manager if using universal_swapper, but kept for ref
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
@ -311,7 +346,8 @@ def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int
if pool_address == '0x0000000000000000000000000000000000000000':
return None, None
pool_contract = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
pool_contract = w3.eth.contract(address=pool_address, abi=pool_abi)
return {
"token0_address": token0_address, "token1_address": token1_address,
@ -397,6 +433,7 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount, token0: str, token1: str, amount0_needed: int, amount1_needed: int, sqrt_price_x96: int, d0: int, d1: int) -> bool:
"""
Checks balances, wraps ETH if needed, and swaps ONLY the required surplus to meet deposit requirements.
Uses universal_swapper for the swap execution.
"""
token0 = clean_address(token0)
token1 = clean_address(token1)
@ -444,12 +481,12 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
# Price of Token0 in terms of Token1
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
swap_call = None
token_in, token_out = None, None
amount_in = 0
chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
buffer_multiplier = Decimal("1.02") # 2% buffer for slippage/price moves
token_in_sym, token_out_sym = None, None
amount_in_float = 0.0
buffer_multiplier = Decimal("1.03")
if deficit0 > 0 and bal1 > amount1_needed:
# Need T0 (ETH), Have extra T1 (USDC)
# Swap T1 -> T0
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus1 = bal1 - amount1_needed
if surplus1 >= amount_in_needed:
token_in, token_out = token1, token0
amount_in = amount_in_needed
# Get Symbols
token_in_sym = token1_c.functions.symbol().call().upper()
token_out_sym = token0_c.functions.symbol().call().upper()
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d1))
logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
else:
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}")
@ -479,38 +519,46 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus0 = bal0 - amount0_needed
if surplus0 >= amount_in_needed:
token_in, token_out = token0, token1
amount_in = amount_in_needed
token_in_sym = token0_c.functions.symbol().call().upper()
token_out_sym = token1_c.functions.symbol().call().upper()
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d0))
logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
else:
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
if token_in and amount_in > 0:
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...")
if token_in_sym and amount_in_float > 0:
logger.info(f"🔄 Delegating Swap to Universal Swapper: {amount_in_float} {token_in_sym} -> {token_out_sym} on {chain_name}...")
try:
# Use Standard Fee (500) if configured fee is weird (like 1 for Aerodrome tickSpacing)
# This ensures the standard router finds a valid pool (WETH/USDC 0.05%)
swap_fee = POOL_FEE if POOL_FEE >= 100 else 500
if not ensure_allowance(w3, account, token_in, UNISWAP_V3_SWAP_ROUTER_ADDRESS, amount_in):
# Call Universal Swapper
execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_float, fee_tier=swap_fee)
# Wait for node indexing
logger.info("⏳ Waiting for balance update...")
time.sleep(2)
# Retry check loop
for i in range(3):
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
if bal0 >= amount0_needed and bal1 >= amount1_needed:
logger.info("✅ Balances sufficient.")
return True
if i < 2:
logger.info(f"⏳ Balance not updated yet, retrying ({i+1}/3)...")
time.sleep(2)
logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
params = (
token_in, token_out, POOL_FEE, account.address,
int(time.time()) + 120,
amount_in,
0, # amountOutMin (Market swap for rebalance)
0
)
receipt = send_transaction_robust(w3, account, router_contract.functions.exactInputSingle(params), extra_msg="Swap Surplus")
if receipt:
# Final check - Recursive check to ensure we hit target or retry
# But return True/False based on immediate check
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
# If we are strictly >= needed, great.
if bal0 >= amount0_needed and bal1 >= amount1_needed:
return True
else:
logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
except Exception as e:
logger.error(f"❌ Universal Swap Failed: {e}")
return False
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
@ -531,14 +579,20 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
# 3. Mint
params = (
base_params = [
token0, token1, POOL_FEE,
tick_lower, tick_upper,
amount0, amount1,
amount0_min, amount1_min,
account.address,
int(time.time()) + 180
)
]
# Aerodrome Slipstream expects sqrtPriceX96 as the last parameter
if "AERODROME" in os.environ.get("TARGET_DEX", "").upper():
base_params.append(0) # sqrtPriceX96
params = tuple(base_params)
receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position")
@ -696,9 +750,43 @@ def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}")
import argparse
import requests
# --- REAL-TIME ORACLE HELPER ---
def get_realtime_price(coin: str) -> Optional[Decimal]:
"""Fetches current mid-price directly from Hyperliquid API (low latency)."""
try:
url = "https://api.hyperliquid.xyz/info"
response = requests.post(url, json={"type": "allMids"}, timeout=2)
if response.status_code == 200:
data = response.json()
# Hyperliquid symbols are usually clean (ETH, BNB)
# Map common variations just in case
target = coin.upper().replace("WETH", "ETH").replace("WBNB", "BNB")
if target in data:
return Decimal(data[target])
except Exception as e:
logger.warning(f"⚠️ Failed to fetch realtime Oracle price: {e}")
return None
# --- MAIN LOOP ---
def main():
# --- ARGUMENT PARSING ---
parser = argparse.ArgumentParser(description="Uniswap CLP Manager")
parser.add_argument("--force", type=float, help="Force open a position with specific range width (e.g., 0.75), ignoring AUTO safe checks.")
args = parser.parse_args()
force_mode_active = False
force_width_pct = Decimal("0")
if args.force:
force_mode_active = True
force_width_pct = Decimal(str(args.force)) / 100 # Convert 0.75 -> 0.0075
logger.warning(f"🚨 FORCE MODE ACTIVE: Will bypass safe checks for FIRST position with width {args.force}%")
logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
load_dotenv(override=True)
@ -722,9 +810,23 @@ def main():
logger.info(f"👤 Wallet: {account.address}")
# Contracts
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
target_dex_name = os.environ.get("TARGET_DEX", "").upper()
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
logger.info("✈️ Using Aerodrome NPM ABI")
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=AERODROME_NPM_ABI)
else:
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
factory_addr = npm.functions.factory().call()
factory = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
# Select Factory ABI based on DEX type
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
logger.info("✈️ Using Aerodrome Factory ABI (tickSpacing instead of fee)")
factory_abi = AERODROME_FACTORY_ABI
else:
factory_abi = UNISWAP_V3_FACTORY_ABI
factory = w3.eth.contract(address=factory_addr, abi=factory_abi)
router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
while True:
@ -817,7 +919,21 @@ def main():
pnl_unrealized = current_pos_value_usd - initial_value
total_pnl_usd = pnl_unrealized + total_fees_usd
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})"
# --- PERSIST PERFORMANCE TO JSON ---
update_position_status(token_id, "OPEN", {
"clp_fees": round(float(total_fees_usd), 2),
"clp_TotPnL": round(float(total_pnl_usd), 2)
})
# Calculate Fees/h
fees_per_h_str = "0.00"
ts_open = active_auto_pos.get('timestamp_open')
if ts_open:
hours_open = (time.time() - ts_open) / 3600
if hours_open > 0.01:
fees_per_h_str = f"{float(total_fees_usd) / hours_open:.2f}"
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f} | ${fees_per_h_str}/h)"
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
# --- KPI LOGGING ---
@ -845,6 +961,32 @@ def main():
log_kpi_snapshot(snapshot)
# --- REPOSITION LOGIC ---
pos_range_mode = active_auto_pos.get("range_mode", RANGE_MODE)
if pos_range_mode == "AUTO" and CLOSE_POSITION_ENABLED:
coin_for_dynamic = pos_details['token0_symbol'] if not is_t0_stable else pos_details['token1_symbol']
new_range_width = calculate_dynamic_range_pct(coin_for_dynamic)
if new_range_width:
# Use initial width from JSON, or current config width as fallback
old_range_width = Decimal(str(active_auto_pos.get("range_width_initial", RANGE_WIDTH_PCT)))
# Condition A: Difference > 20%
width_diff_pct = abs(new_range_width - old_range_width) / old_range_width
# Condition B: Profit > 0.1%
profit_pct = total_pnl_usd / initial_value
logger.info(f"📊 AUTO Check: CurRange {old_range_width*100:.2f}%, NewRange {new_range_width*100:.2f}% | Diff {width_diff_pct*100:.1f}% | Profit {profit_pct*100:.2f}%")
if width_diff_pct > 0.20 and profit_pct > 0.001:
logger.warning(f"🔄 REPOSITION TRIGGERED: Width Diff {width_diff_pct*100:.1f}%, Profit {profit_pct*100:.2f}%")
# Set in_range to False to force the closing logic below
in_range = False
else:
logger.warning(f"⚖️ AUTO Check Skipped: Market indicators for {coin_for_dynamic} are stale or conditions not met.")
if not in_range and CLOSE_POSITION_ENABLED:
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
update_position_status(token_id, "CLOSING")
@ -875,14 +1017,73 @@ def main():
fee = POOL_FEE
pool_addr = factory.functions.getPool(token0, token1, fee).call()
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
pool_c = w3.eth.contract(address=pool_addr, abi=pool_abi)
pool_data = get_pool_dynamic_data(pool_c)
if pool_data:
tick = pool_data['tick']
# Define Range (+/- 2.5%)
# log(1.025) / log(1.0001) approx 247 tick delta
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001))
# --- PRE-CALCULATE ESSENTIALS ---
# Fetch Decimals & Symbols immediately (Required for Oracle Check)
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
d0 = t0_c.functions.decimals().call()
d1 = t1_c.functions.decimals().call()
t0_sym = t0_c.functions.symbol().call().upper()
t1_sym = t1_c.functions.symbol().call().upper()
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
is_t1_stable = any(s in t1_sym for s in stable_symbols)
is_t0_stable = any(s in t0_sym for s in stable_symbols)
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
# Define coin_sym early for Guard Rails
coin_sym = CONFIG.get("COIN_SYMBOL", "ETH")
# --- ORACLE GUARD RAIL ---
# Protect against Pool/Oracle divergence (Manipulation/Depeg/Lag)
if not force_mode_active:
oracle_price = get_realtime_price(coin_sym)
if oracle_price:
pool_price_dec = price_0_in_1 if is_t1_stable else (Decimal("1") / price_0_in_1)
divergence = abs(pool_price_dec - oracle_price) / oracle_price
if divergence > Decimal("0.0025"): # 0.25% Tolerance
logger.warning(f"⚠️ Price Divergence! Pool: {pool_price_dec:.2f} vs Oracle: {oracle_price:.2f} (Diff: {divergence*100:.2f}%). Aborting.")
time.sleep(10)
continue
else:
logger.warning("⚠️ Could not fetch Oracle price. Proceeding with caution (or consider aborting).")
# --- DYNAMIC RANGE CALCULATION ---
active_range_width = RANGE_WIDTH_PCT
current_range_mode = RANGE_MODE
# 1. PRIORITY: Force Mode
if force_mode_active:
logger.warning(f"🚨 FORCE OVERRIDE: Using forced width {force_width_pct*100:.2f}% (Ignoring safe checks)")
active_range_width = force_width_pct
current_range_mode = "FIXED"
# 2. AUTO Mode (Only if not forced)
elif RANGE_MODE == "AUTO":
dynamic_width = calculate_dynamic_range_pct(coin_sym)
if dynamic_width:
active_range_width = dynamic_width
logger.info(f"⚖️ AUTO Range Activated: {active_range_width*100:.4f}%")
else:
logger.info(f"⛔ AUTO conditions not met. Waiting for safe entry...")
time.sleep(MONITOR_INTERVAL_SECONDS)
continue # Skip logic
# 3. FIXED Mode (Default Fallback) is already set by initial active_range_width
# Define Range
tick_delta = int(math.log(1 + float(active_range_width)) / math.log(1.0001))
# Fetch actual tick spacing from pool
tick_spacing = pool_c.functions.tickSpacing().call()
@ -893,28 +1094,10 @@ def main():
# Calculate Amounts
# Target Value logic
d0 = 18 # Default WETH (Corrected below if needed, but we rely on raw logic)
# Actually, we should fetch decimals from contract to be safe, but config assumes standard.
# Fetch Decimals for precision
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
d0 = t0_c.functions.decimals().call()
d1 = t1_c.functions.decimals().call()
# Determine Investment Value in Token1 terms
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC))
# Check which is stable
t0_sym = t0_c.functions.symbol().call().upper()
t1_sym = t1_c.functions.symbol().call().upper()
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
is_t1_stable = any(s in t1_sym for s in stable_symbols)
is_t0_stable = any(s in t0_sym for s in stable_symbols)
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
investment_val_token1 = Decimal("0")
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX":
@ -939,10 +1122,33 @@ def main():
amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_token1, d0, d1, pool_data['sqrtPriceX96'])
if check_and_swap_for_deposit(w3, router, account, token0, token1, amt0, amt1, pool_data['sqrtPriceX96'], d0, d1):
# --- STALE DATA PROTECTION (Pre-Mint) ---
# Check if price moved significantly during calculation/swap
pre_mint_data = get_pool_dynamic_data(pool_c)
if pre_mint_data:
tick_diff = abs(pre_mint_data['tick'] - pool_data['tick'])
# 13 ticks ~ 0.13% price move. Abort if volatile.
if tick_diff > 13:
logger.warning(f"⚠️ Price moved too much ({tick_diff} ticks) during setup/swap. Aborting mint to prevent bad entry.")
time.sleep(5)
continue
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
if minted:
# Calculate entry price and amounts for JSON compatibility
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
# --- DISABLE FORCE MODE AFTER FIRST MINT ---
if force_mode_active:
logger.info("🛑 FORCE MODE CONSUMED: Returning to standard AUTO checks for future positions.")
force_mode_active = False
# --- RE-FETCH PRICE FOR ACCURATE ENTRY DATA (Post-Mint) ---
fresh_pool_data = get_pool_dynamic_data(pool_c)
if fresh_pool_data:
fresh_tick = fresh_pool_data['tick']
price_0_in_1 = price_from_tick(fresh_tick, d0, d1)
logger.info(f"🔄 Refreshed Entry Tick: {fresh_tick} (Was: {pool_data['tick']})")
else:
price_0_in_1 = price_from_tick(pool_data['tick'], d0, d1)
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
@ -970,6 +1176,8 @@ def main():
"range_lower": round(r_lower, 4),
"token0_decimals": d0,
"token1_decimals": d1,
"range_mode": current_range_mode,
"range_width_initial": float(active_range_width),
"timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
}

View File

@ -0,0 +1,104 @@
# CLP Hedging Strategy Matrix (Jan 2026)
*Based on ETH/USDC 0.05% Arbitrum Pool Stats: TVL $74.53M, 24h Vol $23.27M.*
*Calculations include 0.045% round-trip Hyperliquid fees and concentration-adjusted Uniswap fees.*
This document outlines the projected PnL, required hedge sizes, and estimated earnings for different CLP configurations, assuming a fixed hedge position is maintained from entry to exit.
## Technical Assumptions & Methodology
1. **Transaction Costs:** Calculations include Hyperliquid fees (~0.045% round-trip: 0.01% Maker Entry + 0.035% Taker Exit).
2. **Uniswap Fees:** Est. Fees (1h) are calculated using the pool's volume/TVL ratio and a concentration multiplier based on the range width.
3. **Static Hedge:** Assumes the hedge position is opened at the exact same time as the CLP and is **never rebalanced** until the CLP is closed at the range boundary.
4. **Delta Calculation:**
* **Lower Edge:** Position becomes 100% ETH (Max Long Delta).
* **Upper Edge:** Position becomes 100% USDC (Zero Delta).
---
## Capital: $1000 USDC
| Range | Strategy | Hedge Size | Margin (5x) | PnL Lower | PnL Upper | Est. Fees (1h) |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| +/- 0.5 % | Neutral Down | 0.2746 ETH | $167.94 | +0.00 | -3.40 | $1.30 |
| | Neutral Up | 0.0520 ETH | $31.81 | -3.40 | +0.00 | |
| | Fixed | 0.1631 ETH | $99.75 | -1.70 | -1.70 | |
| +/- 1.0 % | Neutral Down | 0.2598 ETH | $158.88 | -0.00 | -5.91 | $0.65 |
| | Neutral Up | 0.0664 ETH | $40.63 | -5.91 | +0.00 | |
| | Fixed | 0.1627 ETH | $99.50 | -2.97 | -2.94 | |
| +/- 2.0 % | Neutral Down | 0.2522 ETH | $154.26 | +0.00 | -10.95 | $0.33 |
| | Neutral Up | 0.0732 ETH | $44.76 | -10.95 | +0.00 | |
| | Fixed | 0.1619 ETH | $99.00 | -5.53 | -5.42 | |
| +/- 4.0 % | Neutral Down | 0.2482 ETH | $151.78 | +0.00 | -21.10 | $0.17 |
| | Neutral Up | 0.0757 ETH | $46.28 | -21.10 | +0.00 | |
| | Fixed | 0.1603 ETH | $98.02 | -10.75 | -10.35 | |
| +/- 5.0 % | Neutral Down | 0.2473 ETH | $151.22 | +0.00 | -26.21 | $0.13 |
| | Neutral Up | 0.0758 ETH | $46.37 | -26.21 | +0.00 | |
| | Fixed | 0.1595 ETH | $97.53 | -13.42 | -12.79 | |
| +/- 10.0 % | Neutral Down | 0.2450 ETH | $149.84 | +0.00 | -52.16 | $0.07 |
| | Neutral Up | 0.0744 ETH | $45.52 | -52.16 | +0.00 | |
| | Fixed | 0.1555 ETH | $95.12 | -27.36 | -24.80 | |
## Capital: $2000 USDC
| Range | Strategy | Hedge Size | Margin (5x) | PnL Lower | PnL Upper | Est. Fees (1h) |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| +/- 0.5 % | Neutral Down | 0.5492 ETH | $335.88 | +0.00 | -6.81 | $2.61 |
| | Neutral Up | 0.1040 ETH | $63.63 | -6.81 | -0.00 | |
| | Fixed | 0.3262 ETH | $199.50 | -3.41 | -3.40 | |
| +/- 1.0 % | Neutral Down | 0.5196 ETH | $317.75 | +0.00 | -11.83 | $1.31 |
| | Neutral Up | 0.1329 ETH | $81.25 | -11.83 | -0.00 | |
| | Fixed | 0.3254 ETH | $199.00 | -5.94 | -5.89 | |
| +/- 2.0 % | Neutral Down | 0.5045 ETH | $308.52 | +0.00 | -21.90 | $0.66 |
| | Neutral Up | 0.1464 ETH | $89.51 | -21.90 | -0.00 | |
| | Fixed | 0.3238 ETH | $198.01 | -11.05 | -10.85 | |
| +/- 4.0 % | Neutral Down | 0.4964 ETH | $303.56 | +0.00 | -42.20 | $0.33 |
| | Neutral Up | 0.1514 ETH | $92.56 | -42.20 | +0.00 | |
| | Fixed | 0.3206 ETH | $196.04 | -21.50 | -20.70 | |
| +/- 5.0 % | Neutral Down | 0.4946 ETH | $302.44 | +0.00 | -52.43 | $0.27 |
| | Neutral Up | 0.1517 ETH | $92.74 | -52.43 | +0.00 | |
| | Fixed | 0.3190 ETH | $195.06 | -26.85 | -25.58 | |
| +/- 10.0 % | Neutral Down | 0.4900 ETH | $299.68 | +0.00 | -104.31 | $0.14 |
| | Neutral Up | 0.1489 ETH | $91.05 | -104.31 | +0.00 | |
| | Fixed | 0.3111 ETH | $190.23 | -54.72 | -49.59 | |
## Capital: $4000 USDC
| Range | Strategy | Hedge Size | Margin (5x) | PnL Lower | PnL Upper | Est. Fees (1h) |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| +/- 0.5 % | Neutral Down | 1.0985 ETH | $671.75 | -0.00 | -13.61 | $5.22 |
| | Neutral Up | 0.2081 ETH | $127.25 | -13.61 | +0.00 | |
| | Fixed | 0.6525 ETH | $399.00 | -6.82 | -6.79 | |
| +/- 1.0 % | Neutral Down | 1.0392 ETH | $635.51 | -0.00 | -23.65 | $2.61 |
| | Neutral Up | 0.2657 ETH | $162.51 | -23.65 | +0.00 | |
| | Fixed | 0.6508 ETH | $398.00 | -11.88 | -11.77 | |
| +/- 2.0 % | Neutral Down | 1.0090 ETH | $617.03 | +0.00 | -43.80 | $1.31 |
| | Neutral Up | 0.2928 ETH | $179.03 | -43.80 | +0.00 | |
| | Fixed | 0.6476 ETH | $396.02 | -22.10 | -21.70 | |
| +/- 4.0 % | Neutral Down | 0.9928 ETH | $607.12 | +0.00 | -84.40 | $0.66 |
| | Neutral Up | 0.3027 ETH | $185.12 | -84.40 | +0.00 | |
| | Fixed | 0.6411 ETH | $392.08 | -43.01 | -41.39 | |
| +/- 5.0 % | Neutral Down | 0.9891 ETH | $604.89 | +0.00 | -104.85 | $0.53 |
| | Neutral Up | 0.3033 ETH | $185.48 | -104.85 | +0.00 | |
| | Fixed | 0.6379 ETH | $390.12 | -53.69 | -51.16 | |
| +/- 10.0 % | Neutral Down | 0.9801 ETH | $599.36 | +0.00 | -208.63 | $0.27 |
| | Neutral Up | 0.2978 ETH | $182.10 | -208.63 | +0.00 | |
| | Fixed | 0.6222 ETH | $380.46 | -109.45 | -99.18 | |
## Capital: $8000 USDC
| Range | Strategy | Hedge Size | Margin (5x) | PnL Lower | PnL Upper | Est. Fees (1h) |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| +/- 0.5 % | Neutral Down | 2.1970 ETH | $1343.50 | +0.00 | -27.23 | $10.43 |
| | Neutral Up | 0.4162 ETH | $254.50 | -27.23 | +0.00 | |
| | Fixed | 1.3049 ETH | $798.00 | -13.64 | -13.59 | |
| +/- 1.0 % | Neutral Down | 2.0784 ETH | $1271.02 | -0.00 | -47.30 | $5.23 |
| | Neutral Up | 0.5315 ETH | $325.01 | -47.30 | +0.00 | |
| | Fixed | 1.3017 ETH | $796.01 | -23.75 | -23.55 | |
| +/- 2.0 % | Neutral Down | 2.0180 ETH | $1234.06 | +0.00 | -87.60 | $2.63 |
| | Neutral Up | 0.5855 ETH | $358.06 | -87.60 | +0.00 | |
| | Fixed | 1.2952 ETH | $792.04 | -44.20 | -43.40 | |
| +/- 4.0 % | Neutral Down | 1.9856 ETH | $1214.24 | +0.00 | -168.80 | $1.33 |
| | Neutral Up | 0.6054 ETH | $370.23 | -168.80 | -0.00 | |
| | Fixed | 1.2823 ETH | $784.16 | -86.02 | -82.78 | |
| +/- 5.0 % | Neutral Down | 1.9783 ETH | $1209.78 | +0.00 | -209.70 | $1.07 |
| | Neutral Up | 0.6066 ETH | $370.96 | -209.70 | -0.00 | |
| | Fixed | 1.2759 ETH | $780.24 | -107.38 | -102.32 | |
| +/- 10.0 % | Neutral Down | 1.9602 ETH | $1198.71 | +0.00 | -417.26 | $0.55 |
| | Neutral Up | 0.5956 ETH | $364.20 | -417.26 | +0.00 | |
| | Fixed | 1.2443 ETH | $760.93 | -218.89 | -198.36 | |

Binary file not shown.

View File

@ -0,0 +1,44 @@
[
{
"type": "AUTOMATIC",
"token_id": 41577981,
"status": "CLOSED",
"target_value": 99.99,
"entry_price": 3146.0405,
"amount0_initial": 0.0153,
"amount1_initial": 51.8762,
"liquidity": "36103333466890",
"range_upper": 3301.0555,
"range_lower": 2986.9335,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767561751,
"time_open": "04.01.26 22:22:31",
"clp_fees": 0.0,
"clp_TotPnL": -0.04
},
{
"type": "AUTOMATIC",
"token_id": 41584557,
"status": "CLOSED",
"target_value": 199.94,
"entry_price": 3147.2991,
"amount0_initial": 0.0307,
"amount1_initial": 103.2276,
"liquidity": "51814114093918",
"range_upper": 3367.7379,
"range_lower": 2927.7912,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767564277,
"time_open": "04.01.26 23:04:37",
"clp_fees": 0.39,
"clp_TotPnL": 1.98,
"hedge_TotPnL": -50.537316,
"hedge_fees_paid": 12.238471,
"combined_TotPnL": -62.76,
"hedge_HL_cost_est": 12.28,
"hedge_pnl_unrealized": -2.07,
"last_sync_hl": 1767685736
}
]

View File

@ -43,76 +43,29 @@ The system consists of three independent Python processes that coordinate via sh
| `telegram_monitor.py` | Telegram bot for notifications. |
| `{TARGET_DEX}_status.json` | **Critical:** Shared state file acting as the database between Manager and Hedger. |
| `.env` | Stores secrets (Private Keys, RPCs). **Do not commit.** |
| `tests/backtest/` | **New:** Professional Backtesting & Optimization Framework. |
| `tools/` | Utility scripts, including the Git Agent for auto-backups. |
| `logs/` | Detailed logs for all processes. |
## Configuration
## Backtesting Framework (Jan 2026 Update)
A robust simulation engine has been implemented to validate strategies before capital commitment.
### Environment Variables (`.env`)
Required variables for operation:
```env
# Blockchain
MAINNET_RPC_URL=... # Arbitrum
BNB_RPC_URL=... # BNB Chain
BASE_RPC_URL=... # Base
MAIN_WALLET_PRIVATE_KEY=...
MAIN_WALLET_ADDRESS=...
### Components
* **`tests/backtest/backtester.py`**: Event-driven engine mocking Web3/Hyperliquid interactions.
* **`tests/backtest/mocks.py`**: Stateful simulator handling balance tracking, V3 tick math, and fee accrual.
* **`tests/backtest/grid_search.py`**: Optimization runner to test parameter combinations (Range Width, Hedging Threshold).
* **`tests/backtest/analyze_results.py`**: Helper to interpret simulation CSV results.
# Hyperliquid
HEDGER_PRIVATE_KEY=... # Usually same as Main Wallet or specialized sub-account
# Telegram
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...
TELEGRAM_MONITOR_ENABLED=True
```
### Strategy Config (`clp_config.py`)
Key parameters controlling the bot's behavior:
* `TARGET_DEX`: Selects the active chain/profile (e.g., "UNISWAP_V3", "PANCAKESWAP_BNB").
* `RANGE_WIDTH_PCT`: Width of the LP position (e.g., `0.05` for +/- 5%).
* `TARGET_INVESTMENT_AMOUNT`: Notional size of the position in USD.
* `SLIPPAGE_TOLERANCE`: Max slippage for minting/swapping.
## Usage
The system is designed to run continuously. It is recommended to use a process manager like `pm2` or `systemd`, or simply run in separate terminal tabs.
1. **Start the Manager:**
```bash
python clp_manager.py
```
* *Action:* Will check for existing positions. If none, it prepares to open one based on config.
2. **Start the Hedger:**
```bash
python clp_hedger.py
```
* *Action:* Will read the position created by the Manager and open a corresponding short on Hyperliquid.
3. **Start Monitoring (Optional):**
```bash
python telegram_monitor.py
```
## Development & Git Agent
This project uses a custom **Git Agent** (`tools/git_agent.py`) for automated version control and backups.
* **Auto-Backup:** Runs hourly (if configured) to create backup branches (e.g., `backup-2025-01-01-12`).
* **Manual Commit:**
```bash
python tools/git_agent.py --backup
```
* **Status:**
```bash
python tools/git_agent.py --status
```
* **Restoration:**
To restore a file from a backup branch:
```bash
git checkout backup-BRANCH-NAME -- path/to/file.py
```
### Progress Status (Jan 1, 2026)
* **Completed:**
* Simulation loop runs end-to-end (Mint -> Accrue Fees -> Close).
* Fixed Mock Pricing logic (handling inverted T0/T1 pairs like USDT/WBNB).
* Implemented realistic Fee Accrual based on Trade Volume + Market Share.
* Verified "In Range" detection and position lifecycle.
* **Pending / Next Steps:**
* **Hedger PnL Verification:** Simulation showed 0.0 Hedging Fees because the price volatility in the 1-day sample was too low to trigger the 10% rebalance threshold. We are lowering thresholds to 1% to force activity and verify costs.
* **NAV Calculation:** Refine "Total PnL" to include Unrealized PnL from both LP and Hedge to handle Impermanent Loss correctly.
* **Final Optimization:** Run the `grid_search.py` with the corrected Market Share (0.02%) and lower thresholds to find the profitable "Sweet Spot".
## Logic Details

View File

@ -201,9 +201,9 @@
{
"type": "AUTOMATIC",
"token_id": 6164702,
"status": "OPEN",
"target_value": 981.88,
"entry_price": 846.4517,
"status": "CLOSED",
"target_value": 993.41,
"entry_price": 866.3337,
"amount0_initial": 490.942,
"amount1_initial": 0.58,
"liquidity": "8220443727732589279738",
@ -212,7 +212,471 @@
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767164052,
"hedge_TotPnL": -0.026171,
"hedge_fees_paid": 0.097756
"hedge_TotPnL": -3.587319,
"hedge_fees_paid": 0.723066,
"clp_fees": 1.75,
"clp_TotPnL": 0.31,
"timestamp_close": 1767189814,
"time_close": "31.12.25 15:03:34"
},
{
"type": "AUTOMATIC",
"token_id": 6166625,
"status": "CLOSED",
"target_value": 996.7,
"entry_price": 873.896,
"amount0_initial": 496.6816,
"amount1_initial": 0.5722,
"liquidity": "8653989263919246133281",
"range_upper": 877.3107,
"range_lower": 870.4946,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767190229,
"time_open": "31.12.25 15:10:29",
"hedge_TotPnL": 4.004047,
"hedge_fees_paid": 0.807563,
"clp_fees": 0.34,
"clp_TotPnL": -3.96,
"timestamp_close": 1767191809,
"time_close": "31.12.25 15:36:49"
},
{
"type": "AUTOMATIC",
"token_id": 6166939,
"status": "CLOSED",
"target_value": 999.11,
"entry_price": 866.9331,
"amount0_initial": 500.0004,
"amount1_initial": 0.5757,
"liquidity": "8709690098157915483248",
"range_upper": 870.3205,
"range_lower": 863.5588,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767192966,
"time_open": "31.12.25 15:56:06",
"hedge_TotPnL": 1.064447,
"hedge_fees_paid": 0.927408,
"clp_fees": 0.3,
"clp_TotPnL": -2.71,
"timestamp_close": 1767193991,
"time_close": "31.12.25 16:13:11"
},
{
"type": "AUTOMATIC",
"token_id": 6167093,
"status": "CLOSED",
"target_value": 996.69,
"entry_price": 864.077,
"amount0_initial": 500.0128,
"amount1_initial": 0.5748,
"liquidity": "8702875143941291654654",
"range_upper": 867.4533,
"range_lower": 860.7139,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767194522,
"time_open": "31.12.25 16:22:02",
"hedge_TotPnL": -3.013382,
"hedge_fees_paid": 0.814047,
"clp_fees": 0.95,
"clp_TotPnL": -2.76,
"timestamp_close": 1767199352,
"time_close": "31.12.25 17:42:32"
},
{
"type": "AUTOMATIC",
"token_id": 6167590,
"status": "CLOSED",
"target_value": 998.37,
"entry_price": 861.6611,
"amount0_initial": 498.363,
"amount1_initial": 0.5803,
"liquidity": "8729751956580574272932",
"range_upper": 865.028,
"range_lower": 858.3074,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767200083,
"time_open": "31.12.25 17:54:43",
"hedge_TotPnL": -4.720271,
"hedge_fees_paid": 1.311938,
"clp_fees": 1.95,
"clp_TotPnL": 2.92,
"timestamp_close": 1767217535,
"time_close": "31.12.25 22:45:35"
},
{
"type": "AUTOMATIC",
"token_id": 6168553,
"status": "CLOSED",
"target_value": 991.55,
"entry_price": 865.4606,
"amount0_initial": 491.5385,
"amount1_initial": 0.5777,
"liquidity": "8651067937842123260294",
"range_upper": 868.8423,
"range_lower": 862.092,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767217854,
"time_open": "31.12.25 22:50:54",
"hedge_TotPnL": -3.016562,
"hedge_fees_paid": 0.460066,
"clp_fees": 0.58,
"clp_TotPnL": 1.55,
"timestamp_close": 1767229894,
"time_close": "01.01.26 02:11:34"
},
{
"type": "AUTOMATIC",
"token_id": 6169279,
"status": "CLOSED",
"target_value": 993.04,
"entry_price": 869.1899,
"amount0_initial": 493.031,
"amount1_initial": 0.5753,
"liquidity": "8645470844979366936741",
"range_upper": 872.5862,
"range_lower": 865.8068,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767230090,
"time_open": "01.01.26 02:14:50",
"hedge_TotPnL": -1.709208,
"hedge_fees_paid": 0.300063,
"clp_fees": 0.22,
"clp_TotPnL": 1.19,
"timestamp_close": 1767232654,
"time_close": "01.01.26 02:57:34"
},
{
"type": "AUTOMATIC",
"token_id": 6169469,
"status": "CLOSED",
"target_value": 996.5,
"entry_price": 873.4592,
"amount0_initial": 496.4932,
"amount1_initial": 0.5724,
"liquidity": "8654359631059929427298",
"range_upper": 876.8721,
"range_lower": 870.0595,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767233101,
"time_open": "01.01.26 03:05:01",
"hedge_TotPnL": 0.379026,
"hedge_fees_paid": 0.415621,
"clp_fees": 0.4,
"clp_TotPnL": -3.21,
"timestamp_close": 1767238291,
"time_close": "01.01.26 04:31:31"
},
{
"type": "AUTOMATIC",
"token_id": 6169789,
"status": "CLOSED",
"target_value": 996.05,
"entry_price": 869.103,
"amount0_initial": 500.0117,
"amount1_initial": 0.5707,
"liquidity": "8672126155624077647253",
"range_upper": 872.4989,
"range_lower": 865.7203,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767238369,
"time_open": "01.01.26 04:32:49",
"hedge_TotPnL": 3.954178,
"hedge_fees_paid": 0.765854,
"clp_fees": 0.21,
"clp_TotPnL": -2.7,
"timestamp_close": 1767242596,
"time_close": "01.01.26 05:43:16"
},
{
"type": "AUTOMATIC",
"token_id": 6170135,
"status": "CLOSED",
"target_value": 998.15,
"entry_price": 862.6094,
"amount0_initial": 500.001,
"amount1_initial": 0.5775,
"liquidity": "8723056935772169247603",
"range_upper": 865.98,
"range_lower": 859.252,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767243101,
"time_open": "01.01.26 05:51:41",
"hedge_TotPnL": 2.409614,
"hedge_fees_paid": 1.355821,
"clp_fees": 0.64,
"clp_TotPnL": -2.48,
"timestamp_close": 1767254432,
"time_close": "01.01.26 09:00:32"
},
{
"type": "AUTOMATIC",
"token_id": 6170841,
"status": "CLOSED",
"target_value": 998.62,
"entry_price": 859.8536,
"amount0_initial": 498.6144,
"amount1_initial": 0.5815,
"liquidity": "8741115554990437903852",
"range_upper": 863.2134,
"range_lower": 856.5069,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767254603,
"time_open": "01.01.26 09:03:23",
"hedge_TotPnL": -4.244326,
"hedge_fees_paid": 1.827099,
"clp_fees": 2.59,
"clp_TotPnL": 3.56,
"timestamp_close": 1767308203,
"time_close": "01.01.26 23:56:43"
},
{
"type": "AUTOMATIC",
"token_id": 6175190,
"status": "CLOSED",
"target_value": 998.54,
"entry_price": 863.8179,
"amount0_initial": 498.5396,
"amount1_initial": 0.5788,
"liquidity": "8720378230633469405596",
"range_upper": 867.1932,
"range_lower": 860.4557,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767308440,
"time_open": "02.01.26 00:00:40",
"hedge_TotPnL": 2.712563,
"hedge_fees_paid": 0.819224,
"clp_fees": 0.62,
"clp_TotPnL": -2.49,
"timestamp_close": 1767320335,
"time_close": "02.01.26 03:18:55"
},
{
"type": "AUTOMATIC",
"token_id": 6175868,
"status": "CLOSED",
"target_value": 991.53,
"entry_price": 860.2836,
"amount0_initial": 491.5252,
"amount1_initial": 0.5812,
"liquidity": "8676952736685102236300",
"range_upper": 863.6451,
"range_lower": 856.9352,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767320584,
"time_open": "02.01.26 03:23:04",
"hedge_TotPnL": -1.782405,
"hedge_fees_paid": 0.312615,
"clp_fees": 0.11,
"clp_TotPnL": 1.09,
"timestamp_close": 1767323453,
"time_close": "02.01.26 04:10:53"
},
{
"type": "AUTOMATIC",
"token_id": 6176051,
"status": "CLOSED",
"target_value": 997.7,
"entry_price": 863.7315,
"amount0_initial": 497.694,
"amount1_initial": 0.5789,
"liquidity": "8713457799891424871655",
"range_upper": 867.1064,
"range_lower": 860.3697,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767324323,
"time_open": "02.01.26 04:25:23",
"hedge_TotPnL": -3.840822,
"hedge_fees_paid": 0.892717,
"clp_fees": 0.65,
"clp_TotPnL": 1.63,
"timestamp_close": 1767335965,
"time_close": "02.01.26 07:39:25"
},
{
"type": "AUTOMATIC",
"token_id": 6176727,
"status": "CLOSED",
"target_value": 990.64,
"entry_price": 867.5401,
"amount0_initial": 490.6325,
"amount1_initial": 0.5764,
"liquidity": "8632807640200638943476",
"range_upper": 870.9299,
"range_lower": 864.1634,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767336634,
"time_open": "02.01.26 07:50:34",
"hedge_TotPnL": -5.442897,
"hedge_fees_paid": 0.85406,
"clp_fees": 0.91,
"clp_TotPnL": 1.88,
"timestamp_close": 1767347410,
"time_close": "02.01.26 10:50:10"
},
{
"type": "AUTOMATIC",
"token_id": 6177360,
"status": "CLOSED",
"target_value": 993.15,
"entry_price": 870.8428,
"amount0_initial": 493.1411,
"amount1_initial": 0.5742,
"liquidity": "8638221415835012765221",
"range_upper": 874.2456,
"range_lower": 867.4533,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767347607,
"time_open": "02.01.26 10:53:27",
"hedge_TotPnL": 1.294631,
"hedge_fees_paid": 1.047541,
"clp_fees": 1.24,
"clp_TotPnL": -1.66,
"timestamp_close": 1767363551,
"time_close": "02.01.26 15:19:11"
},
{
"type": "AUTOMATIC",
"token_id": 6185008,
"status": "CLOSED",
"target_value": 992.5,
"entry_price": 867.1064,
"amount0_initial": 492.4924,
"amount1_initial": 0.5766,
"liquidity": "8651147821199061055073",
"range_upper": 870.4946,
"range_lower": 863.7315,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767363751,
"time_open": "02.01.26 15:22:31",
"hedge_TotPnL": -2.674848,
"hedge_fees_paid": 0.393713,
"clp_fees": 0.32,
"clp_TotPnL": 1.28,
"timestamp_close": 1767364835,
"time_close": "02.01.26 15:40:35"
},
{
"type": "AUTOMATIC",
"token_id": 6185867,
"status": "CLOSED",
"target_value": 996.08,
"entry_price": 875.5579,
"amount0_initial": 496.0791,
"amount1_initial": 0.5711,
"liquidity": "8640396990671870185711",
"range_upper": 878.9791,
"range_lower": 872.15,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767365969,
"time_open": "02.01.26 15:59:29",
"hedge_TotPnL": 2.830618,
"hedge_fees_paid": 1.060949,
"clp_fees": 0.53,
"clp_TotPnL": -2.97,
"timestamp_close": 1767367303,
"time_close": "02.01.26 16:21:43"
},
{
"type": "AUTOMATIC",
"token_id": 6186334,
"status": "CLOSED",
"target_value": 996.88,
"entry_price": 873.9834,
"amount0_initial": 496.8714,
"amount1_initial": 0.5721,
"liquidity": "8655088063104153413073",
"range_upper": 877.3984,
"range_lower": 870.5816,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767367883,
"time_open": "02.01.26 16:31:23",
"hedge_TotPnL": -3.585575,
"hedge_fees_paid": 0.57548,
"clp_fees": 0.13,
"clp_TotPnL": 1.1,
"timestamp_close": 1767368416,
"time_close": "02.01.26 16:40:16"
},
{
"type": "AUTOMATIC",
"token_id": 6186613,
"status": "CLOSED",
"target_value": 993.42,
"entry_price": 880.2984,
"amount0_initial": 493.4134,
"amount1_initial": 0.568,
"liquidity": "8594082766621558309079",
"range_upper": 883.7381,
"range_lower": 876.8721,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767368652,
"time_open": "02.01.26 16:44:12",
"hedge_TotPnL": -4.384157,
"hedge_fees_paid": 0.627319,
"clp_fees": 0.92,
"clp_TotPnL": 1.89,
"timestamp_close": 1767371545,
"time_close": "02.01.26 17:32:25"
},
{
"type": "AUTOMATIC",
"token_id": 6187096,
"status": "CLOSED",
"target_value": 996.91,
"entry_price": 885.9501,
"amount0_initial": 496.8996,
"amount1_initial": 0.5644,
"liquidity": "2271526539550158344821",
"range_upper": 899.159,
"range_lower": 872.9353,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767372001,
"time_open": "02.01.26 17:40:01",
"hedge_TotPnL": 7.60386,
"hedge_fees_paid": 0.283695,
"clp_fees": 1.38,
"clp_TotPnL": -9.61,
"timestamp_close": 1767424468,
"time_close": "03.01.26 08:14:28"
},
{
"type": "AUTOMATIC",
"token_id": 6203530,
"status": "CLOSED",
"target_value": 998.23,
"entry_price": 872.0628,
"amount0_initial": 500.0027,
"amount1_initial": 0.5713,
"liquidity": "2292568457223553397610",
"range_upper": 885.0647,
"range_lower": 859.252,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767424756,
"time_open": "03.01.26 08:19:16",
"hedge_TotPnL": -2.47676,
"hedge_fees_paid": 0.287962,
"clp_fees": 0.19,
"clp_TotPnL": 1.92
}
]

View File

@ -288,7 +288,7 @@
{
"type": "AUTOMATIC",
"token_id": 5182179,
"status": "OPEN",
"status": "CLOSED",
"target_value": 1993.84,
"entry_price": 2969.9855,
"amount0_initial": 0.3347,
@ -299,7 +299,607 @@
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1766968369,
"hedge_TotPnL": -5.078135,
"hedge_fees_paid": 2.029157
"hedge_TotPnL": -62.166433,
"hedge_fees_paid": 2.587208,
"clp_fees": 23.23,
"clp_TotPnL": 47.64,
"timestamp_close": 1767371501,
"time_close": "02.01.26 17:31:41"
},
{
"type": "AUTOMATIC",
"token_id": 5190205,
"status": "CLOSED",
"target_value": 1990.81,
"entry_price": 3136.9306,
"amount0_initial": 0.2991,
"amount1_initial": 1052.4842,
"liquidity": "3750669047237424",
"range_upper": 3165.289,
"range_lower": 3105.7192,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767371848,
"time_open": "02.01.26 17:37:28",
"hedge_TotPnL": 9.40878,
"hedge_fees_paid": 0.538036,
"clp_fees": 6.41,
"clp_TotPnL": -9.78,
"timestamp_close": 1767379841,
"time_close": "02.01.26 19:50:41"
},
{
"type": "AUTOMATIC",
"token_id": 5190713,
"status": "CLOSED",
"target_value": 995.94,
"entry_price": 3103.8564,
"amount0_initial": 0.1562,
"amount1_initial": 511.0986,
"liquidity": "610494695009033",
"range_upper": 3193.9038,
"range_lower": 3010.9236,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767379916,
"time_open": "02.01.26 19:51:56",
"hedge_TotPnL": 0.4043,
"hedge_fees_paid": 0.277557,
"clp_fees": 1.83,
"clp_TotPnL": 0.77
},
{
"type": "AUTOMATIC",
"token_id": 5191754,
"status": "CLOSED",
"target_value": 2986.13,
"entry_price": 3095.7973,
"amount0_initial": 0.4577,
"amount1_initial": 1569.095,
"liquidity": "8270626895418999",
"range_upper": 3115.0499,
"range_lower": 3074.8183,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767430984,
"time_open": "03.01.26 10:03:04",
"hedge_TotPnL": -10.166,
"hedge_fees_paid": 7.014425,
"clp_fees": 8.24,
"clp_TotPnL": 12.66,
"timestamp_close": 1767475160,
"time_close": "03.01.26 22:19:20"
},
{
"type": "AUTOMATIC",
"token_id": 5192638,
"status": "CLOSED",
"target_value": 2994.04,
"entry_price": 3125.0335,
"amount0_initial": 0.46,
"amount1_initial": 1559.9,
"range_upper": 3140.069,
"range_lower": 3108.8263,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767482267,
"time_open": "04.01.26 00:17:47",
"clp_fees": 2998.49,
"clp_TotPnL": 4.45,
"hedge_TotPnL": -9.78996,
"hedge_fees_paid": 0.868435,
"timestamp_close": 1767485229,
"time_close": "04.01.26 01:07:09"
},
{
"type": "AUTOMATIC",
"token_id": 5192707,
"status": "CLOSED",
"target_value": 2999.37,
"entry_price": 3146.0405,
"amount0_initial": 0.4767,
"amount1_initial": 1500.0,
"range_upper": 3158.9651,
"range_lower": 3130.6634,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767485383,
"time_open": "04.01.26 01:09:43",
"clp_fees": 4.35,
"clp_TotPnL": -0.63,
"timestamp_close": 1767488126,
"time_close": "04.01.26 01:55:26"
},
{
"type": "AUTOMATIC",
"token_id": 5192787,
"status": "CLOSED",
"target_value": 2989.41,
"entry_price": 3158.9651,
"amount0_initial": 0.4715,
"amount1_initial": 1500.0,
"range_upper": 3171.6256,
"range_lower": 3143.2105,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767488200,
"time_open": "04.01.26 01:56:40",
"clp_fees": 2.57,
"clp_TotPnL": -10.59,
"hedge_TotPnL": 3.058716,
"hedge_fees_paid": 3.334872,
"timestamp_close": 1767491374,
"time_close": "04.01.26 02:49:34"
},
{
"type": "AUTOMATIC",
"token_id": 5192872,
"status": "CLOSED",
"target_value": 2974.66,
"entry_price": 3142.8962,
"amount0_initial": 0.4692,
"amount1_initial": 1500.0,
"range_upper": 3155.8079,
"range_lower": 3127.5344,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767491446,
"time_open": "04.01.26 02:50:46",
"clp_fees": 6.14,
"clp_TotPnL": 25.34,
"hedge_TotPnL": -1.09566,
"hedge_fees_paid": 3.319216,
"timestamp_close": 1767507012,
"time_close": "04.01.26 07:10:12"
},
{
"type": "AUTOMATIC",
"token_id": 5193076,
"status": "CLOSED",
"target_value": 2927.37,
"entry_price": 3155.8079,
"amount0_initial": 0.4522,
"amount1_initial": 1500.0,
"range_upper": 3168.4557,
"range_lower": 3140.069,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767507083,
"time_open": "04.01.26 07:11:23",
"clp_fees": 2.91,
"clp_TotPnL": -36.11,
"hedge_TotPnL": 0.0,
"hedge_fees_paid": 1.655925,
"timestamp_close": 1767511297,
"time_close": "04.01.26 08:21:37"
},
{
"type": "AUTOMATIC",
"token_id": 5193158,
"status": "CLOSED",
"target_value": 2970.4,
"entry_price": 3139.755,
"amount0_initial": 0.477,
"amount1_initial": 1500.0,
"range_upper": 3152.6538,
"range_lower": 3124.4086,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767511369,
"time_open": "04.01.26 08:22:49",
"clp_fees": 11.2,
"clp_TotPnL": 0.27,
"hedge_TotPnL": -1.76453,
"hedge_fees_paid": 1.292159,
"timestamp_close": 1767541575,
"time_close": "04.01.26 16:46:15"
},
{
"type": "AUTOMATIC",
"token_id": 5193699,
"status": "CLOSED",
"target_value": 2985.65,
"entry_price": 3124.0962,
"amount0_initial": 0.4461,
"amount1_initial": 1592.1238,
"liquidity": "8231768660184301",
"range_upper": 3143.2105,
"range_lower": 3102.6152,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767541587,
"time_open": "04.01.26 16:46:27",
"clp_fees": 5.59,
"clp_TotPnL": 9.87,
"hedge_TotPnL": -2.74009,
"hedge_fees_paid": 3.283603,
"timestamp_close": 1767561023,
"time_close": "04.01.26 22:10:23"
},
{
"type": "AUTOMATIC",
"token_id": 5194064,
"status": "CLOSED",
"target_value": 2995.07,
"entry_price": 3149.8178,
"amount0_initial": 0.4308,
"amount1_initial": 1638.2123,
"liquidity": "8224032596348534",
"range_upper": 3168.4557,
"range_lower": 3127.5344,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767561335,
"time_open": "04.01.26 22:15:35",
"hedge_TotPnL": -10.8216,
"hedge_fees_paid": 0.744327,
"clp_fees": 10.96,
"clp_TotPnL": 14.98,
"timestamp_close": 1767574703,
"time_close": "05.01.26 01:58:23"
},
{
"type": "AUTOMATIC",
"token_id": 5194353,
"status": "CLOSED",
"target_value": 2986.84,
"entry_price": 3167.1887,
"amount0_initial": 0.4622,
"amount1_initial": 1523.0178,
"liquidity": "8178792616583388",
"range_upper": 3187.5227,
"range_lower": 3146.3551,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767575103,
"time_open": "05.01.26 02:05:03",
"hedge_TotPnL": -11.72105,
"hedge_fees_paid": 0.851074,
"clp_fees": 1.32,
"clp_TotPnL": 6.03,
"timestamp_close": 1767575748,
"time_close": "05.01.26 02:15:48"
},
{
"type": "AUTOMATIC",
"token_id": 5194438,
"status": "CLOSED",
"target_value": 2994.26,
"entry_price": 3182.7452,
"amount0_initial": 0.4695,
"amount1_initial": 1499.9963,
"liquidity": "8179052347416944",
"range_upper": 3203.4994,
"range_lower": 3162.1255,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767576062,
"time_open": "05.01.26 02:21:02",
"hedge_TotPnL": -13.130776,
"hedge_fees_paid": 0.866544,
"clp_fees": 2.0,
"clp_TotPnL": 6.88,
"timestamp_close": 1767577312,
"time_close": "05.01.26 02:41:52"
},
{
"type": "AUTOMATIC",
"token_id": 5194549,
"status": "CLOSED",
"target_value": 2993.07,
"entry_price": 3209.2706,
"amount0_initial": 0.4437,
"amount1_initial": 1569.0894,
"liquidity": "8141974814554655",
"range_upper": 3229.2289,
"range_lower": 3187.5227,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767577667,
"time_open": "05.01.26 02:47:47",
"hedge_TotPnL": 9.143032,
"hedge_fees_paid": 0.817659,
"clp_fees": 1.68,
"clp_TotPnL": -14.2,
"timestamp_close": 1767578919,
"time_close": "05.01.26 03:08:39"
},
{
"type": "AUTOMATIC",
"token_id": 5194636,
"status": "CLOSED",
"target_value": 2999.61,
"entry_price": 3191.6689,
"amount0_initial": 0.4122,
"amount1_initial": 1683.9338,
"liquidity": "8182395676682951",
"range_upper": 3209.9125,
"range_lower": 3168.4557,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767579235,
"time_open": "05.01.26 03:13:55",
"hedge_TotPnL": 12.194039,
"hedge_fees_paid": 0.752756,
"clp_fees": 4.31,
"clp_TotPnL": -14.67,
"timestamp_close": 1767587437,
"time_close": "05.01.26 05:30:37"
},
{
"type": "AUTOMATIC",
"token_id": 5194825,
"status": "CLOSED",
"target_value": 2968.38,
"entry_price": 3162.4417,
"amount0_initial": 0.4206,
"amount1_initial": 1638.1774,
"liquidity": "8134444867821814",
"range_upper": 3181.1543,
"range_lower": 3140.069,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1767587750,
"time_open": "05.01.26 05:35:50",
"hedge_TotPnL": -0.1704,
"hedge_fees_paid": 1.553601,
"clp_fees": 15.16,
"clp_TotPnL": 10.5,
"combined_TotPnL": 13.22,
"hedge_HL_cost_est": 0.77,
"hedge_pnl_unrealized": 4.3,
"last_sync_hl": 1767622049,
"timestamp_close": 1767622037,
"time_close": "05.01.26 15:07:17"
},
{
"type": "AUTOMATIC",
"token_id": 5195733,
"status": "CLOSED",
"target_value": 2984.65,
"entry_price": 3150.7629,
"amount0_initial": 0.4341,
"amount1_initial": 1617.0073,
"liquidity": "4637302533941787",
"range_upper": 3184.3369,
"range_lower": 3111.9366,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "AUTO",
"range_width_initial": 0.01160980429312654,
"timestamp_open": 1767622560,
"time_open": "05.01.26 15:16:00",
"hedge_TotPnL": -3.139154,
"hedge_fees_paid": 0.793472,
"combined_TotPnL": 1.6,
"hedge_HL_cost_est": 0.79,
"hedge_pnl_unrealized": -2.92,
"last_sync_hl": 1767624126,
"clp_fees": 2.68,
"clp_TotPnL": 5.4,
"timestamp_close": 1767624113,
"time_close": "05.01.26 15:41:53"
},
{
"type": "AUTOMATIC",
"token_id": 5195860,
"status": "CLOSED",
"target_value": 2976.38,
"entry_price": 3157.3861,
"amount0_initial": 0.438,
"amount1_initial": 1593.5552,
"liquidity": "6634808665424129",
"range_upper": 3181.1543,
"range_lower": 3130.6634,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "AUTO",
"range_width_initial": 0.008451255668317245,
"timestamp_open": 1767624512,
"time_open": "05.01.26 15:48:32",
"hedge_TotPnL": -4.258952,
"hedge_fees_paid": 0.804724,
"combined_TotPnL": 0.01,
"hedge_HL_cost_est": 0.81,
"hedge_pnl_unrealized": -3.03,
"last_sync_hl": 1767624869,
"clp_fees": 1.25,
"clp_TotPnL": 4.26,
"timestamp_close": 1767624855,
"time_close": "05.01.26 15:54:15"
},
{
"type": "AUTOMATIC",
"token_id": 5195910,
"status": "CLOSED",
"target_value": 2957.51,
"entry_price": 3166.872,
"amount0_initial": 0.4342,
"amount1_initial": 1582.386,
"liquidity": "8772781500746006",
"range_upper": 3184.3369,
"range_lower": 3146.3551,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "AUTO",
"range_width_initial": 0.00610577067463115,
"timestamp_open": 1767625173,
"time_open": "05.01.26 15:59:33",
"hedge_TotPnL": -6.49276,
"hedge_fees_paid": 0.782891,
"combined_TotPnL": 2.73,
"hedge_HL_cost_est": 0.78,
"hedge_pnl_unrealized": -5.77,
"last_sync_hl": 1767627649,
"clp_fees": 5.74,
"clp_TotPnL": 9.23,
"timestamp_close": 1767627635,
"time_close": "05.01.26 16:40:35"
},
{
"type": "AUTOMATIC",
"token_id": 5196059,
"status": "CLOSED",
"target_value": 2984.89,
"entry_price": 3194.862,
"amount0_initial": 0.4148,
"amount1_initial": 1659.6726,
"liquidity": "7054813881893230",
"range_upper": 3216.3384,
"range_lower": 3168.4557,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "FIXED",
"range_width_initial": 0.0075,
"timestamp_open": 1767627994,
"time_open": "05.01.26 16:46:34",
"hedge_TotPnL": 9.768993,
"hedge_fees_paid": 0.761925,
"combined_TotPnL": -3.68,
"hedge_HL_cost_est": 0.76,
"hedge_pnl_unrealized": 13.07,
"last_sync_hl": 1767633796,
"clp_fees": 6.47,
"clp_TotPnL": -13.17,
"timestamp_close": 1767633781,
"time_close": "05.01.26 18:23:01"
},
{
"type": "AUTOMATIC",
"token_id": 5196356,
"status": "CLOSED",
"target_value": 2970.03,
"entry_price": 3162.7579,
"amount0_initial": 0.4513,
"amount1_initial": 1542.7246,
"liquidity": "7558082601107499",
"range_upper": 3184.3369,
"range_lower": 3140.069,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "AUTO",
"range_width_initial": 0.007188283945973874,
"timestamp_open": 1767634095,
"time_open": "05.01.26 18:28:15",
"hedge_TotPnL": -5.78208,
"hedge_fees_paid": 0.833176,
"combined_TotPnL": -1.93,
"hedge_HL_cost_est": 0.83,
"hedge_pnl_unrealized": -5.65,
"last_sync_hl": 1767634756,
"clp_fees": 0.53,
"clp_TotPnL": 4.88,
"timestamp_close": 1767634742,
"time_close": "05.01.26 18:39:02"
},
{
"type": "AUTOMATIC",
"token_id": 5196394,
"status": "CLOSED",
"target_value": 2997.55,
"entry_price": 3187.5227,
"amount0_initial": 0.4385,
"amount1_initial": 1599.8109,
"liquidity": "7092801036999878",
"range_upper": 3209.9125,
"range_lower": 3162.1255,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "FIXED",
"range_width_initial": 0.0075,
"timestamp_open": 1767635055,
"time_open": "05.01.26 18:44:15",
"hedge_TotPnL": -10.899792,
"hedge_fees_paid": 0.809798,
"combined_TotPnL": 5.4,
"hedge_HL_cost_est": 0.61,
"hedge_pnl_unrealized": 0.0,
"last_sync_hl": 1767635701,
"clp_fees": 1.13,
"clp_TotPnL": 6.03,
"timestamp_close": 1767635701,
"time_close": "05.01.26 18:55:01"
},
{
"type": "AUTOMATIC",
"token_id": 5196471,
"status": "CLOSED",
"target_value": 2980.42,
"entry_price": 3209.5915,
"amount0_initial": 0.4364,
"amount1_initial": 1579.8119,
"liquidity": "7027957507483544",
"range_upper": 3232.4596,
"range_lower": 3184.3369,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "FIXED",
"range_width_initial": 0.0075,
"timestamp_open": 1767636146,
"time_open": "05.01.26 19:02:26",
"hedge_TotPnL": -13.87062,
"hedge_fees_paid": 0.817984,
"combined_TotPnL": -2.61,
"hedge_HL_cost_est": 1.43,
"hedge_pnl_unrealized": 0.0,
"last_sync_hl": 1767643534,
"clp_fees": 7.68,
"clp_TotPnL": 12.7,
"timestamp_close": 1767643533,
"time_close": "05.01.26 21:05:33"
},
{
"type": "AUTOMATIC",
"token_id": 5196956,
"status": "CLOSED",
"target_value": 2977.3,
"entry_price": 3232.7828,
"amount0_initial": 0.4269,
"amount1_initial": 1597.0858,
"liquidity": "6995425133879491",
"range_upper": 3255.165,
"range_lower": 3206.7043,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "FIXED",
"range_width_initial": 0.0075,
"timestamp_open": 1767643849,
"time_open": "05.01.26 21:10:49",
"hedge_TotPnL": -1.356196,
"hedge_fees_paid": 1.243909,
"combined_TotPnL": -3.62,
"hedge_HL_cost_est": 1.58,
"hedge_pnl_unrealized": 9.64,
"last_sync_hl": 1767660041,
"clp_fees": 10.88,
"clp_TotPnL": -7.56,
"timestamp_close": 1767660028,
"time_close": "06.01.26 01:40:28"
},
{
"type": "AUTOMATIC",
"token_id": 5198324,
"status": "OPEN",
"target_value": 2981.26,
"entry_price": 3217.6251,
"amount0_initial": 0.4045,
"amount1_initial": 1679.6194,
"liquidity": "7021319704792719",
"range_upper": 3238.9306,
"range_lower": 3190.7116,
"token0_decimals": 18,
"token1_decimals": 6,
"range_mode": "AUTO",
"range_width_initial": 0.007592620856214823,
"timestamp_open": 1767688223,
"time_open": "06.01.26 09:30:23",
"hedge_TotPnL": 0.0,
"hedge_fees_paid": 0.0,
"combined_TotPnL": -0.47,
"hedge_HL_cost_est": 0.57,
"hedge_pnl_unrealized": 0.72,
"last_sync_hl": 1767689255,
"clp_fees": 0.12,
"clp_TotPnL": 0.64
}
]

82
florida/clp_abis.py Normal file
View File

@ -0,0 +1,82 @@
import json
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
UNISWAP_V3_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
UNISWAP_V3_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_NPM_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
AERODROME_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
SWAP_ROUTER_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
WETH9_ABI = json.loads('''
[
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
]
''')

View File

@ -8,7 +8,9 @@ STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
# --- DEFAULT STRATEGY ---
DEFAULT_STRATEGY = {
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
"MONITOR_INTERVAL_SECONDS": 300, # Manager loop & sync interval
"LOG_INTERVAL_SECONDS": 300, # Hedger console logging interval
"RANGE_MODE": "AUTO", # Options: "AUTO" (BB-based), "FIXED" (RANGE_WIDTH_PCT)
"CLOSE_POSITION_ENABLED": True, # Allow the bot to automatically close out-of-range positions
"OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior
@ -19,15 +21,16 @@ DEFAULT_STRATEGY = {
"VALUE_REFERENCE": "USD", # Base currency for all calculations
# Range Settings
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting
"RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.03"), # Max allowed slippage for swaps and minting
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction), "FIXED" (Initial Delta)
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
# Unified Hedger Settings
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
"LEVERAGE": 5, # Leverage to use on Hyperliquid
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range
"ZONE_CLOSE_START": Decimal("10.0"), # Distance (pct) from edge to start closing logic
@ -45,7 +48,7 @@ DEFAULT_STRATEGY = {
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
"ENABLE_EDGE_CLEANUP": True, # Force rebalances when price is at range boundaries
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.02"), # % of range width used for edge detection
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.03"), # % of range width used for edge detection
"MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
@ -72,6 +75,9 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500,
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
"RANGE_WIDTH_PCT": Decimal("0.0075"),
},
"UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
@ -98,6 +104,7 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100,
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.1875"), # 0.1875 only for asymmetric shedge % of range width used for edge detection
"RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
@ -123,6 +130,34 @@ CLP_PROFILES = {
"TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10")
},
"AERODROME_BASE_CL": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 100, # TickSpacing 100 pool (0xb2cc...)
"RANGE_WIDTH_PCT": Decimal("0.075"),
"TARGET_INVESTMENT_AMOUNT": 200,
"HEDGE_STRATEGY": "FIXED",
},
"AERODROME_WETH-USDC_008": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC Stable",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 1, # TickSpacing 1 pool (0xdbc6...)
"RANGE_WIDTH_PCT": Decimal("0.05"),
"TARGET_INVESTMENT_AMOUNT": 200,
"HEDGE_STRATEGY": "FIXED",
}
}

View File

@ -14,20 +14,15 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(project_root)
# Import local modules
try:
from logging_utils import setup_logging
except ImportError:
setup_logging = None
# Ensure root logger is clean if we can't use setup_logging
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
# Ensure root logger is clean
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY, TARGET_DEX
# Load environment variables
dotenv_path = os.path.join(current_dir, '.env')
@ -213,7 +208,7 @@ class HyperliquidStrategy:
else: # >=5% range
return Decimal("0.075") # Standard for wide ranges
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict:
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal, strategy_type: str = "ASYMMETRIC") -> Dict:
# Note: current_short_size here is virtual (just for this specific strategy),
# but the unified hedger will use the 'target_short' output primarily.
@ -221,17 +216,33 @@ class HyperliquidStrategy:
# --- ASYMMETRIC COMPENSATION ---
adj_pct = Decimal("0.0")
range_width = self.high_range - self.low_range
if range_width > 0:
dist = current_price - self.entry_price
half_width = range_width / Decimal("2")
norm_dist = dist / half_width
max_boost = self.get_compensation_boost()
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
if strategy_type == "ASYMMETRIC":
range_width = self.high_range - self.low_range
if range_width > 0:
dist = current_price - self.entry_price
half_width = range_width / Decimal("2")
norm_dist = dist / half_width
max_boost = self.get_compensation_boost()
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
# --- FIXED STRATEGY LOGIC ---
if strategy_type == "FIXED":
# Target is exactly the pool delta at entry price
raw_target_short = self.get_pool_delta(self.entry_price)
adj_pct = Decimal("0")
elif strategy_type == "BOTTOM":
if current_price > self.entry_price:
# Disable hedging in upper half
raw_target_short = Decimal("0")
adj_pct = Decimal("0")
else:
# Enable hedging in lower half (standard delta)
# No asymmetric boost applied
adj_pct = Decimal("0")
raw_target_short = pool_delta
adjusted_target_short = raw_target_short * (Decimal("1.0") + adj_pct)
diff = adjusted_target_short - abs(current_short_size)
@ -273,12 +284,23 @@ class UnifiedHedger:
# Market Data Cache
self.last_prices = {}
self.price_history = {} # Symbol -> List[Decimal]
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
self.last_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List)
self.shadow_orders = []
# State: Emergency Close Hysteresis
# Map: (file_path, token_id) -> bool
self.emergency_close_active = {}
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
self.custom_fixed_targets = {}
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
self.hedge_entry_prices = {}
self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -286,6 +308,7 @@ class UnifiedHedger:
def _init_coin_configs(self):
"""Pre-load configuration for known coins from CLP_PROFILES."""
# 1. Load all profiles (order depends on dict iteration)
for profile_key, profile_data in CLP_PROFILES.items():
symbol = profile_data.get("COIN_SYMBOL")
if symbol:
@ -297,6 +320,18 @@ class UnifiedHedger:
# Update with Profile Specifics
self.coin_configs[symbol].update(profile_data)
# 2. Force overwrite with TARGET_DEX profile to ensure precedence
target_profile = CLP_PROFILES.get(TARGET_DEX)
if target_profile:
symbol = target_profile.get("COIN_SYMBOL")
if symbol:
if symbol not in self.coin_configs:
self.coin_configs[symbol] = DEFAULT_STRATEGY.copy()
self.coin_configs[symbol]["sz_decimals"] = 4
logger.info(f"Overwriting config for {symbol} using TARGET_DEX: {TARGET_DEX}")
self.coin_configs[symbol].update(target_profile)
def _get_sz_decimals(self, coin: str) -> int:
try:
meta = self.info.meta()
@ -427,6 +462,7 @@ class UnifiedHedger:
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -478,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN')
}
# Initial hedge entry price is the CLP entry price
self.hedge_entry_prices[key] = entry_price
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
# Ensure JSON has these fields initialized
update_position_stats(key[0], key[1], {
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
})
except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -652,7 +699,7 @@ class UnifiedHedger:
price = to_decimal(mids[coin])
self.last_prices[coin] = price
# Update Price History
# Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
@ -663,19 +710,60 @@ class UnifiedHedger:
if coin not in self.last_prices: continue
price = self.last_prices[coin]
# Get Config & Strategy Type
config = self.coin_configs.get(coin, {})
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
# Calc Logic
calc = strat.calculate_rebalance(price, Decimal("0"))
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates:
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
if status == 'CLOSING':
# If Closing, we want target to be 0 for this strategy
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
is_active_hysteresis = self.emergency_close_active.get(key, False)
if is_active_hysteresis:
# CHECK RESET CONDITION
if strategy_type == "FIXED":
# Reset at 75% of range (from Bottom)
range_width = strat.high_range - strat.low_range
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
else:
reset_threshold = strat.high_range * Decimal("0.999")
if price < reset_threshold:
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
self.emergency_close_active[key] = False
is_active_hysteresis = False
# Capture NEW Dynamic Fixed Target and Entry Price
if strategy_type == "FIXED":
dynamic_delta = strat.get_pool_delta(price)
self.custom_fixed_targets[key] = dynamic_delta
self.hedge_entry_prices[key] = price
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
if not is_active_hysteresis:
# CHECK TRIGGER CONDITION
if price >= strat.high_range:
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
self.emergency_close_active[key] = True
is_active_hysteresis = True
# Reset entry price when closed
self.hedge_entry_prices[key] = Decimal("0")
if status == 'CLOSING' or is_active_hysteresis:
# If Closing OR Hysteresis Active, target is 0
aggregates[coin]['is_closing'] = True
# Do not add to target_short
else:
aggregates[coin]['target_short'] += calc['target_short']
# Use custom fixed target if exists, else standard calc
if strategy_type == "FIXED" and key in self.custom_fixed_targets:
aggregates[coin]['target_short'] += self.custom_fixed_targets[key]
else:
aggregates[coin]['target_short'] += calc['target_short']
aggregates[coin]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct']
@ -693,6 +781,8 @@ class UnifiedHedger:
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True
if dist_bottom_pct < safety_margin_pct:
aggregates[coin]['is_at_bottom_edge'] = True
# Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots)
@ -706,255 +796,256 @@ class UnifiedHedger:
for coin in coins_to_process:
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
price = self.last_prices.get(coin, Decimal("0")) # FIX: Explicitly get price for this coin
price = self.last_prices.get(coin, Decimal("0"))
if price == 0: continue
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short)
target_position = -target_short_abs # We want to be Short, so negative size
target_short_abs = data['target_short']
target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
diff = target_position - current_pos
diff_abs = abs(diff)
# Thresholds
config = self.coin_configs.get(coin, {})
min_thresh = config.get("min_threshold", Decimal("0.008"))
# Volatility Multiplier
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005")
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
# FORCE EDGE CLEANUP
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
if data['is_at_edge'] and enable_edge_cleanup:
if dynamic_thresh > min_thresh:
# logger.info(f"[EDGE] {coin} forced to min threshold.")
dynamic_thresh = min_thresh
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
# Check Trigger
action_needed = diff_abs > dynamic_thresh
# Determine Intent (Moved UP for Order Logic)
is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders
existing_orders = orders_map.get(coin, [])
force_taker_retry = False
# Fishing Config
enable_fishing = config.get("ENABLE_FISHING", False)
fishing_timeout = config.get("FISHING_TIMEOUT_FALLBACK", 30)
# Check Existing Orders for compatibility
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side'] # 'B' or 'A'
o_side = o['side']
o_timestamp = o.get('timestamp', int(time.time()*1000))
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
# Price Check (within buffer)
dist_pct = abs(price - o_price) / price
# Maker Timeout Check (General)
maker_timeout = config.get("MAKER_ORDER_TIMEOUT", 300)
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > maker_timeout:
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.")
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid)
continue
# Fishing Timeout Check
if enable_fishing and is_same_side and order_age_sec > fishing_timeout:
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
self.cancel_order(coin, o_oid)
force_taker_retry = True
continue # Do not mark matched, let it flow to execution
continue
if is_same_side and dist_pct < price_buffer_pct:
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
order_matched = True
if int(time.time()) % 10 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%) | Age: {order_age_sec:.1f}s")
if int(time.time()) % 15 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
# --- EXECUTION LOGIC ---
if not order_matched:
if action_needed or force_taker_retry:
# Determine Urgency / Bypass Cooldown
bypass_cooldown = False
force_maker = False
if not order_matched and (action_needed or force_taker_retry):
if force_taker_retry: bypass_cooldown = True
elif data.get('is_closing', False): bypass_cooldown = True
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5: force_maker = True
else: continue # Skip startup ghost positions
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
# Prevent IOC for BUYs at bottom edge
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
bypass_cooldown = True
# --- BOTTOM STRATEGY SAFEGUARDS ---
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
if strategy_type == "BOTTOM":
# strict: "do not use taker orders... except only on very bottom"
if not data.get('is_at_bottom_edge', False):
bypass_cooldown = False
force_maker = False
force_taker_retry = False # Disable taker retry from fishing
# 0. Forced Taker Retry (Fishing Timeout)
if force_taker_retry:
bypass_cooldown = True
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker")
# --- ASYMMETRIC HEDGE CHECK ---
is_asymmetric_blocked = False
p_mid_asym = Decimal("0")
# strategy_type already fetched above
# 1. Urgent Closing -> Taker
elif data.get('is_closing', False):
bypass_cooldown = True
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit")
if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
total_L_asym = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
# 2. Ghost/Cleanup -> Maker
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5:
force_maker = True
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce")
else:
logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)")
continue # Skip execution for this coin
# Large Hedge Check (Only Force Taker if AT EDGE)
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
bypass_cooldown = True
logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
# Large hedge but safe zone -> Maker is fine, but maybe log it
logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.")
gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
if gamma_asym > 0:
p_mid_asym = price - (diff_abs / gamma_asym)
if not data.get('is_at_edge', False) and price >= p_mid_asym:
is_asymmetric_blocked = True
# --- EXECUTION ---
if not order_matched and not is_asymmetric_blocked:
if action_needed or force_taker_retry:
last_trade = self.last_trade_times.get(coin, 0)
min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60)
min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60)
can_trade = False
if bypass_cooldown:
can_trade = True
elif time.time() - last_trade > min_time_trade:
can_trade = True
if can_trade:
# Get Orderbook for Price
if coin not in l2_snapshots:
l2_snapshots[coin] = self.info.l2_snapshot(coin)
if bypass_cooldown or (time.time() - last_trade > min_time):
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
levels = l2_snapshots[coin]['levels']
if not levels[0] or not levels[1]: continue
if levels[0] and levels[1]:
bid, ask = to_decimal(levels[0][0]['px']), to_decimal(levels[1][0]['px'])
if bypass_cooldown and not force_maker:
exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999")
order_type = "Ioc"
else:
exec_price = bid if is_buy_bool else ask
order_type = "Alo"
bid = to_decimal(levels[0][0]['px'])
ask = to_decimal(levels[1][0]['px'])
logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
if order_type == "Ioc":
shadow_price = bid if is_buy_bool else ask
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
# Price logic
create_shadow = False
logger.info("Sleeping 10s for position update...")
time.sleep(10)
self._update_closed_pnl(coin)
else:
# Idle Cleanup
if existing_orders and not order_matched:
for o in existing_orders: self.cancel_order(coin, o['oid'])
# Decide Order Type: Taker (Ioc) or Maker (Alo)
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo)
# --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
total_L_log = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_log += strat_inst.L
# Logic:
# If Force Maker -> Alo
# Else if Urgent -> Ioc
# Else if Enable Fishing -> Alo
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
# Update all active strategies for this coin in JSON
if total_L_log > 0 and price > 0:
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
# Let's clarify:
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo.
# New logic:
# If bypass_cooldown -> Ioc
# Else -> Alo (Fishing)
# CLP Value Calc
def get_clp_value(p, s):
if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
if bypass_cooldown and not force_maker:
exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999")
order_type = "Ioc"
create_shadow = True
else:
# Fishing / Standard Maker
exec_price = bid if is_buy_bool else ask
order_type = "Alo"
clp_curr_val = get_clp_value(price, strat_inst)
logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f} | Type: {order_type}")
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
# Shadow Order
if create_shadow:
shadow_price = bid if is_buy_bool else ask
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
self.shadow_orders.append({
'coin': coin,
'side': side_str,
'price': shadow_price,
'expires_at': time.time() + shadow_timeout
})
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
# UPDATED: Sleep for API Lag (Phase 5.1)
logger.info("Sleeping 10s to allow position update...")
time.sleep(10)
# --- UPDATE CLOSED PnL FROM API ---
self._update_closed_pnl(coin)
# USE TRACKED HEDGE ENTRY PRICE
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
else:
# Cooldown log
pass
hedge_pnl_curr = Decimal("0")
else:
# Action NOT needed
# Cleanup any dangling orders
if existing_orders:
for o in existing_orders:
logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})")
self.cancel_order(coin, o['oid'])
fee_close_curr = (target_size * price) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# --- IDLE LOGGING (Restored Format) ---
# Calculate aggregate Gamma to estimate triggers
# Gamma = 0.5 * Sum(L) * P^-1.5
# We need Sum(L) for this coin.
total_L = Decimal("0")
# We need to re-iterate or cache L.
# Simpler: Just re-sum L from active strats for this coin.
for key, strat in self.strategies.items():
if self.strategy_states[key]['coin'] == coin:
total_L += strat.L
# Retrieve Realized PnL & Fees from State
realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
if total_L > 0 and price > 0:
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5")))
if gamma > 0:
# Equilibrium Price (Diff = 0)
p_mid = price + (diff / gamma)
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
# Triggers
p_buy = price + (dynamic_thresh + diff) / gamma
p_sell = price - (dynamic_thresh - diff) / gamma
cur_hl_cost = realized_fees + fee_close_curr
if int(time.time()) % 30 == 0:
# Sync to JSON every 1s
update_position_stats(k_strat[0], k_strat[1], {
"combined_TotPnL": round(float(tot_curr), 2),
"hedge_HL_cost_est": round(float(cur_hl_cost), 2),
"hedge_pnl_unrealized": round(float(hedge_pnl_curr), 2),
"last_sync_hl": int(time.time())
})
# --- THROTTLED STATUS LOGGING (300s) ---
now = time.time()
last_log = self.last_idle_log_times.get(coin, 0)
log_interval = config.get("LOG_INTERVAL_SECONDS", 300)
if now - last_log >= log_interval:
self.last_idle_log_times[coin] = now
if is_asymmetric_blocked:
logger.info(f"[ASYMMETRIC] Blocking BUY. Px ({price:.2f}) >= Eq ({p_mid_asym:.2f}) & Not Edge")
if total_L_log > 0 and price > 0:
gamma_log = (Decimal("0.5") * total_L_log * (price ** Decimal("-1.5")))
if gamma_log > 0:
p_mid_log = price - (diff / gamma_log)
p_buy = price + (dynamic_thresh + diff) / gamma_log
p_sell = price - (dynamic_thresh - diff) / gamma_log
pad = " " if coin == "BNB" else ""
adj_val = data.get('adj_pct', Decimal("0")) * 100
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl_total = Decimal("0")
fees_total = Decimal("0")
for k, s_state in self.strategy_states.items():
if s_state['coin'] == coin:
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
total_pnl = (closed_pnl - fees) + unrealized
total_pnl = (closed_pnl_total - fees_total) + unrealized
# Log individual strategy PnL
if strategy_type == "FIXED":
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
pnl_pad = " " if unrealized >= 0 else ""
tot_pnl_pad = " " if total_pnl >= 0 else ""
# Recalculate for logging (including bounds)
clp_curr_val = get_clp_value(price, strat_inst)
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {adj_val:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f}{pnl_pad} | TotPnL: {total_pnl:.2f}{tot_pnl_pad}")
else:
if int(time.time()) % 30 == 0:
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
hedge_pnl_low = (h_entry_px - strat_inst.low_range) * target_size
hedge_pnl_high = (h_entry_px - strat_inst.high_range) * target_size
fee_open = (target_size * h_entry_px) * Decimal("0.000144")
else:
hedge_pnl_curr = hedge_pnl_low = hedge_pnl_high = Decimal("0")
fee_open = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
fee_close_low = (target_size * strat_inst.low_range) * Decimal("0.000432")
fee_close_high = (target_size * strat_inst.high_range) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr - (fee_open + fee_close_curr) + uni_fees
tot_low = (clp_low_val - strat_inst.target_value) + hedge_pnl_low - (fee_open + fee_close_low) + uni_fees
tot_high = (clp_high_val - strat_inst.target_value) + hedge_pnl_high - (fee_open + fee_close_high) + uni_fees
cur_hl_cost = fee_open + fee_close_curr
# ID or Range to distinguish
strat_id = str(k_strat[1]) # Token ID
logger.info(f"[FIXED] {coin} #{strat_id} | TotPnL: {tot_curr:+.2f} | Down: {tot_low:+.2f} | Up: {tot_high:+.2f} (Inc: Fees ${uni_fees:.2f}, HL Cost ${cur_hl_cost:.2f})")
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid_log:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {data.get('adj_pct',0)*100:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f} | HedgePnL: {total_pnl:.2f}")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
if int(time.time()) % 30 == 0:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))

View File

@ -63,65 +63,34 @@ formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# --- ABIs ---
# (Kept minimal for brevity, normally would load from files)
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
[
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
UNISWAP_V3_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
UNISWAP_V3_FACTORY_ABI = json.loads('''
[
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
SWAP_ROUTER_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
WETH9_ABI = json.loads('''
[
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
]
''')
from clp_abis import (
NONFUNGIBLE_POSITION_MANAGER_ABI,
UNISWAP_V3_POOL_ABI,
ERC20_ABI,
UNISWAP_V3_FACTORY_ABI,
AERODROME_FACTORY_ABI,
AERODROME_POOL_ABI,
AERODROME_NPM_ABI,
SWAP_ROUTER_ABI,
WETH9_ABI
)
from clp_config import get_current_config, STATUS_FILE
from tools.universal_swapper import execute_swap
# --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_config()
DEX_TO_CHAIN = {
"UNISWAP_V3": "ARBITRUM",
"UNISWAP_wide": "ARBITRUM",
"PANCAKESWAP_BNB": "BSC",
"WETH_CBBTC_BASE": "BASE",
"UNISWAP_BASE_CL": "BASE",
"AERODROME_BASE_CL": "BASE",
"AERODROME_WETH-USDC_008": "BASE"
}
# --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
@ -130,11 +99,77 @@ REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", Tr
TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01"))
RANGE_MODE = CONFIG.get("RANGE_MODE", "FIXED")
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
# --- AUTO RANGE HELPERS ---
def get_market_indicators() -> Optional[Dict]:
file_path = os.path.join("market_data", "indicators.json")
if not os.path.exists(file_path):
return None
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Check Freshness (5m)
last_updated_str = data.get("last_updated")
if not last_updated_str: return None
last_updated = datetime.fromisoformat(last_updated_str)
if (datetime.now() - last_updated).total_seconds() > 300:
logger.warning("⚠️ Market indicators file is stale (>5m).")
return None
return data
except Exception as e:
logger.error(f"Error reading indicators: {e}")
return None
def calculate_dynamic_range_pct(coin: str) -> Optional[Decimal]:
indicators = get_market_indicators()
if not indicators: return None
# Normalize symbols (Hyperliquid uses ETH, BNB while DEX uses WETH, WBNB)
symbol_map = {"WETH": "ETH", "WBNB": "BNB"}
lookup_coin = symbol_map.get(coin.upper(), coin.upper())
coin_data = indicators.get("data", {}).get(lookup_coin)
if not coin_data: return None
try:
price = Decimal(str(coin_data["current_price"]))
bb12 = coin_data["bb"]["12h"]
bb_low = Decimal(str(bb12["lower"]))
bb_high = Decimal(str(bb12["upper"]))
ma88 = Decimal(str(coin_data["ma"]["88"]))
# Condition 2: Price inside BB 12h
if not (bb_low <= price <= bb_high):
logger.warning(f"⚖️ AUTO: Price {price:.2f} is outside BB 12h ({bb_low:.2f} - {bb_high:.2f}). Skipping AUTO.")
return None
# Condition 3: MA 88 inside BB 12h
if not (bb_low <= ma88 <= bb_high):
logger.warning(f"⚖️ AUTO: MA 88 {ma88:.2f} is outside BB 12h. Skipping AUTO.")
return None
# Calculation: Max distance to BB edge
dist_low = abs(price - bb_low)
dist_high = abs(price - bb_high)
max_dist = max(dist_low, dist_high)
range_pct = max_dist / price
return range_pct
except (KeyError, TypeError, ValueError) as e:
logger.error(f"Error in dynamic range calc: {e}")
return None
# --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
# Router address not strictly needed for Manager if using universal_swapper, but kept for ref
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
@ -311,7 +346,8 @@ def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int
if pool_address == '0x0000000000000000000000000000000000000000':
return None, None
pool_contract = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
pool_contract = w3.eth.contract(address=pool_address, abi=pool_abi)
return {
"token0_address": token0_address, "token1_address": token1_address,
@ -397,6 +433,7 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount, token0: str, token1: str, amount0_needed: int, amount1_needed: int, sqrt_price_x96: int, d0: int, d1: int) -> bool:
"""
Checks balances, wraps ETH if needed, and swaps ONLY the required surplus to meet deposit requirements.
Uses universal_swapper for the swap execution.
"""
token0 = clean_address(token0)
token1 = clean_address(token1)
@ -444,12 +481,12 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
# Price of Token0 in terms of Token1
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
swap_call = None
token_in, token_out = None, None
amount_in = 0
chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
buffer_multiplier = Decimal("1.02") # 2% buffer for slippage/price moves
token_in_sym, token_out_sym = None, None
amount_in_float = 0.0
buffer_multiplier = Decimal("1.03")
if deficit0 > 0 and bal1 > amount1_needed:
# Need T0 (ETH), Have extra T1 (USDC)
# Swap T1 -> T0
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus1 = bal1 - amount1_needed
if surplus1 >= amount_in_needed:
token_in, token_out = token1, token0
amount_in = amount_in_needed
# Get Symbols
token_in_sym = token1_c.functions.symbol().call().upper()
token_out_sym = token0_c.functions.symbol().call().upper()
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d1))
logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
else:
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}")
@ -479,38 +519,46 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus0 = bal0 - amount0_needed
if surplus0 >= amount_in_needed:
token_in, token_out = token0, token1
amount_in = amount_in_needed
token_in_sym = token0_c.functions.symbol().call().upper()
token_out_sym = token1_c.functions.symbol().call().upper()
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d0))
logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
else:
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
if token_in and amount_in > 0:
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...")
if token_in_sym and amount_in_float > 0:
logger.info(f"🔄 Delegating Swap to Universal Swapper: {amount_in_float} {token_in_sym} -> {token_out_sym} on {chain_name}...")
try:
# Use Standard Fee (500) if configured fee is weird (like 1 for Aerodrome tickSpacing)
# This ensures the standard router finds a valid pool (WETH/USDC 0.05%)
swap_fee = POOL_FEE if POOL_FEE >= 100 else 500
if not ensure_allowance(w3, account, token_in, UNISWAP_V3_SWAP_ROUTER_ADDRESS, amount_in):
# Call Universal Swapper
execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_float, fee_tier=swap_fee)
# Wait for node indexing
logger.info("⏳ Waiting for balance update...")
time.sleep(2)
# Retry check loop
for i in range(3):
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
if bal0 >= amount0_needed and bal1 >= amount1_needed:
logger.info("✅ Balances sufficient.")
return True
if i < 2:
logger.info(f"⏳ Balance not updated yet, retrying ({i+1}/3)...")
time.sleep(2)
logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
params = (
token_in, token_out, POOL_FEE, account.address,
int(time.time()) + 120,
amount_in,
0, # amountOutMin (Market swap for rebalance)
0
)
receipt = send_transaction_robust(w3, account, router_contract.functions.exactInputSingle(params), extra_msg="Swap Surplus")
if receipt:
# Final check - Recursive check to ensure we hit target or retry
# But return True/False based on immediate check
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
# If we are strictly >= needed, great.
if bal0 >= amount0_needed and bal1 >= amount1_needed:
return True
else:
logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
except Exception as e:
logger.error(f"❌ Universal Swap Failed: {e}")
return False
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
@ -531,14 +579,20 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
# 3. Mint
params = (
base_params = [
token0, token1, POOL_FEE,
tick_lower, tick_upper,
amount0, amount1,
amount0_min, amount1_min,
account.address,
int(time.time()) + 180
)
]
# Aerodrome Slipstream expects sqrtPriceX96 as the last parameter
if "AERODROME" in os.environ.get("TARGET_DEX", "").upper():
base_params.append(0) # sqrtPriceX96
params = tuple(base_params)
receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position")
@ -696,9 +750,43 @@ def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}")
import argparse
import requests
# --- REAL-TIME ORACLE HELPER ---
def get_realtime_price(coin: str) -> Optional[Decimal]:
"""Fetches current mid-price directly from Hyperliquid API (low latency)."""
try:
url = "https://api.hyperliquid.xyz/info"
response = requests.post(url, json={"type": "allMids"}, timeout=2)
if response.status_code == 200:
data = response.json()
# Hyperliquid symbols are usually clean (ETH, BNB)
# Map common variations just in case
target = coin.upper().replace("WETH", "ETH").replace("WBNB", "BNB")
if target in data:
return Decimal(data[target])
except Exception as e:
logger.warning(f"⚠️ Failed to fetch realtime Oracle price: {e}")
return None
# --- MAIN LOOP ---
def main():
# --- ARGUMENT PARSING ---
parser = argparse.ArgumentParser(description="Uniswap CLP Manager")
parser.add_argument("--force", type=float, help="Force open a position with specific range width (e.g., 0.75), ignoring AUTO safe checks.")
args = parser.parse_args()
force_mode_active = False
force_width_pct = Decimal("0")
if args.force:
force_mode_active = True
force_width_pct = Decimal(str(args.force)) / 100 # Convert 0.75 -> 0.0075
logger.warning(f"🚨 FORCE MODE ACTIVE: Will bypass safe checks for FIRST position with width {args.force}%")
logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
load_dotenv(override=True)
@ -722,9 +810,23 @@ def main():
logger.info(f"👤 Wallet: {account.address}")
# Contracts
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
target_dex_name = os.environ.get("TARGET_DEX", "").upper()
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
logger.info("✈️ Using Aerodrome NPM ABI")
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=AERODROME_NPM_ABI)
else:
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
factory_addr = npm.functions.factory().call()
factory = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
# Select Factory ABI based on DEX type
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
logger.info("✈️ Using Aerodrome Factory ABI (tickSpacing instead of fee)")
factory_abi = AERODROME_FACTORY_ABI
else:
factory_abi = UNISWAP_V3_FACTORY_ABI
factory = w3.eth.contract(address=factory_addr, abi=factory_abi)
router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
while True:
@ -817,7 +919,21 @@ def main():
pnl_unrealized = current_pos_value_usd - initial_value
total_pnl_usd = pnl_unrealized + total_fees_usd
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})"
# --- PERSIST PERFORMANCE TO JSON ---
update_position_status(token_id, "OPEN", {
"clp_fees": round(float(total_fees_usd), 2),
"clp_TotPnL": round(float(total_pnl_usd), 2)
})
# Calculate Fees/h
fees_per_h_str = "0.00"
ts_open = active_auto_pos.get('timestamp_open')
if ts_open:
hours_open = (time.time() - ts_open) / 3600
if hours_open > 0.01:
fees_per_h_str = f"{float(total_fees_usd) / hours_open:.2f}"
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f} | ${fees_per_h_str}/h)"
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
# --- KPI LOGGING ---
@ -845,6 +961,32 @@ def main():
log_kpi_snapshot(snapshot)
# --- REPOSITION LOGIC ---
pos_range_mode = active_auto_pos.get("range_mode", RANGE_MODE)
if pos_range_mode == "AUTO" and CLOSE_POSITION_ENABLED:
coin_for_dynamic = pos_details['token0_symbol'] if not is_t0_stable else pos_details['token1_symbol']
new_range_width = calculate_dynamic_range_pct(coin_for_dynamic)
if new_range_width:
# Use initial width from JSON, or current config width as fallback
old_range_width = Decimal(str(active_auto_pos.get("range_width_initial", RANGE_WIDTH_PCT)))
# Condition A: Difference > 20%
width_diff_pct = abs(new_range_width - old_range_width) / old_range_width
# Condition B: Profit > 0.1%
profit_pct = total_pnl_usd / initial_value
logger.info(f"📊 AUTO Check: CurRange {old_range_width*100:.2f}%, NewRange {new_range_width*100:.2f}% | Diff {width_diff_pct*100:.1f}% | Profit {profit_pct*100:.2f}%")
if width_diff_pct > 0.20 and profit_pct > 0.001:
logger.warning(f"🔄 REPOSITION TRIGGERED: Width Diff {width_diff_pct*100:.1f}%, Profit {profit_pct*100:.2f}%")
# Set in_range to False to force the closing logic below
in_range = False
else:
logger.warning(f"⚖️ AUTO Check Skipped: Market indicators for {coin_for_dynamic} are stale or conditions not met.")
if not in_range and CLOSE_POSITION_ENABLED:
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
update_position_status(token_id, "CLOSING")
@ -875,14 +1017,73 @@ def main():
fee = POOL_FEE
pool_addr = factory.functions.getPool(token0, token1, fee).call()
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
pool_c = w3.eth.contract(address=pool_addr, abi=pool_abi)
pool_data = get_pool_dynamic_data(pool_c)
if pool_data:
tick = pool_data['tick']
# Define Range (+/- 2.5%)
# log(1.025) / log(1.0001) approx 247 tick delta
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001))
# --- PRE-CALCULATE ESSENTIALS ---
# Fetch Decimals & Symbols immediately (Required for Oracle Check)
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
d0 = t0_c.functions.decimals().call()
d1 = t1_c.functions.decimals().call()
t0_sym = t0_c.functions.symbol().call().upper()
t1_sym = t1_c.functions.symbol().call().upper()
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
is_t1_stable = any(s in t1_sym for s in stable_symbols)
is_t0_stable = any(s in t0_sym for s in stable_symbols)
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
# Define coin_sym early for Guard Rails
coin_sym = CONFIG.get("COIN_SYMBOL", "ETH")
# --- ORACLE GUARD RAIL ---
# Protect against Pool/Oracle divergence (Manipulation/Depeg/Lag)
if not force_mode_active:
oracle_price = get_realtime_price(coin_sym)
if oracle_price:
pool_price_dec = price_0_in_1 if is_t1_stable else (Decimal("1") / price_0_in_1)
divergence = abs(pool_price_dec - oracle_price) / oracle_price
if divergence > Decimal("0.0025"): # 0.25% Tolerance
logger.warning(f"⚠️ Price Divergence! Pool: {pool_price_dec:.2f} vs Oracle: {oracle_price:.2f} (Diff: {divergence*100:.2f}%). Aborting.")
time.sleep(10)
continue
else:
logger.warning("⚠️ Could not fetch Oracle price. Proceeding with caution (or consider aborting).")
# --- DYNAMIC RANGE CALCULATION ---
active_range_width = RANGE_WIDTH_PCT
current_range_mode = RANGE_MODE
# 1. PRIORITY: Force Mode
if force_mode_active:
logger.warning(f"🚨 FORCE OVERRIDE: Using forced width {force_width_pct*100:.2f}% (Ignoring safe checks)")
active_range_width = force_width_pct
current_range_mode = "FIXED"
# 2. AUTO Mode (Only if not forced)
elif RANGE_MODE == "AUTO":
dynamic_width = calculate_dynamic_range_pct(coin_sym)
if dynamic_width:
active_range_width = dynamic_width
logger.info(f"⚖️ AUTO Range Activated: {active_range_width*100:.4f}%")
else:
logger.info(f"⛔ AUTO conditions not met. Waiting for safe entry...")
time.sleep(MONITOR_INTERVAL_SECONDS)
continue # Skip logic
# 3. FIXED Mode (Default Fallback) is already set by initial active_range_width
# Define Range
tick_delta = int(math.log(1 + float(active_range_width)) / math.log(1.0001))
# Fetch actual tick spacing from pool
tick_spacing = pool_c.functions.tickSpacing().call()
@ -893,28 +1094,10 @@ def main():
# Calculate Amounts
# Target Value logic
d0 = 18 # Default WETH (Corrected below if needed, but we rely on raw logic)
# Actually, we should fetch decimals from contract to be safe, but config assumes standard.
# Fetch Decimals for precision
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
d0 = t0_c.functions.decimals().call()
d1 = t1_c.functions.decimals().call()
# Determine Investment Value in Token1 terms
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC))
# Check which is stable
t0_sym = t0_c.functions.symbol().call().upper()
t1_sym = t1_c.functions.symbol().call().upper()
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
is_t1_stable = any(s in t1_sym for s in stable_symbols)
is_t0_stable = any(s in t0_sym for s in stable_symbols)
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
investment_val_token1 = Decimal("0")
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX":
@ -939,10 +1122,33 @@ def main():
amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_token1, d0, d1, pool_data['sqrtPriceX96'])
if check_and_swap_for_deposit(w3, router, account, token0, token1, amt0, amt1, pool_data['sqrtPriceX96'], d0, d1):
# --- STALE DATA PROTECTION (Pre-Mint) ---
# Check if price moved significantly during calculation/swap
pre_mint_data = get_pool_dynamic_data(pool_c)
if pre_mint_data:
tick_diff = abs(pre_mint_data['tick'] - pool_data['tick'])
# 13 ticks ~ 0.13% price move. Abort if volatile.
if tick_diff > 13:
logger.warning(f"⚠️ Price moved too much ({tick_diff} ticks) during setup/swap. Aborting mint to prevent bad entry.")
time.sleep(5)
continue
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
if minted:
# Calculate entry price and amounts for JSON compatibility
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
# --- DISABLE FORCE MODE AFTER FIRST MINT ---
if force_mode_active:
logger.info("🛑 FORCE MODE CONSUMED: Returning to standard AUTO checks for future positions.")
force_mode_active = False
# --- RE-FETCH PRICE FOR ACCURATE ENTRY DATA (Post-Mint) ---
fresh_pool_data = get_pool_dynamic_data(pool_c)
if fresh_pool_data:
fresh_tick = fresh_pool_data['tick']
price_0_in_1 = price_from_tick(fresh_tick, d0, d1)
logger.info(f"🔄 Refreshed Entry Tick: {fresh_tick} (Was: {pool_data['tick']})")
else:
price_0_in_1 = price_from_tick(pool_data['tick'], d0, d1)
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
@ -970,6 +1176,8 @@ def main():
"range_lower": round(r_lower, 4),
"token0_decimals": d0,
"token1_decimals": d1,
"range_mode": current_range_mode,
"range_width_initial": float(active_range_width),
"timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
}

View File

@ -0,0 +1,103 @@
# Aerodrome Slipstream (CLP) Integration Guide
This document details the specific technical requirements for integrating **Aerodrome Slipstream** (Concentrated Liquidity) pools into a Uniswap V3-compatible bot. Aerodrome Slipstream is a fork of Uniswap V3 (via Velodrome V2) but introduces critical ABI and logic changes that cause standard implementations to fail.
## 1. Key Differences from Uniswap V3
| Feature | Standard Uniswap V3 | Aerodrome Slipstream |
| :--- | :--- | :--- |
| **Factory Pool Lookup** | `getPool(tokenA, tokenB, fee)` | `getPool(tokenA, tokenB, tickSpacing)` |
| **NPM Mint Parameter** | `uint24 fee` | `int24 tickSpacing` |
| **NPM Mint Struct** | `MintParams { ..., deadline }` | `MintParams { ..., deadline, sqrtPriceX96 }` |
| **Pool Identification** | Fee Tier (e.g., 500, 3000) | Tick Spacing (e.g., 1, 100) |
## 2. ABI Modifications
To interact with the Aerodrome `NonfungiblePositionManager` (NPM), you must use a modified ABI. The standard Uniswap V3 NPM ABI will result in revert errors during encoding.
### MintParams Struct
The `MintParams` struct in the `mint` function input must be defined as follows:
```json
{
"components": [
{"internalType": "address", "name": "token0", "type": "address"},
{"internalType": "address", "name": "token1", "type": "address"},
{"internalType": "int24", "name": "tickSpacing", "type": "int24"}, // CHANGED from uint24 fee
{"internalType": "int24", "name": "tickLower", "type": "int24"},
{"internalType": "int24", "name": "tickUpper", "type": "int24"},
{"internalType": "uint256", "name": "amount0Desired", "type": "uint256"},
{"internalType": "uint256", "name": "amount1Desired", "type": "uint256"},
{"internalType": "uint256", "name": "amount0Min", "type": "uint256"},
{"internalType": "uint256", "name": "amount1Min", "type": "uint256"},
{"internalType": "address", "name": "recipient", "type": "address"},
{"internalType": "uint256", "name": "deadline", "type": "uint256"},
{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"} // ADDED
],
"internalType": "struct INonfungiblePositionManager.MintParams",
"name": "params",
"type": "tuple"
}
```
## 3. Python Implementation Strategy
### A. Configuration
When defining the pool configuration, use the **Tick Spacing** value (e.g., 100) where you would normally put the Fee.
```python
"AERODROME_BASE_CL": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72", # Aerodrome NPM
"POOL_FEE": 100, # Actual TickSpacing (e.g., 100 for volatile, 1 for stable)
...
}
```
### B. Logic Changes (`clp_manager.py`)
1. **ABI Selection:** Dynamically switch between Standard and Aerodrome ABIs based on the target DEX.
2. **Parameter Construction:** When calling `mint`, check if the target is Aerodrome. If so, append `0` (for `sqrtPriceX96`) to the arguments tuple.
```python
# Pseudo-code for Mint Call
base_params = [
token0, token1,
tick_spacing, # Passed as int24
tick_lower, tick_upper,
amount0, amount1,
amount0_min, amount1_min,
recipient,
deadline
]
if is_aerodrome:
base_params.append(0) # sqrtPriceX96 must be present and 0 for existing pools
npm_contract.functions.mint(tuple(base_params)).transact(...)
```
### C. Swap Router
Aerodrome Slipstream's **SwapRouter** (`0xbe6D...`) uses the `SwapRouter01` ABI style (includes `deadline` in `ExactInputSingleParams`), whereas standard Uniswap V3 on Base often uses `SwapRouter02` (no deadline).
* **Tip:** For simplicity, you can use the **Standard Uniswap V3 Router** (`0x2626...`) on Base to swap tokens (WETH/USDC) even if you are providing liquidity on Aerodrome, provided the tokens are standard. This avoids ABI headaches with the Aerodrome Router if you only need simple swaps.
## 4. Troubleshooting Common Errors
* **`('execution reverted', 'no data')`**:
* **Cause 1:** Passing `fee` (uint24) instead of `tickSpacing` (int24).
* **Cause 2:** Missing `sqrtPriceX96` parameter in the struct.
* **Cause 3:** Tick range (`tickLower`, `tickUpper`) not aligned to the pool's `tickSpacing` (e.g., must be multiples of 100).
* **`BadFunctionCallOutput` / `InsufficientDataBytes`**:
* **Cause:** Using the wrong Contract Address for the chain (e.g., using Mainnet NPM address on Base).
* **Cause:** Calling `factory()` on a contract that doesn't have it (wrong ABI or Proxy).
## 5. Addresses (Base)
| Contract | Address |
| :--- | :--- |
| **Aerodrome NPM** | `0x827922686190790b37229fd06084350E74485b72` |
| **Aerodrome Factory** | `0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A` |
| **Uniswap V3 NPM** | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` |
| **WETH** | `0x4200000000000000000000000000000000000006` |
| **USDC** | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |

View File

@ -0,0 +1,66 @@
# Interpreting CLP Pool Data
This guide explains how to read and use the data generated by the **Pool Scanner** and **Analyzer** tools to select the most profitable liquidity pools.
## 1. The Data Pipeline
1. **Scanner (`pool_scanner.py`):** Runs continuously. Connects to the blockchain every 10 minutes and records the "Heartbeat" of each pool:
* **Tick:** The current price tick.
* **FeeGrowthGlobal:** A cumulative counter of all fees earned by the pool since inception.
2. **Analyzer (`analyze_pool_data.py`):** Runs on demand. It "replays" the history recorded by the scanner to simulate how a specific strategy (e.g., $10k investment, +/- 10% range) would have performed.
## 2. Reading the Analyzer Report
When you run `python tools/analyze_pool_data.py`, you get a table like this:
```text
=== POOL PERFORMANCE REPORT ===
Pool Duration Rebalances Final Equity (Est) ROI %
Uniswap V3 (Base) - WETH/USDC 1 days 2 10050.00 0.50
Aerodrome SlipStream (Base) - WETH/USDC 1 days 0 10010.00 0.10
```
### Key Metrics
* **Duration:** How long the scanner has been tracking this pool. Longer duration = more reliable data.
* **Rebalances:** How many times the price went **Out of Range** (+/- 10%) during this period.
* **Low is Good:** Means the price is stable relative to your range. Less work for the bot, fewer fees paid.
* **High is Bad:** Means the pool is volatile. You are paying frequent swap/gas fees to move your range.
* **Final Equity (Est):** Your simulated $10,000 starting capital after:
* (+) Adding estimated Fee Income.
* (-) Subtracting Rebalance Costs (0.1% per rebalance).
* (+/-) Asset Value Change (Impermanent Loss is inherently captured because we track value in USD).
* **ROI %:** The return on investment for the duration.
* `0.50%` in 1 day approx `180%` APR (compounded).
## 3. Selecting a Pool
Use the report to find the "Sweet Spot":
| Scenario | Verdict |
| :--- | :--- |
| **High Fees, Low Rebalances** | **🥇 BEST.** The ideal pool. Price stays in range, volume is high. |
| **High Fees, High Rebalances** | **⚠️ RISKY.** You earn a lot, but you burn a lot on swaps/gas. Net profit might be lower than expected. |
| **Low Fees, Low Rebalances** | **😴 SAFE.** Good for "set and forget," but returns are meager. |
| **Low Fees, High Rebalances** | **❌ AVOID.** You will lose money rebalancing a chop-heavy pool with no volume. |
## 4. Advanced: Raw Data (`pool_history.csv`)
If you open the CSV directly, you will see columns like `feeGrowthGlobal0X128`.
* **Fee Growth:** This number ONLY goes up.
* **Speed of Growth:** The faster this number increases, the higher the trading volume (and APR) of the pool.
* **Tick:**
* `Price = 1.0001 ^ Tick`
* Stable Tick = Low Volatility.
## 5. Simulation Logic
The Analyzer assumes:
1. **Initial Investment:** $10,000 USD.
2. **Strategy:** Active Management (Auto-Rebalance).
3. **Range:** +/- 10% (Configurable in script).
4. **Cost:** 0.1% of capital per rebalance (Swap fee + Gas estimate).
5. **Fee Accrual:** You ONLY earn fees when the recorded tick is inside your virtual range.
*Note: This is a "Paper Trading" simulation. Real-world slippage and exact execution timing may vary.*

View File

@ -0,0 +1,93 @@
# Security & Optimization Protocols
This document details the safety mechanisms, entry guardrails, and optimization strategies implemented in the Uniswap Auto CLP & Hedger system.
## 1. CLP Manager: Entry & Position Safety
The `clp_manager.py` module is responsible for opening and managing Concentrated Liquidity Positions (CLP). It implements strict checks to ensure positions are only opened under favorable conditions.
### A. Oracle Guard Rail (Anti-Manipulation)
**Goal:** Prevent entering a pool that is actively being manipulated (Flash Loans) or has momentarily de-pegged.
* **Mechanism:** Before any calculation, the bot fetches the **Real-Time Mid Price** directly from the Hyperliquid API.
* **Logic:**
* `Pool Price` is calculated from the on-chain `sqrtPriceX96`.
* `Oracle Price` is fetched from Hyperliquid (`https://api.hyperliquid.xyz/info`).
* **Rule:** If `abs(Pool - Oracle) / Oracle > 0.25%`, the bot **ABORTS** the entry.
* **Benefit:** Protects against entering at a "fake" price which would lead to instant arbitrage loss (LVR).
### B. Stale Tick Protection (Volatility Guard)
**Goal:** Prevent entering a position if the price moves significantly during the transaction setup phase (e.g., while swapping tokens).
* **Mechanism:**
1. The bot calculates the target tick range at $T_0$.
2. It performs necessary token swaps (e.g., USDC -> WETH) which takes time ($T_1$).
3. **Critical Check:** Just before minting ($T_2$), it re-fetches the current pool tick.
* **Rule:** If `abs(Tick_T0 - Tick_T2) > 13 ticks` (approx 0.13%), the bot **ABORTS** the mint transaction.
* **Benefit:** Ensures the range is centered on the *actual* execution price, not an outdated one.
### C. Post-Mint Accuracy
**Goal:** Ensure the "Entry Price" recorded for the Hedger is 100% accurate.
* **Mechanism:** Immediately after the Mint transaction is confirmed on-chain, the bot fetches the pool state *one more time*.
* **Benefit:** Captures the exact price impact of the user's own liquidity insertion, preventing discrepancies in the Hedger's PnL calculations.
### D. Safe Entry Zones (AUTO Mode)
**Goal:** Only enter when mean reversion is statistically likely.
* **Mechanism:**
* **Bollinger Bands (BB):** Price must be inside the 12h BB.
* **Moving Average (MA):** The MA88 must also be inside the 12h BB.
* **Benefit:** Avoids opening positions during breakout trends or extreme volatility expansion.
### E. Manual Override (Force Mode)
**Goal:** Allow operator intervention for testing or recovery.
* **Command:** `python clp_manager.py --force <width>` (e.g., 0.95).
* **Behavior:** Bypasses Oracle, BB, and MA checks for the **first** position only. Automatically disables itself after one successful mint.
---
## 2. CLP Hedger: Risk Management
The `clp_hedger.py` module manages the Delta-Neutral hedge on Hyperliquid.
### A. Emergency Edge Closure (Stop-Loss)
**Goal:** Prevent indefinite hedging losses if the price breaks out of the LP range violently.
* **Trigger:** If `Price >= Range Upper Bound`.
* **Action:** Immediately **CLOSE** the short hedge position.
* **Benefit:** Stops the strategy from "selling low and buying high" (hedging) when the LP position is already out of range and effectively essentially 100% stablecoin (impermanent loss realized).
### B. Hysteresis Reset
**Goal:** Prevent "whipsaw" losses (opening/closing repeatedly) if the price hovers exactly at the edge.
* **Logic:** Once an Emergency Closure triggers, the hedge does **not** re-open immediately if the price dips slightly back in.
* **Reset Condition:** Price must drop back to a "Safe Zone" (e.g., 75% of the range width or a specific buffer below the edge).
* **Benefit:** Filters out noise at the range boundaries.
### C. Asymmetric Compensation (EAC)
**Goal:** Reduce the "Buy High/Sell Low" churn near the edges of the range.
* **Mechanism:** The target hedge delta is adjusted (reduced) as the price approaches the boundaries.
* **Logic:** `Target Hedge = Pool Delta * (1 - Proximity_Factor)`.
* **Benefit:** Softens the impact of entering/exiting the range, preserving capital.
### D. Fishing Orders (Maker Rebates)
**Goal:** Reduce execution costs.
* **Mechanism:** Instead of market dumping (Taker fee ~0.035%), the bot places Limit Orders (Maker) slightly away from the spread.
* **Logic:** If the price moves favorably, the order fills, earning a rebate (or paying 0 fee). If not filled within `TIMEOUT`, it falls back to a Taker order if the delta drift is critical.
---
## 3. Future Improvements (Roadmap)
### A. WebSocket Integration
* **Current:** Polling REST API every 30-300s (or 1s for guard rails).
* **Upgrade:** Implement a persistent WebSocket connection to Hyperliquid and the EVM RPC.
* **Benefit:** Sub-100ms reaction times to volatility events.
### B. Multi-Chain Arbitrage Check
* **Current:** Checks Hyperliquid vs Pool.
* **Upgrade:** Check Binance/Coinbase prices.
* **Benefit:** Detects on-chain lag before it happens (CEX leads DEX).
### C. Dynamic Fee Optimization
* **Current:** Fixed `POOL_FEE`.
* **Upgrade:** Automatically switch between 0.05% and 0.01% pools based on volatility and volume metrics.
### D. Smart Rebalance (Inventory Management)
* **Current:** Swaps surplus tokens using 1inch/Universal Router.
* **Upgrade:** Use CowSwap or CoW intents for MEV-protected rebalancing of large inventory imbalances.

View File

@ -0,0 +1,49 @@
import csv
import sys
import os
def analyze():
current_dir = os.path.dirname(os.path.abspath(__file__))
results_file = os.path.join(current_dir, "optimization_results.csv")
if not os.path.exists(results_file):
print(f"File not found: {results_file}")
return
print(f"Analyzing {results_file}...")
best_pnl = -float('inf')
best_config = None
rows = []
with open(results_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
rows.append(row)
pnl = float(row['TOTAL_PNL'])
uni_fees = float(row['UNI_FEES'])
hl_pnl = float(row['HL_PNL'])
print(f"Config: Range={row['RANGE_WIDTH_PCT']}, Thresh={row['BASE_REBALANCE_THRESHOLD_PCT']} | PnL: ${pnl:.2f} (Fees: ${uni_fees:.2f}, Hedge: ${hl_pnl:.2f})")
if pnl > best_pnl:
best_pnl = pnl
best_config = row
print("\n" + "="*40)
print(f"🏆 BEST CONFIGURATION")
print("="*40)
if best_config:
print(f"Range Width: {float(best_config['RANGE_WIDTH_PCT'])*100:.2f}%")
print(f"Rebalance Thresh: {float(best_config['BASE_REBALANCE_THRESHOLD_PCT'])*100:.0f}%")
print(f"Total PnL: ${float(best_config['TOTAL_PNL']):.2f}")
print(f" > Uni Fees: ${float(best_config['UNI_FEES']):.2f}")
print(f" > Hedge PnL: ${float(best_config['HL_PNL']):.2f}")
else:
print("No valid results found.")
if __name__ == "__main__":
analyze()

View File

@ -0,0 +1,312 @@
import sys
import os
import csv
import time
import json
import logging
from decimal import Decimal
from unittest.mock import MagicMock, patch
# Add project root to path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
sys.path.append(project_root)
from tests.backtest.mocks import MockExchangeState, MockWeb3, MockExchangeAPI, MockInfo, MockContract
# Setup Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(name)s] %(message)s')
logger = logging.getLogger("BACKTESTER")
class Backtester:
def __init__(self, book_file, trades_file, config_overrides=None):
self.book_file = book_file
self.trades_file = trades_file
self.config_overrides = config_overrides or {}
self.events = []
self.state = MockExchangeState()
# Mocks
self.mock_web3 = MockWeb3(self.state)
self.mock_hl_api = MockExchangeAPI(self.state)
self.mock_hl_info = MockInfo(self.state)
# Components (Lazy loaded)
self.manager = None
self.hedger = None
def load_data(self):
logger.info("Loading Market Data...")
# Load Book
with open(self.book_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
self.events.append({
"type": "BOOK",
"ts": int(row['timestamp_ms']),
"data": row
})
# Load Trades
# (Optional: Trades are useful for market impact, but for basic PnL tracking
# based on mid-price, Book is sufficient. Loading trades just to advance time)
with open(self.trades_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
self.events.append({
"type": "TRADE",
"ts": int(row['timestamp_ms']),
"data": row
})
# Sort by Timestamp
self.events.sort(key=lambda x: x['ts'])
logger.info(f"Loaded {len(self.events)} events.")
def patch_and_init(self):
logger.info("Initializing Logic...")
# --- PATCH MANAGER ---
# We need to patch clp_manager.Web3 to return our MockWeb3
# And os.environ for config
with patch.dict(os.environ, {
"TARGET_DEX": "PANCAKESWAP_BNB", # Example
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
"BNB_RPC_URL": "http://mock",
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
"MAIN_WALLET_ADDRESS": "0xMyWallet"
}):
import clp_manager
import clp_hedger
# Apply Config Overrides
if self.config_overrides:
logger.info(f"Applying Config Overrides: {self.config_overrides}")
for k, v in self.config_overrides.items():
# Patch Manager
if hasattr(clp_manager, k):
setattr(clp_manager, k, v)
# Patch Hedger
if hasattr(clp_hedger, k):
setattr(clp_hedger, k, v)
# 1. Init Manager
# clp_manager.main() connects to Web3. We need to inject our mock.
# Since clp_manager creates w3 inside main(), we can't inject easily without patching Web3 class.
self.manager_module = clp_manager
self.hedger_module = clp_hedger
def run(self):
self.load_data()
self.patch_and_init()
# MOCK TIME
start_time = self.events[0]['ts'] / 1000.0
# STATUS FILE MOCK
self.status_memory = [] # List[Dict]
def mock_load_status():
logger.info(f"MOCK LOAD STATUS: Found {len(self.status_memory)} items")
return self.status_memory
def mock_save_status(data):
logger.info(f"MOCK SAVE STATUS: Saving {len(data)} items")
self.status_memory = data
def mock_hedger_scan():
return []
# We need to globally patch time.time and the Libraries
web3_class_mock = MagicMock(return_value=self.mock_web3)
web3_class_mock.to_wei = self.mock_web3.to_wei
web3_class_mock.from_wei = self.mock_web3.from_wei
web3_class_mock.is_address = self.mock_web3.is_address
web3_class_mock.to_checksum_address = lambda x: x
# Mock Web3.keccak to return correct topics
def mock_keccak(text=None, hexstr=None):
# Known Topics
if text == "Transfer(address,address,uint256)":
return bytes.fromhex("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
if text == "IncreaseLiquidity(uint256,uint128,uint256,uint256)":
return bytes.fromhex("7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde")
if text == "DecreaseLiquidity(uint256,uint128,uint256,uint256)":
# 0x26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4
return bytes.fromhex("26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4")
if text == "Collect(uint256,address,uint256,uint256)":
# 0x70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d
return bytes.fromhex("70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d")
return b'\x00'*32
web3_class_mock.keccak = MagicMock(side_effect=mock_keccak)
# Ensure environment is patched during the whole run
env_patch = {
"TARGET_DEX": "PANCAKESWAP_BNB",
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
"BNB_RPC_URL": "http://mock",
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
"MAIN_WALLET_ADDRESS": "0xMyWallet"
}
with patch.dict(os.environ, env_patch), \
patch('time.time', side_effect=lambda: self.state.current_time_ms / 1000.0), \
patch('clp_manager.Web3', web3_class_mock), \
patch('clp_hedger.Account.from_key', return_value=MagicMock(address="0xMyWallet")), \
patch('clp_hedger.Exchange', return_value=self.mock_hl_api), \
patch('clp_hedger.Info', return_value=self.mock_hl_info), \
patch('clp_manager.load_status_data', side_effect=mock_load_status), \
patch('clp_manager.save_status_data', side_effect=mock_save_status), \
patch('clp_manager.clean_address', side_effect=lambda x: x), \
patch('clp_hedger.glob.glob', return_value=[]):
# Initialize Hedger (It creates the classes in __init__)
self.hedger = self.hedger_module.UnifiedHedger()
# Initialize Manager Components manually (simulate main setup)
w3 = self.mock_web3
account = MagicMock(address="0xMyWallet")
npm = w3.eth.contract("0xNPM", [])
factory = w3.eth.contract("0xFactory", [])
router = w3.eth.contract("0xRouter", [])
# --- SIMULATION LOOP ---
last_manager_tick = 0
manager_interval = 60 * 1000
trade_count = len([e for e in self.events if e['type'] == "TRADE"])
book_count = len([e for e in self.events if e['type'] == "BOOK"])
logger.info(f"SIMULATION START: {len(self.events)} total events ({book_count} BOOK, {trade_count} TRADE)")
for event in self.events:
self.state.current_time_ms = event['ts']
# Update Market
if event['type'] == "BOOK":
row = event['data']
mid = Decimal(row['mid_price'])
self.state.update_price("BNB", mid)
if event['type'] == "TRADE":
self.state.process_trade(event['data'])
# Run Logic
# 1. Manager (Every X seconds)
if self.state.current_time_ms - last_manager_tick > manager_interval:
self.manager_module.run_tick(w3, account, npm, factory, router)
last_manager_tick = self.state.current_time_ms
# SYNC MANAGER STATUS TO HEDGER
for pos in self.status_memory:
if pos.get('status') == 'OPEN' and pos.get('type') == 'AUTOMATIC':
key = ("MOCK", pos['token_id'])
if key not in self.hedger.strategies:
self.hedger._init_single_strategy(key, pos, "BNB")
else:
self.hedger.strategy_states[key]['status'] = pos.get('status', 'OPEN')
elif pos.get('status') == 'CLOSED':
key = ("MOCK", pos['token_id'])
if key in self.hedger.strategies:
self.hedger.strategy_states[key]['status'] = 'CLOSED'
# 2. Hedger (Every Tick/Event)
self.hedger.run_tick()
# Finalize: Collect accrued fees from open positions
logger.info(f"Finalizing... Checking {len(self.state.uni_positions)} open positions.")
for token_id, pos in self.state.uni_positions.items():
raw_owed0 = pos.get('tokensOwed0', 0)
logger.info(f"DEBUG: Position {token_id} Raw TokensOwed0: {raw_owed0}")
owed0 = Decimal(raw_owed0) / Decimal(10**18)
owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18)
# Convert to USD
price = self.state.prices.get("BNB", Decimal("0"))
# Fee0 is USDT (USD), Fee1 is WBNB
usd_val = owed0 + (owed1 * price)
if usd_val > 0:
self.state.uni_fees_collected += usd_val
logger.info(f"Finalizing Open Position {token_id}: Accrued Fees ${usd_val:.2f}")
logger.info("Backtest Complete.")
logger.info(f"Final Uni Fees: {self.state.uni_fees_collected}")
logger.info(f"Final HL PnL: {self.state.hl_realized_pnl - self.state.hl_fees_paid}")
def calculate_final_nav(self):
"""Calculates total Net Asset Value (USD) at the end of simulation."""
total_usd = Decimal("0")
# 1. Wallet Balances
# We assume T0=USDT, T1=WBNB for this profile
price = self.state.prices.get("BNB", Decimal("0"))
for sym, bal in self.state.wallet_balances.items():
if sym in ["USDC", "USDT"]:
total_usd += bal
elif sym in ["BNB", "WBNB", "NATIVE"]:
total_usd += bal * price
elif sym in ["ETH", "WETH"]:
# If ETH price available? We mocked update_price("BNB") only.
# Assuming ETH price static or 0 if not tracked
eth_price = self.state.prices.get("ETH", Decimal("0"))
total_usd += bal * eth_price
# 2. Uniswap Positions (Liquidity Value)
# Value = Amount0 * Price0 + Amount1 * Price1
# We need to calculate amounts from liquidity & current price
import math
# Helper to get amounts from liquidity
def get_amounts(liquidity, sqrt_price_x96, tick_lower, tick_upper):
# Simplified: Use the amounts we stored at mint time?
# No, that's initial. We need current value.
# But calculating precise amounts from liquidity/sqrtPrice requires complex math.
# For approximation, we can look at what the manager logged as "Deposited"
# if price hasn't moved much, or implement full liquidity math.
# Since implementing full math here is complex, let's use a simplified approach:
# If we are in range, we have a mix.
# If out of range, we have 100% of one token.
# Better: The Mock 'mint' stored initial amounts.
# We can adjust by price ratio? No, IL is non-linear.
# Let's use the 'decrease_liquidity' logic mock if available?
# Or just assume Liquidity Value = Initial Value + PnL (Fees) - IL.
# For this MVP, let's just count the Fees collected (Realized) + Initial Capital (Wallet).
# BUT we spent wallet funds to open LP.
# So Wallet is LOW. LP has Value.
# We MUST value the LP.
# Let's approximate:
# Value = Liquidity / (something) ...
# Actually, `clp_manager.py` calculates `actual_value` on entry.
# We can track `entry_value` in the position state.
return Decimal("0") # Placeholder if we can't calc easily
# 3. Hyperliquid Positions (Unrealized PnL + Margin)
hl_equity = self.state.hl_balances.get("USDC", 0) # Margin
for sym, pos in self.state.hl_positions.items():
hl_equity += pos['unrealized_pnl']
total_usd += hl_equity
return total_usd
if __name__ == "__main__":
# Example usage:
# python tests/backtest/backtester.py market_data/BNB_raw_20251230_book.csv market_data/BNB_raw_20251230_trades.csv
if len(sys.argv) < 3:
print("Usage: python backtester.py <book_csv> <trades_csv>")
else:
bt = Backtester(sys.argv[1], sys.argv[2])
bt.run()

View File

@ -0,0 +1,82 @@
import sys
import os
import csv
import itertools
from decimal import Decimal
# Add project root to path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
sys.path.append(project_root)
from tests.backtest.backtester import Backtester
def main():
# Grid Parameters
# We want to optimize:
# 1. RANGE_WIDTH_PCT: How wide is the LP position? (e.g. 0.01 = +/-1%, 0.05 = +/-5%)
# 2. BASE_REBALANCE_THRESHOLD_PCT: When do we hedge? (e.g. 0.10 = 10% delta drift, 0.20 = 20%)
param_grid = {
"RANGE_WIDTH_PCT": [0.005, 0.01, 0.025, 0.05],
"BASE_REBALANCE_THRESHOLD_PCT": [0.01, 0.05]
}
keys, values = zip(*param_grid.items())
combinations = [dict(zip(keys, v)) for v in itertools.product(*values)]
results = []
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
print(f"Starting Grid Search with {len(combinations)} combinations...")
for idx, config in enumerate(combinations):
print(f"\n--- Run {idx+1}/{len(combinations)}: {config} ---")
# Initialize Backtester with overrides
bt = Backtester(book_file, trades_file, config_overrides=config)
try:
bt.run()
# Collect Metrics
uni_fees = bt.state.uni_fees_collected
hl_realized = bt.state.hl_realized_pnl - bt.state.hl_fees_paid
# HL Unrealized
hl_unrealized = sum(p['unrealized_pnl'] for p in bt.state.hl_positions.values())
# Total PnL (Yield + Hedge Result) - Ignoring IL for now (Mock limitation)
total_pnl = uni_fees + hl_realized + hl_unrealized
result = {
**config,
"UNI_FEES": float(uni_fees),
"HL_REALIZED": float(hl_realized),
"HL_UNREALIZED": float(hl_unrealized),
"TOTAL_PNL": float(total_pnl)
}
results.append(result)
print(f"Result: {result}")
except Exception as e:
print(f"Run failed: {e}")
import traceback
traceback.print_exc()
# Save Results
out_file = os.path.join(current_dir, "optimization_results.csv")
keys = list(combinations[0].keys()) + ["UNI_FEES", "HL_REALIZED", "HL_UNREALIZED", "TOTAL_PNL"]
with open(out_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(results)
print(f"\nGrid Search Complete. Results saved to {out_file}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,607 @@
import time
import logging
from decimal import Decimal
from typing import Dict, List, Any, Optional
from hexbytes import HexBytes
logger = logging.getLogger("BACKTEST_MOCK")
class MockExchangeState:
"""
Central source of truth for the simulation.
Acts as the "Blockchain" and the "CEX Engine".
"""
def __init__(self):
self.current_time_ms = 0
self.prices = {} # symbol -> price (Decimal)
self.ticks = {} # symbol -> current tick (int)
# Balances
self.wallet_balances = {
"NATIVE": Decimal("100.0"), # ETH or BNB
"ETH": Decimal("100.0"),
"USDC": Decimal("100000.0"),
"WETH": Decimal("100.0"),
"BNB": Decimal("100.0"),
"WBNB": Decimal("100.0"),
"USDT": Decimal("100000.0")
}
self.hl_balances = {"USDC": Decimal("10000.0"), "USDT": Decimal("10000.0")}
self.hl_positions = {} # symbol -> {size, entry_px, unrealized_pnl}
# Uniswap Positions
self.next_token_id = 1000
self.uni_positions = {} # token_id -> {liquidity, tickLower, tickUpper, ...}
# Hyperliquid Orders
self.hl_orders = [] # List of {oid, coin, side, limitPx, sz, timestamp}
self.next_oid = 1
# Fees/PnL Tracking
self.uni_fees_collected = Decimal("0.0")
self.hl_fees_paid = Decimal("0.0")
self.hl_realized_pnl = Decimal("0.0")
# Pending TXs (Simulating Mempool/Execution)
self.pending_txs = []
def update_price(self, symbol: str, price: Decimal, tick: int = 0):
self.prices[symbol] = price
if tick:
self.ticks[symbol] = tick
# Update PnL for open positions
if symbol in self.hl_positions:
pos = self.hl_positions[symbol]
size = pos['size']
if size != 0:
# Long: (Price - Entry) * Size
# Short: (Entry - Price) * abs(Size)
if size > 0:
pos['unrealized_pnl'] = (price - pos['entry_px']) * size
else:
pos['unrealized_pnl'] = (pos['entry_px'] - price) * abs(size)
def process_trade(self, trade_data):
"""Simulate fee accumulation from market trades."""
# trade_data: {price, size, ...} from CSV
try:
# DEBUG: Confirm entry
if getattr(self, '_debug_trade_entry', False) is False:
logger.info(f"DEBUG: Processing Trades... Positions: {len(self.uni_positions)}")
self._debug_trade_entry = True
price = Decimal(trade_data['price'])
size = Decimal(trade_data['size']) # Amount in Base Token (BNB)
# Simple Fee Logic:
# If trade price is within a position's range, it earns fees.
# Fee = Volume * 0.05% (Fee Tier) * MarketShare (Assume 10%)
fee_tier = Decimal("0.0005") # 0.05%
# Realistic Market Share Simulation
# Assume Pool Depth in active ticks is $5,000,000
# Our Position is approx $1,000
# Share = 1,000 / 5,000,000 = 0.0002 (0.02%)
market_share = Decimal("0.0002")
import math
# Current Tick of the trade
try:
# price = 1.0001^tick -> tick = log(price) / log(1.0001)
# Note: If T0=USDT, Price T0/T1 = 1/Price_USD.
# But our TickLower/Upper in Mock are generated based on T0=USDT logic?
# clp_manager calculates ticks based on price_from_tick logic.
# If T0=USDT, T1=WBNB. Price (T0/T1) ~ 0.00116.
# Ticks will be negative.
# Trade Price is 860.
# We need to invert price to get tick if the pool is inverted.
# For BNB tests, we know T0=USDT.
# Invert price for tick calc
inv_price = Decimal("1") / price
tick = int(math.log(float(inv_price)) / math.log(1.0001))
except:
tick = 0
# Iterate all OPEN Uniswap positions
for token_id, pos in self.uni_positions.items():
# Check Range
if pos['tickLower'] <= tick <= pos['tickUpper']:
vol_usd = price * size
fee_earned = vol_usd * fee_tier * market_share
pos['tokensOwed0'] = pos.get('tokensOwed0', 0) + int(fee_earned * 10**18)
# Debug logging (Disabled for production runs)
# if getattr(self, '_debug_fee_log_count', 0) < 10:
# logger.info(f"DEBUG: Fee Earned! Tick {tick} inside {pos['tickLower']} <-> {pos['tickUpper']}. Fee: {fee_earned}")
# self._debug_fee_log_count = getattr(self, '_debug_fee_log_count', 0) + 1
else:
# Debug logging (Disabled)
# if getattr(self, '_debug_tick_log_count', 0) < 10:
# logger.info(f"DEBUG: Trade Tick {tick} OUTSIDE {pos['tickLower']} <-> {pos['tickUpper']} (Price: {price})")
# self._debug_tick_log_count = getattr(self, '_debug_tick_log_count', 0) + 1
pass
except Exception as e:
logger.error(f"Error processing trade: {e}")
def process_transaction(self, tx_data):
"""Executes a pending transaction and updates state."""
func = tx_data['func']
args = tx_data['args']
value = tx_data.get('value', 0)
contract_addr = tx_data['contract']
logger.info(f"PROCESSING TX: {func} on {contract_addr} Val: {value}")
# 1. DEPOSIT (Wrap)
if func == "deposit":
# Wrap Native -> Wrapped
# Assume contract_addr is the wrapped token
# In mocks, we map address to symbol
# But we don't have the instance here easily, so we guess.
# If value > 0, it's a wrap.
amount = Decimal(value) / Decimal(10**18)
if self.wallet_balances.get("NATIVE", 0) >= amount:
self.wallet_balances["NATIVE"] -= amount
# Find which token this is.
# If it's the WETH/WBNB address
target_token = "WBNB" # Default assumption for this test profile
if contract_addr == "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": target_token = "WETH"
self.wallet_balances[target_token] = self.wallet_balances.get(target_token, 0) + amount
logger.info(f"Wrapped {amount} NATIVE to {target_token}")
else:
logger.error("Insufficient NATIVE balance for wrap")
# 2. MINT (New Position)
elif func == "mint":
# Params: (token0, token1, fee, tickLower, tickUpper, amount0Desired, amount1Desired, ...)
params = args[0]
token0 = params[0]
token1 = params[1]
amount0 = Decimal(params[5]) / Decimal(10**18) # Approx decimals
amount1 = Decimal(params[6]) / Decimal(10**18)
logger.info(f"Minting Position: {amount0} T0, {amount1} T1")
# Deduct Balances (Simplified: assuming we have enough)
# In real mock we should check symbols
self.wallet_balances["WBNB"] = max(0, self.wallet_balances.get("WBNB", 0) - amount0)
self.wallet_balances["USDT"] = max(0, self.wallet_balances.get("USDT", 0) - amount1)
# Create Position
token_id = self.next_token_id
self.next_token_id += 1
self.uni_positions[token_id] = {
"token0": token0,
"token1": token1,
"tickLower": params[3],
"tickUpper": params[4],
"liquidity": 1000000 # Dummy liquidity
}
logger.info(f"Minted TokenID: {token_id}")
self.last_minted_token_id = token_id
return token_id # Helper return
# 3. COLLECT (Fees)
elif func == "collect":
# Params: (params) -> (tokenId, recipient, amount0Max, amount1Max)
params = args[0]
token_id = params[0]
# Retrieve accumulated fees
if token_id in self.uni_positions:
pos = self.uni_positions[token_id]
owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18)
owed0 = Decimal(pos.get('tokensOwed0', 0)) / Decimal(10**18)
# Reset
pos['tokensOwed1'] = 0
pos['tokensOwed0'] = 0
fee0 = owed0
fee1 = owed1
else:
fee0 = Decimal("0")
fee1 = Decimal("0")
self.wallet_balances["WBNB"] = self.wallet_balances.get("WBNB", 0) + fee0
self.wallet_balances["USDT"] = self.wallet_balances.get("USDT", 0) + fee1
# Calculate USD Value of fees
# T0 = USDT, T1 = WBNB
# fee0 is USDT, fee1 is WBNB
price = self.state.prices.get("BNB", Decimal("0"))
usd_val = fee0 + (fee1 * price)
self.uni_fees_collected += usd_val
logger.info(f"Collected Fees for {token_id}: {fee0:.4f} T0 + {fee1:.4f} T1 = ${usd_val:.2f}")
# 4. SWAP (ExactInputSingle)
elif func == "exactInputSingle":
# Params: (params) -> struct
# struct ExactInputSingleParams {
# address tokenIn; address tokenOut; fee; recipient; deadline; amountIn; amountOutMinimum; sqrtPriceLimitX96;
# }
# Since args[0] is the struct (tuple/list)
# We need to guess indices or check ABI.
# Standard: tokenIn(0), tokenOut(1), fee(2), recipient(3), deadline(4), amountIn(5), minOut(6)
params = args[0]
token_in_addr = params[0]
token_out_addr = params[1]
amount_in_wei = params[5]
# Map address to symbol
# We can't access contract instance here easily, so use known map or iterate
sym_map = {
"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH",
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC",
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB",
"0x55d398326f99059fF775485246999027B3197955": "USDT"
}
sym_in = sym_map.get(token_in_addr, "UNKNOWN")
sym_out = sym_map.get(token_out_addr, "UNKNOWN")
amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Approx
if sym_in in ["USDC", "USDT"]: amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Mock usually 18 dec for simplicity unless specified
# Price calculation
# If swapping Base (WBNB) -> Quote (USDT), Price is ~300
# If Quote -> Base, Price is 1/300
price = self.prices.get("BNB", Decimal("300"))
amount_out = 0
if sym_in == "WBNB" and sym_out == "USDT":
amount_out = amount_in * price
elif sym_in == "USDT" and sym_out == "WBNB":
amount_out = amount_in / price
else:
amount_out = amount_in # 1:1 fallback
self.wallet_balances[sym_in] = max(0, self.wallet_balances.get(sym_in, 0) - amount_in)
self.wallet_balances[sym_out] = self.wallet_balances.get(sym_out, 0) + amount_out
logger.info(f"SWAP: {amount_in:.4f} {sym_in} -> {amount_out:.4f} {sym_out}")
def match_orders(self):
"""Simple order matching against current price."""
# In a real backtest, we'd check High/Low of the candle or Orderbook depth.
# Here we assume perfect liquidity at current price for simplicity,
# or implement simple slippage.
pass
# --- WEB3 MOCKS ---
class MockContractFunction:
def __init__(self, name, parent_contract, state: MockExchangeState):
self.name = name
self.contract = parent_contract
self.state = state
self.args = []
def __call__(self, *args, **kwargs):
self.args = args
return self
def call(self, transaction=None):
# SIMULATE READS
if self.name == "slot0":
# Determine Pair
symbol = "BNB" if "BNB" in self.state.prices else "ETH"
price = self.state.prices.get(symbol, Decimal("300"))
# For BNB Chain, T0 is USDT (0x55d), T1 is WBNB (0xbb4)
# Price of T0 (USDT) in T1 (WBNB) is 1 / Price
if symbol == "BNB":
if price > 0:
price = Decimal("1") / price
else:
price = Decimal("0")
sqrt_px = price.sqrt() * (2**96)
# Tick
import math
try:
# price = 1.0001^tick
tick = int(math.log(float(price)) / math.log(1.0001))
except:
tick = 0
return (int(sqrt_px), tick, 0, 0, 0, 0, True)
if self.name == "positions":
token_id = self.args[0]
if token_id in self.state.uni_positions:
p = self.state.uni_positions[token_id]
return (0, "", p['token0'], p['token1'], 500, p['tickLower'], p['tickUpper'], p['liquidity'],
0, 0, p.get('tokensOwed0', 0), p.get('tokensOwed1', 0))
else:
raise Exception("Position not found")
if self.name == "balanceOf":
addr = self.args[0]
# Hacky: detect token by contract address
symbol = self.contract.symbol_map.get(self.contract.address, "UNKNOWN")
return int(self.state.wallet_balances.get(symbol, 0) * (10**self.contract.decimals_val))
if self.name == "decimals":
return self.contract.decimals_val
if self.name == "symbol":
return self.contract.symbol_val
if self.name == "allowance":
return 10**50 # Infinite allowance
# Pool Methods
if self.name == "tickSpacing":
return 10
if self.name == "token0":
# Return USDT for BNB profile (0x55d < 0xbb4)
if "BNB" in self.state.prices:
return "0x55d398326f99059fF775485246999027B3197955"
return "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH
if self.name == "token1":
# Return WBNB for BNB profile
if "BNB" in self.state.prices:
return "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
return "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC
if self.name == "fee":
return 500
if self.name == "liquidity":
return 1000000000000000000
return None
def build_transaction(self, tx_params):
# Queue Transaction for Execution
self.state.pending_txs.append({
"func": self.name,
"args": self.args,
"contract": self.contract.address,
"value": tx_params.get("value", 0)
})
return {"data": "0xMOCK", "to": self.contract.address, "value": tx_params.get("value", 0)}
def estimate_gas(self, tx_params):
return 100000
class MockContract:
def __init__(self, address, abi, state: MockExchangeState):
self.address = address
self.abi = abi
self.state = state
self.functions = self
# Meta for simulation
self.symbol_map = {
"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH",
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC",
# BNB Chain
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB", # FIXED: Was BNB, but address is WBNB
"0x55d398326f99059fF775485246999027B3197955": "USDT"
}
symbol = self.symbol_map.get(address, "MOCK")
is_stable = symbol in ["USDC", "USDT"]
self.decimals_val = 18 if not is_stable else 6
if symbol == "USDT": self.decimals_val = 18 # BNB USDT is 18
self.symbol_val = symbol
def __getattr__(self, name):
return MockContractFunction(name, self, self.state)
class MockEth:
def __init__(self, state: MockExchangeState):
self.state = state
self.chain_id = 42161
self.max_priority_fee = 100000000
def contract(self, address, abi):
return MockContract(address, abi, self.state)
def get_block(self, block_identifier):
return {'baseFeePerGas': 100000000, 'timestamp': self.state.current_time_ms // 1000}
def get_balance(self, address):
# Native balance
return int(self.state.wallet_balances.get("NATIVE", 0) * 10**18)
def get_transaction_count(self, account, block_identifier=None):
return 1
def send_raw_transaction(self, raw_tx):
# EXECUTE PENDING TX
if self.state.pending_txs:
tx_data = self.state.pending_txs.pop(0)
res = self.state.process_transaction(tx_data)
return b'\x00' * 32
def wait_for_transaction_receipt(self, tx_hash, timeout=120):
# MOCK LOGS GENERATION
# We assume every tx is a successful Mint for now to test the flow
# In a real engine we'd inspect the tx data to determine the event
# Transfer Topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
# IncreaseLiquidity Topic: 0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde
increase_topic = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
# Use the actual minted ID if available, else dummy
real_token_id = getattr(self.state, 'last_minted_token_id', 123456)
token_id_hex = hex(real_token_id)[2:].zfill(64)
# Liquidity + Amount0 + Amount1
# 1000 Liquidity, 1 ETH (18 dec), 3000 USDC (6 dec)
# 1 ETH = 1e18, 3000 USDC = 3e9
data_liq = "00000000000000000000000000000000000000000000000000000000000003e8" # 1000
data_amt0 = "0000000000000000000000000000000000000000000000000de0b6b3a7640000" # 1e18
data_amt1 = "00000000000000000000000000000000000000000000000000000000b2d05e00" # 3e9
class Receipt:
status = 1
blockNumber = 12345
logs = [
{
'topics': [
HexBytes(transfer_topic),
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # From
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # To
HexBytes("0x" + token_id_hex) # TokenID
],
'data': b''
},
{
'topics': [
HexBytes(increase_topic)
],
'data': bytes.fromhex(data_liq + data_amt0 + data_amt1)
}
]
return Receipt()
class MockWeb3:
def __init__(self, state: MockExchangeState):
self.eth = MockEth(state)
self.middleware_onion = type('obj', (object,), {'inject': lambda *args, **kwargs: None})
def is_connected(self):
return True
def to_checksum_address(self, addr):
return addr
def is_address(self, addr):
return True
@staticmethod
def to_wei(val, unit):
if unit == 'gwei': return int(val * 10**9)
if unit == 'ether': return int(val * 10**18)
return int(val)
@staticmethod
def from_wei(val, unit):
if unit == 'gwei': return Decimal(val) / Decimal(10**9)
if unit == 'ether': return Decimal(val) / Decimal(10**18)
return Decimal(val)
@staticmethod
def keccak(text=None):
return b'\x00'*32 # Dummy
# --- HYPERLIQUID MOCKS ---
class MockInfo:
def __init__(self, state: MockExchangeState):
self.state = state
def all_mids(self):
# Return string prices as per API
return {k: str(v) for k, v in self.state.prices.items()}
def user_state(self, address):
positions = []
for sym, pos in self.state.hl_positions.items():
positions.append({
"position": {
"coin": sym,
"szi": str(pos['size']),
"entryPx": str(pos['entry_px']),
"unrealizedPnl": str(pos['unrealized_pnl'])
}
})
return {
"marginSummary": {
"accountValue": str(self.state.hl_balances.get("USDC", 0)),
"totalMarginUsed": "0",
"totalNtlPos": "0",
"totalRawUsd": "0"
},
"assetPositions": positions
}
def open_orders(self, address):
return self.state.hl_orders
def user_fills(self, address):
return [] # TODO: Store fills in state
def l2_snapshot(self, coin):
# Generate artificial orderbook around mid price
price = self.state.prices.get(coin, Decimal("0"))
if price == 0: return {'levels': [[], []]}
# Spread 0.05%
bid = price * Decimal("0.99975")
ask = price * Decimal("1.00025")
return {
"levels": [
[{"px": str(bid), "sz": "100.0", "n": 1}], # Bids
[{"px": str(ask), "sz": "100.0", "n": 1}] # Asks
]
}
class MockExchangeAPI:
def __init__(self, state: MockExchangeState):
self.state = state
def order(self, coin, is_buy, sz, limit_px, order_type, reduce_only=False):
# Execute immediately for IO/Market, or add to book
# Simulating Fill
price = Decimal(str(limit_px))
size = Decimal(str(sz))
cost = price * size
# Fee (Taker 0.035%)
fee = cost * Decimal("0.00035")
self.state.hl_fees_paid += fee
# Update Position
if coin not in self.state.hl_positions:
self.state.hl_positions[coin] = {'size': Decimal(0), 'entry_px': Decimal(0), 'unrealized_pnl': Decimal(0)}
pos = self.state.hl_positions[coin]
current_size = pos['size']
# Update Entry Price (Weighted Average)
# New Entry = (OldSize * OldEntry + NewSize * NewPrice) / (OldSize + NewSize)
signed_size = size if is_buy else -size
new_size = current_size + signed_size
if new_size == 0:
pos['entry_px'] = 0
elif (current_size > 0 and signed_size > 0) or (current_size < 0 and signed_size < 0):
# Increasing position
val_old = abs(current_size) * pos['entry_px']
val_new = size * price
pos['entry_px'] = (val_old + val_new) / abs(new_size)
else:
# Closing/Reducing - Entry Price doesn't change, PnL is realized
# Fraction closed
closed_ratio = min(abs(signed_size), abs(current_size)) / abs(current_size)
# This logic is simplified, real PnL logic is complex
pos['size'] = new_size
logger.info(f"MOCK HL EXEC: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price}. New Size: {new_size}")
return {"status": "ok", "response": {"data": {"statuses": [{"filled": {"oid": 123}}]}}}
def cancel(self, coin, oid):
self.state.hl_orders = [o for o in self.state.hl_orders if o['oid'] != oid]
return {"status": "ok"}

View File

@ -0,0 +1,25 @@
import os
import sys
# Add project root to path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
sys.path.append(project_root)
from tests.backtest.backtester import Backtester
def main():
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
if not os.path.exists(book_file):
print(f"Error: Data file not found: {book_file}")
return
print(f"Starting Backtest on {book_file}...")
bt = Backtester(book_file, trades_file)
bt.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,107 @@
import os
import json
import pandas as pd
import math
from decimal import Decimal
from datetime import datetime, timedelta
# --- SETTINGS ---
HISTORY_FILE = os.path.join("market_data", "pool_history.csv")
INVESTMENT_USD = 10000
RANGE_WIDTH_PCT = 0.10 # +/- 10%
REBALANCE_COST_PCT = 0.001 # 0.1% fee for rebalancing (swaps + gas)
def tick_to_price(tick):
return 1.0001 ** tick
def get_delta_from_pct(pct):
# tick_delta = log(1+pct) / log(1.0001)
return int(math.log(1 + pct) / math.log(1.0001))
def analyze():
if not os.path.exists(HISTORY_FILE):
print("No history file found. Run pool_scanner.py first.")
return
df = pd.read_csv(HISTORY_FILE)
df['timestamp'] = pd.to_datetime(df['timestamp'])
pools = df['pool_name'].unique()
results = []
for pool in pools:
pdf = df[df['pool_name'] == pool].sort_values('timestamp').copy()
if len(pdf) < 2: continue
# Initial Setup
start_row = pdf.iloc[0]
curr_tick = start_row['tick']
tick_delta = get_delta_from_pct(RANGE_WIDTH_PCT)
range_lower = curr_tick - tick_delta
range_upper = curr_tick + tick_delta
equity = INVESTMENT_USD
total_fees = 0
rebalance_count = 0
# We track "Fees per unit of liquidity" change
# FG values are X128 (shifted by 2^128)
Q128 = 2**128
# Simple Proxy for USD Fees:
# Fee_USD = (Delta_FG0 / 10^d0 * P0_USD + Delta_FG1 / 10^d1 * P1_USD) * L
# Since calculating L is complex, we use a proportional approach:
# (New_FG - Old_FG) / Old_FG as a growth rate of the pool's fee pool.
for i in range(1, len(pdf)):
row = pdf.iloc[i]
prev = pdf.iloc[i-1]
p_tick = row['tick']
# 1. Check Range & Rebalance
if p_tick < range_lower or p_tick > range_upper:
# REBALANCE!
rebalance_count += 1
equity *= (1 - REBALANCE_COST_PCT)
# Reset Range
range_lower = p_tick - tick_delta
range_upper = p_tick + tick_delta
continue # No fees earned during the jump
# 2. Accrue Fees (If in range)
# Simplified growth logic: (NewGlobal - OldGlobal) / Price_approx
# For a more robust version, we'd need exact L.
# Here we track the delta of the raw FG counters.
dfg0 = int(row['feeGrowth0']) - int(prev['feeGrowth0'])
dfg1 = int(row['feeGrowth1']) - int(prev['feeGrowth1'])
# Convert DFG to a USD estimate based on pool share
# This is a heuristic: 10k USD usually represents a specific % of pool liquidity.
# We assume a fixed liquidity L derived from 10k at start.
# L = 10000 / (sqrt(P) - sqrt(Pa)) ...
# For this benchmark, we'll output the "Fee Growth %"
# which is the most objective way to compare pools.
# (Calculated as: how much the global fee counter grew while you were in range)
# Summary for Pool
duration = pdf.iloc[-1]['timestamp'] - pdf.iloc[0]['timestamp']
results.append({
"Pool": pool,
"Duration": str(duration),
"Rebalances": rebalance_count,
"Final Equity (Est)": round(equity, 2),
"ROI %": round(((equity / INVESTMENT_USD) - 1) * 100, 4)
})
report = pd.DataFrame(results)
print("\n=== POOL PERFORMANCE REPORT ===")
print(report.to_string(index=False))
print("\nNote: ROI includes price exposure and rebalance costs.")
if __name__ == "__main__":
analyze()

View File

@ -0,0 +1,171 @@
import json
import time
import requests
import math
import os
from datetime import datetime
from statistics import mean, stdev
# --- Configuration ---
COINS = ["ETH"]
# Mapping of label to number of 1-minute periods
PERIODS_CONFIG = {
"37m": 37,
"3h": 3 * 60, # 180 minutes
"12h": 12 * 60, # 720 minutes
"24h": 24 * 60 # 1440 minutes
}
MA_PERIODS = [33, 44, 88, 144]
STD_DEV_MULTIPLIER = 1.6 # Standard deviation multiplier for bands
OUTPUT_FILE = os.path.join("market_data", "indicators.json")
API_URL = "https://api.hyperliquid.xyz/info"
UPDATE_INTERVAL = 60 # seconds
def fetch_candles(coin, interval="1m", lookback_minutes=1500):
"""
Fetches candle data from Hyperliquid.
We need at least enough candles for the longest period (1440).
Requesting slightly more to be safe.
"""
# Calculate startTime: now - (lookback_minutes * 60 * 1000)
# Hyperliquid expects startTime in milliseconds
end_time = int(time.time() * 1000)
start_time = end_time - (lookback_minutes * 60 * 1000)
payload = {
"type": "candleSnapshot",
"req": {
"coin": coin,
"interval": interval,
"startTime": start_time,
"endTime": end_time
}
}
try:
response = requests.post(API_URL, json=payload, timeout=10)
response.raise_for_status()
data = response.json()
# Data format is typically a list of dicts:
# {'t': 170..., 'T': 170..., 's': 'ETH', 'i': '1m', 'o': '...', 'c': '...', 'h': '...', 'l': '...', 'v': '...', 'n': ...}
# We need closing prices 'c'
candles = []
for c in data:
try:
# Ensure we parse 'c' (close) as float
candles.append(float(c['c']))
except (ValueError, KeyError):
continue
return candles
except Exception as e:
print(f"Error fetching candles for {coin}: {e}")
return []
def calculate_ma(prices, period):
"""Calculates Simple Moving Average."""
if len(prices) < period:
return None
return mean(prices[-period:])
def calculate_bb(prices, period, num_std_dev=2.0):
"""
Calculates Bollinger Bands for the LAST 'period' items in prices.
Returns {mid, upper, lower} or None if insufficient data.
"""
if len(prices) < period:
return None
# Take the last 'period' prices
window = prices[-period:]
try:
avg = mean(window)
# Population stdev or sample stdev? Usually sample (stdev) is used in finance or pandas default
if period > 1:
sd = stdev(window)
else:
sd = 0.0
upper = avg + (num_std_dev * sd)
lower = avg - (num_std_dev * sd)
return {
"mid": avg,
"upper": upper,
"lower": lower,
"std": sd
}
except Exception as e:
print(f"Error calculating BB: {e}")
return None
def main():
print(f"Starting Market Data Calculator for {COINS}")
print(f"BB Periods: {PERIODS_CONFIG}")
print(f"MA Periods: {MA_PERIODS}")
print(f"Output: {OUTPUT_FILE}")
# Ensure directory exists
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
while True:
try:
results = {
"last_updated": datetime.now().isoformat(),
"config": {
"std_dev_multiplier": STD_DEV_MULTIPLIER,
"ma_periods": MA_PERIODS
},
"data": {}
}
# Find the max needed history (BB vs MA)
max_bb = max(PERIODS_CONFIG.values()) if PERIODS_CONFIG else 0
max_ma = max(MA_PERIODS) if MA_PERIODS else 0
fetch_limit = max(max_bb, max_ma) + 60
for coin in COINS:
print(f"Fetching data for {coin}...", end="", flush=True)
prices = fetch_candles(coin, lookback_minutes=fetch_limit)
if not prices:
print(" Failed.")
continue
print(f" Got {len(prices)} candles.", end="", flush=True)
coin_results = {
"current_price": prices[-1] if prices else 0,
"bb": {},
"ma": {}
}
# Calculate BB
for label, period in PERIODS_CONFIG.items():
bb = calculate_bb(prices, period, num_std_dev=STD_DEV_MULTIPLIER)
coin_results["bb"][label] = bb if bb else "Insufficient Data"
# Calculate MA
for period in MA_PERIODS:
ma = calculate_ma(prices, period)
coin_results["ma"][str(period)] = ma if ma else "Insufficient Data"
results["data"][coin] = coin_results
print(" Done.")
# Save to file
with open(OUTPUT_FILE, 'w') as f:
json.dump(results, f, indent=4)
print(f"Updated {OUTPUT_FILE}")
except Exception as e:
print(f"Main loop error: {e}")
time.sleep(UPDATE_INTERVAL)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,92 @@
import os
import sys
import json
from web3 import Web3
from dotenv import load_dotenv
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from clp_abis import AERODROME_FACTORY_ABI, UNISWAP_V3_POOL_ABI
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
if not RPC_URL:
print("Error: BASE_RPC_URL not set")
sys.exit(1)
w3 = Web3(Web3.HTTPProvider(RPC_URL))
FACTORY_ADDRESS = "0x827922686190790b37229fd06084350E74485b72"
WETH = "0x4200000000000000000000000000000000000006"
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
TICK_SPACING = 1
def check_pool():
print(f"Connecting to Base... {w3.client_version}")
# 1. Get Factory Address from NPM
npm_address = "0x827922686190790b37229fd06084350E74485b72"
# NPM ABI minimal
npm_abi = [{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}]
npm = w3.eth.contract(address=npm_address, abi=npm_abi)
try:
factory_address = npm.functions.factory().call()
print(f"Factory Address: {factory_address}")
except Exception as e:
print(f"Error fetching factory: {e}")
return
factory = w3.eth.contract(address=factory_address, abi=AERODROME_FACTORY_ABI)
# 2. Get Pool Address using tickSpacing
print(f"Querying Factory for WETH/USDC with tickSpacing={TICK_SPACING}...")
try:
pool_address = factory.functions.getPool(WETH, USDC, TICK_SPACING).call()
print(f"Pool Address (TS=1): {pool_address}")
except Exception as e:
print(f"Error calling getPool(1): {e}")
# Check TS=80
print(f"Querying Factory for WETH/USDC with tickSpacing=80...")
try:
pool_address_80 = factory.functions.getPool(WETH, USDC, 80).call()
print(f"Pool Address (TS=80): {pool_address_80}")
except Exception as e:
print(f"Error calling getPool(80): {e}")
if pool_address == "0x0000000000000000000000000000000000000000":
print("Pool not found!")
return
# 3. Check Pool Details
pool = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
try:
# Try to read fee()
# Note: Standard UniV3 pools have 'fee()'.
# Aerodrome Slipstream pools might not, or it might be different.
fee = pool.functions.fee().call()
print(f"Pool.fee(): {fee}")
except Exception as e:
print(f"Could not read pool.fee(): {e}")
try:
# Read tickSpacing()
ts = pool.functions.tickSpacing().call()
print(f"Pool.tickSpacing(): {ts}")
except Exception as e:
print(f"Could not read pool.tickSpacing(): {e}")
try:
# Read slot0
slot0 = pool.functions.slot0().call()
print(f"Pool.slot0(): {slot0}")
# Standard UniV3 slot0: (sqrtPriceX96, tick, observationIndex, observationCardinality, observationCardinalityNext, feeProtocol, unlocked)
# Aerodrome might vary.
except Exception as e:
print(f"Could not read pool.slot0(): {e}")
if __name__ == "__main__":
check_pool()

View File

@ -0,0 +1,103 @@
import os
import sys
import json
from web3 import Web3
from dotenv import load_dotenv
from decimal import Decimal
# Add project root
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Load Env
load_dotenv()
RPC_URL = os.environ.get("MAINNET_RPC_URL") # Arbitrum RPC
POOL_ADDRESS = "0xC6962004f452bE9203591991D15f6b388e09E8D0" # ARB/WETH 500
ERC20_ABI = [
{"constant": True, "inputs": [], "name": "symbol", "outputs": [{"name": "", "type": "string"}], "payable": False, "stateMutability": "view", "type": "function"},
{"constant": True, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": False, "stateMutability": "view", "type": "function"},
{"constant": True, "inputs": [{"name": "_owner", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "balance", "type": "uint256"}], "payable": False, "stateMutability": "view", "type": "function"}
]
POOL_ABI = [
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"}
]
def main():
if not RPC_URL:
print("Error: MAINNET_RPC_URL not found in .env")
return
w3 = Web3(Web3.HTTPProvider(RPC_URL))
if not w3.is_connected():
print("Error: Could not connect to RPC")
return
print(f"Connected to Arbitrum: {w3.eth.block_number}")
pool_contract = w3.eth.contract(address=POOL_ADDRESS, abi=POOL_ABI)
# 1. Metadata
t0_addr = pool_contract.functions.token0().call()
t1_addr = pool_contract.functions.token1().call()
fee = pool_contract.functions.fee().call()
t0_contract = w3.eth.contract(address=t0_addr, abi=ERC20_ABI)
t1_contract = w3.eth.contract(address=t1_addr, abi=ERC20_ABI)
t0_sym = t0_contract.functions.symbol().call()
t1_sym = t1_contract.functions.symbol().call()
t0_dec = t0_contract.functions.decimals().call()
t1_dec = t1_contract.functions.decimals().call()
print(f"\nPool: {t0_sym} / {t1_sym} ({fee/10000}%)")
print(f"Token0: {t0_sym} ({t0_addr}) - {t0_dec} dec")
print(f"Token1: {t1_sym} ({t1_addr}) - {t1_dec} dec")
# 2. State
slot0 = pool_contract.functions.slot0().call()
sqrt_price = slot0[0]
tick = slot0[1]
liquidity = pool_contract.functions.liquidity().call()
print(f"\nState:")
print(f"Liquidity: {liquidity}")
print(f"Tick: {tick}")
print(f"SqrtPriceX96: {sqrt_price}")
# 3. Price Calc
# price = (sqrt / 2^96)^2
p = (Decimal(sqrt_price) / Decimal(2**96)) ** 2
# Adjust for decimals: Price = raw_price * 10^(d0 - d1)
adj_price = p * (Decimal(10) ** (t0_dec - t1_dec))
inv_price = Decimal(1) / adj_price if adj_price > 0 else 0
print(f"\nPrices:")
print(f"1 {t0_sym} = {adj_price:.6f} {t1_sym}")
print(f"1 {t1_sym} = {inv_price:.6f} {t0_sym}")
# 4. TVL Estimation (Balances)
t0_bal = t0_contract.functions.balanceOf(POOL_ADDRESS).call()
t1_bal = t1_contract.functions.balanceOf(POOL_ADDRESS).call()
t0_human = Decimal(t0_bal) / Decimal(10**t0_dec)
t1_human = Decimal(t1_bal) / Decimal(10**t1_dec)
print(f"\nTVL (Locked in Contract):")
print(f"{t0_human:,.2f} {t0_sym}")
print(f"{t1_human:,.2f} {t1_sym}")
# Assume WETH is approx 3350 (or whatever current market is, we can use slot0 price if one is stable)
# If one is USD stable, we can calc total.
# ARB / WETH. WETH is ~$3300.
# Let's just output raw.
if __name__ == "__main__":
main()

View File

@ -0,0 +1,93 @@
import os
import json
from web3 import Web3
from dotenv import load_dotenv
load_dotenv()
# Aerodrome Slipstream Config
RPC_URL = os.environ.get("BASE_RPC_URL")
NPM_ADDRESS = "0x827922686190790b37229fd06084350E74485b72"
WETH = "0x4200000000000000000000000000000000000006"
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
TARGET_POOL = "0xdbc6998296caa1652a810dc8d3baf4a8294330f1"
# ABIs
NPM_ABI = json.loads('[{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]')
FACTORY_ABI = json.loads('[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"int24","name":"","type":"int24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]')
def main():
print(f"Connecting to {RPC_URL}...")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
if not w3.is_connected():
print("Failed to connect.")
return
print(f"NPM: {NPM_ADDRESS}")
npm = w3.eth.contract(address=NPM_ADDRESS, abi=NPM_ABI)
try:
factory_addr = npm.functions.factory().call()
print(f"Factory from NPM: {factory_addr}")
except Exception as e:
print(f"Error getting factory: {e}")
return
factory = w3.eth.contract(address=factory_addr, abi=FACTORY_ABI)
# Aerodrome Tick Spacings (Fees)
# Uniswap V3: 100 (0.01%), 500 (0.05%), 3000 (0.3%), 10000 (1%)
# Aerodrome Slipstream might use tickSpacing directly or different fee numbers.
# Slipstream: 1 (0.01%), 50 (0.05%), 200 (0.3%), 2000 (1%) ???
# Or maybe it uses standard Uniswap fees?
# Let's try standard first, then tickSpacing values.
# Common Uniswap V3 Fees
fees_to_test = [100, 500, 3000, 10000]
# Aerodrome specific? (1, 50, 100, 200) - Aerodrome uses tickSpacing as the identifier in getPool sometimes?
# Wait, Uniswap V3 Factory getPool takes (tokenA, tokenB, fee).
# Some forks take (tokenA, tokenB, tickSpacing).
fees_to_test += [1, 50, 200, 2000]
print(f"Testing getPool for WETH/USDC...")
found = False
for fee in fees_to_test:
try:
pool = factory.functions.getPool(WETH, USDC, fee).call()
print(f"Fee {fee}: {pool}")
if pool.lower() == TARGET_POOL.lower():
print(f"✅ MATCH FOUND! Fee Tier is: {fee}")
found = True
except Exception as e:
print(f"Fee {fee}: Error ({e})")
# ... (Existing code)
if not found:
# ... (Existing code)
pass
# DEBUG SLOT0
print(f"\nDebugging slot0 for {TARGET_POOL}...")
target_pool_checksum = Web3.to_checksum_address(TARGET_POOL)
# Try raw call to see data length
raw_data = w3.eth.call({
'to': target_pool_checksum,
'data': w3.keccak(text="slot0()").hex()[:10]
})
print(f"Raw Slot0 Data ({len(raw_data)} bytes): {raw_data.hex()}")
# Try standard ABI
POOL_ABI = json.loads('[{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"uint24","name":"feeProtocol","type":"uint24"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"}]')
pool_c = w3.eth.contract(address=target_pool_checksum, abi=POOL_ABI)
try:
data = pool_c.functions.slot0().call()
print(f"Standard V3 Slot0 Data: {data}")
except Exception as e:
print(f"Standard V3 Slot0 Failed: {e}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,33 @@
import os
import sys
import json
from web3 import Web3
from dotenv import load_dotenv
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from clp_abis import NONFUNGIBLE_POSITION_MANAGER_ABI
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
NPM_ADDRESS = "0x827922686190790b37229fd06084350E74485b72"
WETH = "0x4200000000000000000000000000000000000006"
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
def check_code():
npm_addr = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"
try:
code = w3.eth.get_code(npm_addr)
print(f"Code at {npm_addr}: {len(code)} bytes")
if len(code) > 0:
# Try calling factory()
npm = w3.eth.contract(address=npm_addr, abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
f = npm.functions.factory().call()
print(f"Factory: {f}")
except Exception as e:
print(f"Check failed: {e}")
if __name__ == "__main__":
check_code()

104
florida/tools/debug_mint.py Normal file
View File

@ -0,0 +1,104 @@
import os
import sys
import json
import time
from decimal import Decimal
from web3 import Web3
from dotenv import load_dotenv
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from clp_abis import NONFUNGIBLE_POSITION_MANAGER_ABI
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
if not RPC_URL:
print("Error: BASE_RPC_URL not set")
sys.exit(1)
w3 = Web3(Web3.HTTPProvider(RPC_URL))
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
account = w3.eth.account.from_key(private_key)
NPM_ADDRESS = "0x827922686190790b37229fd06084350E74485b72"
WETH = "0x4200000000000000000000000000000000000006"
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
# Amounts (Reduced to fit allowance)
AMOUNT0 = 10000000000000000 # 0.01 ETH
AMOUNT1 = 10000000 # 10 USDC
# Ticks (Approx from logs/logic, using TS=1)
# Current Tick ~ -201984 (Price ~3050)
# Just picking a valid range around current price
CURRENT_TICK = -201984
TICK_LOWER = CURRENT_TICK - 200
TICK_UPPER = CURRENT_TICK + 200
def try_mint_simulation(fee_value):
print(f"\n--- Testing Mint with fee={fee_value} ---")
npm = w3.eth.contract(address=NPM_ADDRESS, abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
# FETCH REAL TICK
pool_address = "0xb2cc224c1c9feE385f8ad6a55b4d94E92359DC59" # TS=100 Pool
pool_abi = [{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"}]
pool = w3.eth.contract(address=pool_address, abi=pool_abi)
try:
slot0 = pool.functions.slot0().call()
current_tick = slot0[1]
print(f"Current Tick: {current_tick}")
except:
current_tick = -200000
print("Failed to fetch tick, using default.")
# Align to TS=100
TS = 100
tick_lower = (current_tick // TS) * TS - (TS * 2) # -200 ticks
tick_upper = (current_tick // TS) * TS + (TS * 2) # +200 ticks
print(f"Ticks: {tick_lower} <-> {tick_upper} (TS={TS})")
# CHECK ALLOWANCES
erc20_abi = [{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
weth_c = w3.eth.contract(address=WETH, abi=erc20_abi)
usdc_c = w3.eth.contract(address=USDC, abi=erc20_abi)
a0 = weth_c.functions.allowance(account.address, NPM_ADDRESS).call()
a1 = usdc_c.functions.allowance(account.address, NPM_ADDRESS).call()
print(f"Allowances: WETH={a0}, USDC={a1}")
params = (
WETH, USDC,
fee_value, # The value in question
tick_lower, tick_upper,
AMOUNT0, AMOUNT1,
0, 0, # Min amounts 0
account.address,
int(time.time()) + 180
)
tx_params = {
'from': account.address,
'nonce': w3.eth.get_transaction_count(account.address),
'value': 0,
'gas': 500000,
'gasPrice': w3.eth.gas_price
}
try:
# Simulate (call)
npm.functions.mint(params).call(tx_params)
print("✅ Simulation SUCCESS!")
return True
except Exception as e:
print(f"❌ Simulation FAILED: {e}")
return False
if __name__ == "__main__":
# Test candidates
candidates = [1, 100, 200, 500, 3000, 10000]
for fee in candidates:
try_mint_simulation(fee)

View File

@ -0,0 +1,39 @@
import os
import sys
import json
from web3 import Web3
from dotenv import load_dotenv
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from clp_abis import AERODROME_FACTORY_ABI
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
FACTORY_ADDRESS = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A" # From previous debug
def check_mapping():
# Try standard V3 factory ABI method: feeAmountTickSpacing(uint24)
abi = [{"inputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "name": "feeAmountTickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"}]
factory = w3.eth.contract(address=FACTORY_ADDRESS, abi=abi)
candidates = [1, 50, 80, 100, 200, 400, 500, 2000, 3000, 10000]
print("--- Fee -> TickSpacing Mapping ---")
for fee in candidates:
try:
ts = factory.functions.feeAmountTickSpacing(fee).call()
print(f"Fee {fee} -> TS {ts}")
except Exception as e:
# print(f"Fee {fee} -> Failed")
pass
# Check if there is a 'tickSpacing' method on Factory?
# Aerodrome Factory ABI we used has 'getPool(tokenA, tokenB, tickSpacing)'.
# This implies Factory doesn't use fee mapping for getPool, it uses TS directly.
# BUT NPM 'mint' uses 'fee'.
# So NPM MUST have a mapping.
if __name__ == "__main__":
check_mapping()

43
florida/tools/debug_tx.py Normal file
View File

@ -0,0 +1,43 @@
import os
import sys
import json
from web3 import Web3
from dotenv import load_dotenv
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
if not RPC_URL:
print("Error: BASE_RPC_URL not set")
sys.exit(1)
w3 = Web3(Web3.HTTPProvider(RPC_URL))
TX_HASH = "0x40c926a0d792a00d7a549f57f76ccb65c14c49ca7120563ea1229d1dae40457d"
def check_tx():
print(f"Fetching receipt for {TX_HASH}...")
try:
receipt = w3.eth.get_transaction_receipt(TX_HASH)
print(f"Status: {receipt['status']}")
print(f"Block: {receipt['blockNumber']}")
print(f"Logs: {len(receipt['logs'])}")
for i, log in enumerate(receipt['logs']):
print(f"--- Log {i} ---")
print(f"Address: {log['address']}")
print(f"Topics: {[t.hex() for t in log['topics']]}")
# Try to decode Swap event?
# Swap(address sender, address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
# Topic0: 0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67
if log['topics'][0].hex() == "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67":
print(">>> SWAP EVENT DETECTED <<<")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
check_tx()

View File

@ -0,0 +1,3 @@
from web3 import Web3
addr = "0xbe6d8f0d397708d99755b7857067757F97174d7d"
print(Web3.to_checksum_address(addr))

View File

@ -0,0 +1,66 @@
import os
import sys
import json
import time
from web3 import Web3
from dotenv import load_dotenv
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from clp_abis import ERC20_ABI
load_dotenv()
RPC_URL = os.environ.get("BASE_RPC_URL")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
account = w3.eth.account.from_key(private_key)
NPM_ADDRESS = "0x827922686190790b37229fd06084350E74485b72"
WETH = "0x4200000000000000000000000000000000000006"
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
# Modified ABI: int24 tickSpacing instead of uint24 fee, AND sqrtPriceX96
MINT_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
def try_mint():
npm = w3.eth.contract(address=NPM_ADDRESS, abi=MINT_ABI)
# Tick Logic for 0xb2cc (TS=100)
# Current Tick -195800
current_tick = -195800
# Align to 100
# -196000 and -195600
tl = -196000
tu = -195600
# Amounts
a0 = 10000000000000000 # 0.01 ETH
a1 = 10000000 # 10 USDC
# tickSpacing param
ts = 100
params = (
WETH, USDC,
ts, # int24
tl, tu,
a0, a1,
0, 0,
account.address,
int(time.time()) + 180,
0 # sqrtPriceX96
)
print(f"Simulating mint with TS={ts} (int24)...")
try:
npm.functions.mint(params).call({'from': account.address, 'gas': 1000000})
print("✅ SUCCESS!")
except Exception as e:
print(f"❌ FAILED: {e}")
if __name__ == "__main__":
try_mint()

View File

@ -0,0 +1,192 @@
import os
import json
import time
import pandas as pd
from decimal import Decimal
from datetime import datetime
from web3 import Web3
from dotenv import load_dotenv
# --- CONFIGURATION ---
CONFIG_FILE = os.path.join(os.path.dirname(__file__), "pool_scanner_config.json")
STATE_FILE = os.path.join("market_data", "pool_scanner_state.json")
HISTORY_FILE = os.path.join("market_data", "pool_history.csv")
load_dotenv()
# RPC MAP
RPC_MAP = {
"ARBITRUM": os.environ.get("MAINNET_RPC_URL"),
"BSC": os.environ.get("BNB_RPC_URL"),
"BASE": os.environ.get("BASE_RPC_URL")
}
# ABIS
POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
# PancakeSwap V3 uses uint32 for feeProtocol
PANCAKE_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('[{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}]')
def get_w3(chain):
url = RPC_MAP.get(chain)
if not url: return None
return Web3(Web3.HTTPProvider(url))
def load_state():
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
return json.load(f)
return {}
def save_state(state):
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def append_history(data):
df = pd.DataFrame([data])
header = not os.path.exists(HISTORY_FILE)
df.to_csv(HISTORY_FILE, mode='a', header=header, index=False)
def get_liquidity_for_amount(amount, sqrt_price_x96, tick_lower, tick_upper, decimal_diff):
# Simplified Liquidity Calc for 50/50 deposit simulation
# L = Amount / (sqrt(P) - sqrt(Pa)) for one side...
# For now, we assume simple V3 math or just track Fee Growth per Unit Liquidity
# Real simulation is complex.
# TRICK: We will track "Fee Growth per 1 Unit of Liquidity" directly (Raw X128).
# Then user can multiply by their theoretical L later.
return 1
def main():
print("Starting Pool Scanner...")
with open(CONFIG_FILE, 'r') as f:
pools = json.load(f)
state = load_state()
# Init Web3 cache
w3_instances = {}
for pool in pools:
name = pool['name']
chain = pool['chain']
# Fix Checksum
try:
addr = Web3.to_checksum_address(pool['pool_address'])
except Exception:
print(f" ❌ Invalid Address: {pool['pool_address']}")
continue
is_aero = pool.get('is_aerodrome', False)
print(f"Scanning {name} ({chain})...")
if chain not in w3_instances:
w3_instances[chain] = get_w3(chain)
w3 = w3_instances[chain]
if not w3 or not w3.is_connected():
print(f" ❌ RPC Error for {chain}")
continue
try:
if is_aero:
abi = AERODROME_POOL_ABI
elif chain == "BSC":
abi = PANCAKE_POOL_ABI
else:
abi = POOL_ABI
contract = w3.eth.contract(address=addr, abi=abi)
# Fetch Data
slot0 = contract.functions.slot0().call()
tick = slot0[1]
sqrt_price = slot0[0]
fg0 = contract.functions.feeGrowthGlobal0X128().call()
fg1 = contract.functions.feeGrowthGlobal1X128().call()
# Fetch Decimals (Once)
if name not in state:
t0 = contract.functions.token0().call()
t1 = contract.functions.token1().call()
d0 = w3.eth.contract(address=t0, abi=ERC20_ABI).functions.decimals().call()
d1 = w3.eth.contract(address=t1, abi=ERC20_ABI).functions.decimals().call()
state[name] = {
"init_tick": tick,
"init_fg0": fg0,
"init_fg1": fg1,
"decimals": [d0, d1],
"cumulative_fees_usd": 0.0,
"last_fg0": fg0,
"last_fg1": fg1
}
# Update State
prev = state[name]
diff0 = fg0 - prev['last_fg0']
diff1 = fg1 - prev['last_fg1']
# Calculate USD Value of Fees (Approx)
# Need Liquidity.
# If we assume 1 unit of Liquidity?
# Fee = Diff * L / 2^128
# Update Last
prev['last_fg0'] = fg0
prev['last_fg1'] = fg1
prev['last_tick'] = tick
prev['last_update'] = datetime.now().isoformat()
# Save History
record = {
"timestamp": datetime.now().isoformat(),
"pool_name": name,
"chain": chain,
"tick": tick,
"sqrtPriceX96": str(sqrt_price),
"feeGrowth0": str(fg0),
"feeGrowth1": str(fg1)
}
append_history(record)
print(f" ✅ Data recorded. Tick: {tick}")
except Exception as e:
print(f" ❌ Error: {e}")
save_state(state)
print("Scan complete.")
if __name__ == "__main__":
while True:
main()
time.sleep(600) # 10 minutes

View File

@ -0,0 +1,54 @@
[
{
"name": "Uniswap V3 (Arbitrum) - ETH/USDC",
"chain": "ARBITRUM",
"pool_address": "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443",
"fee_tier": 500,
"simulation": {
"investment_usd": 10000,
"range_width_pct": 0.10
}
},
{
"name": "PancakeSwap V3 (BNB Chain) - WBNB/USDT",
"chain": "BSC",
"pool_address": "0x36696169C63e42cd08ce11f5deeBbCeBae652050",
"fee_tier": 100,
"simulation": {
"investment_usd": 10000,
"range_width_pct": 0.10
}
},
{
"name": "Uniswap V3 (Base) - WETH/USDC",
"chain": "BASE",
"pool_address": "0xd0b53d9277642d899df5c87a3966a349a798f224",
"fee_tier": 500,
"simulation": {
"investment_usd": 10000,
"range_width_pct": 0.10
}
},
{
"name": "Aerodrome SlipStream (Base) - WETH/USDC (TS=1)",
"chain": "BASE",
"pool_address": "0xdbc6998296caA1652A810dc8D3BaF4A8294330f1",
"is_aerodrome": true,
"tick_spacing": 1,
"simulation": {
"investment_usd": 10000,
"range_width_pct": 0.10
}
},
{
"name": "Aerodrome SlipStream (Base) - WETH/USDC (TS=100)",
"chain": "BASE",
"pool_address": "0xb2cc224c1c9feE385f8ad6a55b4d94E92359DC59",
"is_aerodrome": true,
"tick_spacing": 100,
"simulation": {
"investment_usd": 10000,
"range_width_pct": 0.10
}
}
]

View File

@ -0,0 +1,349 @@
import os
import sys
import json
import argparse
import time
from decimal import Decimal
from dotenv import load_dotenv
from web3 import Web3
# Load environment variables
load_dotenv()
# --- CONFIGURATION ---
# ABIs
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
WETH_ABI = json.loads('''
[
{"inputs": [], "name": "deposit", "outputs": [], "stateMutability": "payable", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]
''')
# SwapRouter01 (With Deadline in struct) - e.g. Arbitrum 0xE592..., BSC
SWAP_ROUTER_01_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint200"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
# SwapRouter02 (NO Deadline in struct) - e.g. Base 0x2626...
SWAP_ROUTER_02_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct IV3SwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
CHAIN_CONFIG = {
"ARBITRUM": {
"rpc_env": "MAINNET_RPC_URL",
"chain_id": 42161,
"router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", # SwapRouter02
"abi_version": 2,
"tokens": {
"USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"WETH": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"ETH": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # Alias to WETH for wrapping
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 500
},
"BASE": {
"rpc_env": "BASE_RPC_URL",
"chain_id": 8453,
"router": "0x2626664c2603336E57B271c5C0b26F421741e481", # SwapRouter02
"abi_version": 2,
"tokens": {
"USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"WETH": "0x4200000000000000000000000000000000000006",
"ETH": "0x4200000000000000000000000000000000000006", # Alias to WETH for wrapping
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 500
},
"BASE_AERO": {
"rpc_env": "BASE_RPC_URL",
"chain_id": 8453,
"router": "0xbe6D8f0D397708D99755B7857067757f97174d7d", # Aerodrome Slipstream SwapRouter
"abi_version": 1, # Router requires deadline (Standard SwapRouter01 style)
"tokens": {
"USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"WETH": "0x4200000000000000000000000000000000000006",
"ETH": "0x4200000000000000000000000000000000000006",
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 1 # TickSpacing 1 (0.01%)
},
"BSC": {
"rpc_env": "BNB_RPC_URL",
"chain_id": 56,
"router": "0x1b81D678ffb9C0263b24A97847620C99d213eB14", # PancakeSwap V3
"abi_version": 1,
"tokens": {
"USDC": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
"WBNB": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"BNB": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" # Alias to WBNB for wrapping
},
"default_fee": 500
}
}
def get_web3(chain_name):
config = CHAIN_CONFIG.get(chain_name.upper())
if not config:
raise ValueError(f"Unsupported chain: {chain_name}")
rpc_url = os.environ.get(config["rpc_env"])
if not rpc_url:
raise ValueError(f"RPC URL not found in environment for {config['rpc_env']}")
w3 = Web3(Web3.HTTPProvider(rpc_url))
if not w3.is_connected():
raise ConnectionError(f"Failed to connect to {chain_name} RPC")
return w3, config
def approve_token(w3, token_contract, spender_address, amount, private_key, my_address):
"""
Checks allowance and approves if necessary.
Robust gas handling.
"""
allowance = token_contract.functions.allowance(my_address, spender_address).call()
if allowance >= amount:
print(f"Token already approved (Allowance: {allowance})")
return True
print(f"Approving token... (Current: {allowance}, Needed: {amount})")
# Build tx base
tx_params = {
'from': my_address,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Determine Gas Strategy
try:
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
# EIP-1559
base_fee = latest_block['baseFeePerGas']
priority_fee = w3.to_wei(0.1, 'gwei') # Conservative priority
tx_params['maxFeePerGas'] = int(base_fee * 1.5) + priority_fee
tx_params['maxPriorityFeePerGas'] = priority_fee
else:
# Legacy
tx_params['gasPrice'] = w3.eth.gas_price
except Exception as e:
print(f"Error determining gas strategy: {e}. Fallback to w3.eth.gas_price")
tx_params['gasPrice'] = w3.eth.gas_price
# Build transaction
tx = token_contract.functions.approve(spender_address, 2**256 - 1).build_transaction(tx_params)
# Sign and send
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Approval Tx sent: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt.status == 1:
print("Approval successful.")
return True
else:
print("Approval failed.")
return False
def wrap_eth(w3, weth_address, amount_wei, private_key, my_address):
"""
Wraps native ETH/BNB to WETH/WBNB.
"""
print(f"Wrapping native token to wrapped version...")
weth_contract = w3.eth.contract(address=weth_address, abi=WETH_ABI)
tx_params = {
'from': my_address,
'value': amount_wei,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Gas logic (Simplified)
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
base_fee = latest_block['baseFeePerGas']
tx_params['maxFeePerGas'] = int(base_fee * 1.5) + w3.to_wei(0.1, 'gwei')
tx_params['maxPriorityFeePerGas'] = w3.to_wei(0.1, 'gwei')
else:
tx_params['gasPrice'] = w3.eth.gas_price
tx = weth_contract.functions.deposit().build_transaction(tx_params)
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Wrapping Tx sent: {tx_hash.hex()}")
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Wrapping successful.")
def execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_readable, fee_tier=None, slippage_pct=0.5):
"""
Main function to execute swap.
"""
chain_name = chain_name.upper()
token_in_sym = token_in_sym.upper()
token_out_sym = token_out_sym.upper()
w3, config = get_web3(chain_name)
# Get private key
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
if not private_key:
raise ValueError("MAIN_WALLET_PRIVATE_KEY not found in environment variables")
account = w3.eth.account.from_key(private_key)
my_address = account.address
print(f"Connected to {chain_name} as {my_address}")
# Validate tokens
if token_in_sym not in config["tokens"]:
raise ValueError(f"Token {token_in_sym} not supported on {chain_name}")
if token_out_sym not in config["tokens"]:
raise ValueError(f"Token {token_out_sym} not supported on {chain_name}")
token_in_addr = config["tokens"][token_in_sym]
token_out_addr = config["tokens"][token_out_sym]
router_addr = config["router"]
abi_ver = config.get("abi_version", 1)
# Initialize Contracts
token_in_contract = w3.eth.contract(address=token_in_addr, abi=ERC20_ABI)
token_out_contract = w3.eth.contract(address=token_out_addr, abi=ERC20_ABI)
router_abi = SWAP_ROUTER_01_ABI if abi_ver == 1 else SWAP_ROUTER_02_ABI
router_contract = w3.eth.contract(address=router_addr, abi=router_abi)
# Decimals (ETH/BNB and their wrapped versions all use 18)
decimals_in = 18 if token_in_sym in ["ETH", "BNB"] else token_in_contract.functions.decimals().call()
amount_in_wei = int(Decimal(str(amount_in_readable)) * Decimal(10)**decimals_in)
print(f"Preparing to swap {amount_in_readable} {token_in_sym} -> {token_out_sym}")
# Handle Native Wrap
if token_in_sym in ["ETH", "BNB"]:
# Check native balance
native_balance = w3.eth.get_balance(my_address)
if native_balance < amount_in_wei:
raise ValueError(f"Insufficient native balance. Have {native_balance / 10**18}, need {amount_in_readable}")
# Check if we already have enough wrapped token
w_balance = token_in_contract.functions.balanceOf(my_address).call()
if w_balance < amount_in_wei:
wrap_eth(w3, token_in_addr, amount_in_wei - w_balance, private_key, my_address)
else:
# Check Token Balance
balance = token_in_contract.functions.balanceOf(my_address).call()
if balance < amount_in_wei:
raise ValueError(f"Insufficient balance. Have {balance / 10**decimals_in} {token_in_sym}, need {amount_in_readable}")
# Approve
approve_token(w3, token_in_contract, router_addr, amount_in_wei, private_key, my_address)
# Prepare Swap Params
used_fee = fee_tier if fee_tier else config["default_fee"]
amount_out_min = 0
if abi_ver == 1:
# Router 01 (Deadline in struct)
params = (
token_in_addr,
token_out_addr,
used_fee,
my_address,
int(time.time()) + 120, # deadline
amount_in_wei,
amount_out_min,
0 # sqrtPriceLimitX96
)
else:
# Router 02 (No Deadline in struct)
params = (
token_in_addr,
token_out_addr,
used_fee,
my_address,
# No deadline here
amount_in_wei,
amount_out_min,
0 # sqrtPriceLimitX96
)
print(f"Swapping... Fee Tier: {used_fee} | ABI: V{abi_ver}")
# Build Tx
tx_build = {
'from': my_address,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Estimate Gas
try:
gas_estimate = router_contract.functions.exactInputSingle(params).estimate_gas(tx_build)
tx_build['gas'] = int(gas_estimate * 1.2)
except Exception as e:
print(f"Gas estimation failed: {e}. Using default gas limit (500k).")
tx_build['gas'] = 500000
# Add Gas Price (Same robust logic as approve)
if chain_name == "BSC":
tx_build['gasPrice'] = w3.eth.gas_price
else:
try:
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
base_fee = latest_block['baseFeePerGas']
priority_fee = w3.to_wei(0.1, 'gwei')
tx_build['maxFeePerGas'] = int(base_fee * 1.5) + priority_fee
tx_build['maxPriorityFeePerGas'] = priority_fee
else:
tx_build['gasPrice'] = w3.eth.gas_price
except:
tx_build['gasPrice'] = w3.eth.gas_price
# Sign and Send
tx_func = router_contract.functions.exactInputSingle(params)
tx = tx_func.build_transaction(tx_build)
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Swap Tx Sent: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt.status == 1:
print("Swap Successful!")
else:
print("Swap Failed!")
# print(receipt) # verbose
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Universal Swapper for Arbitrum, Base, BSC")
parser.add_argument("chain", help="Chain name (ARBITRUM, BASE, BSC)")
parser.add_argument("token_in", help="Token to sell (e.g. USDC)")
parser.add_argument("token_out", help="Token to buy (e.g. WETH)")
parser.add_argument("amount", help="Amount to swap", type=float)
parser.add_argument("--fee", help="Fee tier (e.g. 500, 3000)", type=int)
args = parser.parse_args()
try:
execute_swap(args.chain, args.token_in, args.token_out, args.amount, args.fee)
except Exception as e:
print(f"Error: {e}")

56
gen_sim.py Normal file
View File

@ -0,0 +1,56 @@
from decimal import Decimal, getcontext
getcontext().prec = 60
P_entry = Decimal('3057.65')
capitals = [1000, 2000, 4000, 8000]
ranges = [0.005, 0.01, 0.02, 0.04, 0.05, 0.10]
tvl = Decimal('74530000')
vol24h = Decimal('23270000')
fee_tier = Decimal('0.0005')
hl_fee_rate = Decimal('0.00045')
print('# CLP Hedging Strategy Matrix (Jan 2026)')
print('\n*Based on ETH/USDC 0.05% Arbitrum Pool Stats: TVL $74.53M, 24h Vol $23.27M.*')
print('*Calculations include 0.045% round-trip Hyperliquid fees and concentration-adjusted Uniswap fees.*\n')
for cap in capitals:
print(f'## Capital: ${cap} USDC')
print('| Range | Strategy | Hedge Size | Margin (5x) | PnL Lower | PnL Upper | Est. Fees (1h) |')
print('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |')
for r in ranges:
Pa = P_entry * (Decimal('1') - Decimal(str(r)))
Pb = P_entry * (Decimal('1') + Decimal(str(r)))
L = Decimal(str(cap)) / (Decimal('2')*P_entry.sqrt() - Pa.sqrt() - P_entry/Pb.sqrt())
V_low = L * Pa * (Decimal('1')/Pa.sqrt() - Decimal('1')/Pb.sqrt())
V_high = L * (Pb.sqrt() - Pa.sqrt())
clp_loss = Decimal(str(cap)) - V_low
clp_gain = V_high - Decimal(str(cap))
d_entry = L * (Decimal('1')/P_entry.sqrt() - Decimal('1')/Pb.sqrt())
conc = Decimal('1') / (Decimal('1') - (Pa/Pb).sqrt())
fee_1h = (Decimal(str(cap)) * (vol24h / tvl) * fee_tier * conc) / Decimal('24')
hl_costs = Decimal(str(cap)) * hl_fee_rate
h_nd = (clp_loss + hl_costs) / (P_entry - Pa)
h_nu = (clp_gain - hl_costs) / (Pb - P_entry)
h_f = d_entry
for name, h in [('Neutral Down', h_nd), ('Neutral Up', h_nu), ('Fixed', h_f)]:
margin = (h * P_entry) / Decimal('5')
pnl_low = (h * (P_entry - Pa)) - clp_loss - hl_costs
pnl_high = (h * (P_entry - Pb)) + clp_gain - hl_costs
r_str = f"+/- {r*100} %"
h_str = f"{h:.4f} ETH"
m_str = f"${margin:.2f}"
pl_str = f"{pnl_low:+.2f}"
ph_str = f"{pnl_high:+.2f}"
f_str = f"${fee_1h:.2f}"
display_r = r_str if name == 'Neutral Down' else ''
display_f = f_str if name == 'Neutral Down' else ''
print(f'| {display_r} | {name} | {h_str} | {m_str} | {pl_str} | {ph_str} | {display_f} |')
print('\n')

View File

@ -42,7 +42,9 @@ console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(os.path.join(current_dir, 'logs', 'telegram_monitor.log'), encoding='utf-8')
from clp_config import TARGET_DEX
file_handler = logging.FileHandler(os.path.join(current_dir, 'logs', f'{TARGET_DEX}_telegram_monitor.log'), encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter())
file_formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s')
@ -56,9 +58,10 @@ TELEGRAM_ENABLED = os.getenv('TELEGRAM_MONITOR_ENABLED', 'False').lower() == 'tr
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID', '')
TELEGRAM_CHECK_INTERVAL = int(os.getenv('TELEGRAM_CHECK_INTERVAL_SECONDS', '60'))
from clp_config import STATUS_FILE
TELEGRAM_STATE_FILE = os.getenv('TELEGRAM_STATE_FILE', 'telegram_monitor_state.json')
TELEGRAM_TIMEOUT = int(os.getenv('TELEGRAM_TIMEOUT_SECONDS', '10'))
HEDGE_STATUS_FILE = os.getenv('HEDGE_STATUS_FILE', 'hedge_status.json')
HEDGE_STATUS_FILE = os.getenv('HEDGE_STATUS_FILE', STATUS_FILE)
class TelegramNotifier:
"""Handles Telegram API communication"""

107
tools/analyze_pool_data.py Normal file
View File

@ -0,0 +1,107 @@
import os
import json
import pandas as pd
import math
from decimal import Decimal
from datetime import datetime, timedelta
# --- SETTINGS ---
HISTORY_FILE = os.path.join("market_data", "pool_history.csv")
INVESTMENT_USD = 10000
RANGE_WIDTH_PCT = 0.10 # +/- 10%
REBALANCE_COST_PCT = 0.001 # 0.1% fee for rebalancing (swaps + gas)
def tick_to_price(tick):
return 1.0001 ** tick
def get_delta_from_pct(pct):
# tick_delta = log(1+pct) / log(1.0001)
return int(math.log(1 + pct) / math.log(1.0001))
def analyze():
if not os.path.exists(HISTORY_FILE):
print("No history file found. Run pool_scanner.py first.")
return
df = pd.read_csv(HISTORY_FILE)
df['timestamp'] = pd.to_datetime(df['timestamp'])
pools = df['pool_name'].unique()
results = []
for pool in pools:
pdf = df[df['pool_name'] == pool].sort_values('timestamp').copy()
if len(pdf) < 2: continue
# Initial Setup
start_row = pdf.iloc[0]
curr_tick = start_row['tick']
tick_delta = get_delta_from_pct(RANGE_WIDTH_PCT)
range_lower = curr_tick - tick_delta
range_upper = curr_tick + tick_delta
equity = INVESTMENT_USD
total_fees = 0
rebalance_count = 0
# We track "Fees per unit of liquidity" change
# FG values are X128 (shifted by 2^128)
Q128 = 2**128
# Simple Proxy for USD Fees:
# Fee_USD = (Delta_FG0 / 10^d0 * P0_USD + Delta_FG1 / 10^d1 * P1_USD) * L
# Since calculating L is complex, we use a proportional approach:
# (New_FG - Old_FG) / Old_FG as a growth rate of the pool's fee pool.
for i in range(1, len(pdf)):
row = pdf.iloc[i]
prev = pdf.iloc[i-1]
p_tick = row['tick']
# 1. Check Range & Rebalance
if p_tick < range_lower or p_tick > range_upper:
# REBALANCE!
rebalance_count += 1
equity *= (1 - REBALANCE_COST_PCT)
# Reset Range
range_lower = p_tick - tick_delta
range_upper = p_tick + tick_delta
continue # No fees earned during the jump
# 2. Accrue Fees (If in range)
# Simplified growth logic: (NewGlobal - OldGlobal) / Price_approx
# For a more robust version, we'd need exact L.
# Here we track the delta of the raw FG counters.
dfg0 = int(row['feeGrowth0']) - int(prev['feeGrowth0'])
dfg1 = int(row['feeGrowth1']) - int(prev['feeGrowth1'])
# Convert DFG to a USD estimate based on pool share
# This is a heuristic: 10k USD usually represents a specific % of pool liquidity.
# We assume a fixed liquidity L derived from 10k at start.
# L = 10000 / (sqrt(P) - sqrt(Pa)) ...
# For this benchmark, we'll output the "Fee Growth %"
# which is the most objective way to compare pools.
# (Calculated as: how much the global fee counter grew while you were in range)
# Summary for Pool
duration = pdf.iloc[-1]['timestamp'] - pdf.iloc[0]['timestamp']
results.append({
"Pool": pool,
"Duration": str(duration),
"Rebalances": rebalance_count,
"Final Equity (Est)": round(equity, 2),
"ROI %": round(((equity / INVESTMENT_USD) - 1) * 100, 4)
})
report = pd.DataFrame(results)
print("\n=== POOL PERFORMANCE REPORT ===")
print(report.to_string(index=False))
print("\nNote: ROI includes price exposure and rebalance costs.")
if __name__ == "__main__":
analyze()

View File

@ -0,0 +1,171 @@
import json
import time
import requests
import math
import os
from datetime import datetime
from statistics import mean, stdev
# --- Configuration ---
COINS = ["ETH"]
# Mapping of label to number of 1-minute periods
PERIODS_CONFIG = {
"37m": 37,
"3h": 3 * 60, # 180 minutes
"12h": 12 * 60, # 720 minutes
"24h": 24 * 60 # 1440 minutes
}
MA_PERIODS = [33, 44, 88, 144]
STD_DEV_MULTIPLIER = 1.6 # Standard deviation multiplier for bands
OUTPUT_FILE = os.path.join("market_data", "indicators.json")
API_URL = "https://api.hyperliquid.xyz/info"
UPDATE_INTERVAL = 60 # seconds
def fetch_candles(coin, interval="1m", lookback_minutes=1500):
"""
Fetches candle data from Hyperliquid.
We need at least enough candles for the longest period (1440).
Requesting slightly more to be safe.
"""
# Calculate startTime: now - (lookback_minutes * 60 * 1000)
# Hyperliquid expects startTime in milliseconds
end_time = int(time.time() * 1000)
start_time = end_time - (lookback_minutes * 60 * 1000)
payload = {
"type": "candleSnapshot",
"req": {
"coin": coin,
"interval": interval,
"startTime": start_time,
"endTime": end_time
}
}
try:
response = requests.post(API_URL, json=payload, timeout=10)
response.raise_for_status()
data = response.json()
# Data format is typically a list of dicts:
# {'t': 170..., 'T': 170..., 's': 'ETH', 'i': '1m', 'o': '...', 'c': '...', 'h': '...', 'l': '...', 'v': '...', 'n': ...}
# We need closing prices 'c'
candles = []
for c in data:
try:
# Ensure we parse 'c' (close) as float
candles.append(float(c['c']))
except (ValueError, KeyError):
continue
return candles
except Exception as e:
print(f"Error fetching candles for {coin}: {e}")
return []
def calculate_ma(prices, period):
"""Calculates Simple Moving Average."""
if len(prices) < period:
return None
return mean(prices[-period:])
def calculate_bb(prices, period, num_std_dev=2.0):
"""
Calculates Bollinger Bands for the LAST 'period' items in prices.
Returns {mid, upper, lower} or None if insufficient data.
"""
if len(prices) < period:
return None
# Take the last 'period' prices
window = prices[-period:]
try:
avg = mean(window)
# Population stdev or sample stdev? Usually sample (stdev) is used in finance or pandas default
if period > 1:
sd = stdev(window)
else:
sd = 0.0
upper = avg + (num_std_dev * sd)
lower = avg - (num_std_dev * sd)
return {
"mid": avg,
"upper": upper,
"lower": lower,
"std": sd
}
except Exception as e:
print(f"Error calculating BB: {e}")
return None
def main():
print(f"Starting Market Data Calculator for {COINS}")
print(f"BB Periods: {PERIODS_CONFIG}")
print(f"MA Periods: {MA_PERIODS}")
print(f"Output: {OUTPUT_FILE}")
# Ensure directory exists
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
while True:
try:
results = {
"last_updated": datetime.now().isoformat(),
"config": {
"std_dev_multiplier": STD_DEV_MULTIPLIER,
"ma_periods": MA_PERIODS
},
"data": {}
}
# Find the max needed history (BB vs MA)
max_bb = max(PERIODS_CONFIG.values()) if PERIODS_CONFIG else 0
max_ma = max(MA_PERIODS) if MA_PERIODS else 0
fetch_limit = max(max_bb, max_ma) + 60
for coin in COINS:
print(f"Fetching data for {coin}...", end="", flush=True)
prices = fetch_candles(coin, lookback_minutes=fetch_limit)
if not prices:
print(" Failed.")
continue
print(f" Got {len(prices)} candles.", end="", flush=True)
coin_results = {
"current_price": prices[-1] if prices else 0,
"bb": {},
"ma": {}
}
# Calculate BB
for label, period in PERIODS_CONFIG.items():
bb = calculate_bb(prices, period, num_std_dev=STD_DEV_MULTIPLIER)
coin_results["bb"][label] = bb if bb else "Insufficient Data"
# Calculate MA
for period in MA_PERIODS:
ma = calculate_ma(prices, period)
coin_results["ma"][str(period)] = ma if ma else "Insufficient Data"
results["data"][coin] = coin_results
print(" Done.")
# Save to file
with open(OUTPUT_FILE, 'w') as f:
json.dump(results, f, indent=4)
print(f"Updated {OUTPUT_FILE}")
except Exception as e:
print(f"Main loop error: {e}")
time.sleep(UPDATE_INTERVAL)
if __name__ == "__main__":
main()

192
tools/pool_scanner.py Normal file
View File

@ -0,0 +1,192 @@
import os
import json
import time
import pandas as pd
from decimal import Decimal
from datetime import datetime
from web3 import Web3
from dotenv import load_dotenv
# --- CONFIGURATION ---
CONFIG_FILE = os.path.join(os.path.dirname(__file__), "pool_scanner_config.json")
STATE_FILE = os.path.join("market_data", "pool_scanner_state.json")
HISTORY_FILE = os.path.join("market_data", "pool_history.csv")
load_dotenv()
# RPC MAP
RPC_MAP = {
"ARBITRUM": os.environ.get("MAINNET_RPC_URL"),
"BSC": os.environ.get("BNB_RPC_URL"),
"BASE": os.environ.get("BASE_RPC_URL")
}
# ABIS
POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
# PancakeSwap V3 uses uint32 for feeProtocol
PANCAKE_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
AERODROME_POOL_ABI = json.loads('''
[
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
]
''')
ERC20_ABI = json.loads('[{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}]')
def get_w3(chain):
url = RPC_MAP.get(chain)
if not url: return None
return Web3(Web3.HTTPProvider(url))
def load_state():
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
return json.load(f)
return {}
def save_state(state):
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def append_history(data):
df = pd.DataFrame([data])
header = not os.path.exists(HISTORY_FILE)
df.to_csv(HISTORY_FILE, mode='a', header=header, index=False)
def get_liquidity_for_amount(amount, sqrt_price_x96, tick_lower, tick_upper, decimal_diff):
# Simplified Liquidity Calc for 50/50 deposit simulation
# L = Amount / (sqrt(P) - sqrt(Pa)) for one side...
# For now, we assume simple V3 math or just track Fee Growth per Unit Liquidity
# Real simulation is complex.
# TRICK: We will track "Fee Growth per 1 Unit of Liquidity" directly (Raw X128).
# Then user can multiply by their theoretical L later.
return 1
def main():
print("Starting Pool Scanner...")
with open(CONFIG_FILE, 'r') as f:
pools = json.load(f)
state = load_state()
# Init Web3 cache
w3_instances = {}
for pool in pools:
name = pool['name']
chain = pool['chain']
# Fix Checksum
try:
addr = Web3.to_checksum_address(pool['pool_address'])
except Exception:
print(f" ❌ Invalid Address: {pool['pool_address']}")
continue
is_aero = pool.get('is_aerodrome', False)
print(f"Scanning {name} ({chain})...")
if chain not in w3_instances:
w3_instances[chain] = get_w3(chain)
w3 = w3_instances[chain]
if not w3 or not w3.is_connected():
print(f" ❌ RPC Error for {chain}")
continue
try:
if is_aero:
abi = AERODROME_POOL_ABI
elif chain == "BSC":
abi = PANCAKE_POOL_ABI
else:
abi = POOL_ABI
contract = w3.eth.contract(address=addr, abi=abi)
# Fetch Data
slot0 = contract.functions.slot0().call()
tick = slot0[1]
sqrt_price = slot0[0]
fg0 = contract.functions.feeGrowthGlobal0X128().call()
fg1 = contract.functions.feeGrowthGlobal1X128().call()
# Fetch Decimals (Once)
if name not in state:
t0 = contract.functions.token0().call()
t1 = contract.functions.token1().call()
d0 = w3.eth.contract(address=t0, abi=ERC20_ABI).functions.decimals().call()
d1 = w3.eth.contract(address=t1, abi=ERC20_ABI).functions.decimals().call()
state[name] = {
"init_tick": tick,
"init_fg0": fg0,
"init_fg1": fg1,
"decimals": [d0, d1],
"cumulative_fees_usd": 0.0,
"last_fg0": fg0,
"last_fg1": fg1
}
# Update State
prev = state[name]
diff0 = fg0 - prev['last_fg0']
diff1 = fg1 - prev['last_fg1']
# Calculate USD Value of Fees (Approx)
# Need Liquidity.
# If we assume 1 unit of Liquidity?
# Fee = Diff * L / 2^128
# Update Last
prev['last_fg0'] = fg0
prev['last_fg1'] = fg1
prev['last_tick'] = tick
prev['last_update'] = datetime.now().isoformat()
# Save History
record = {
"timestamp": datetime.now().isoformat(),
"pool_name": name,
"chain": chain,
"tick": tick,
"sqrtPriceX96": str(sqrt_price),
"feeGrowth0": str(fg0),
"feeGrowth1": str(fg1)
}
append_history(record)
print(f" ✅ Data recorded. Tick: {tick}")
except Exception as e:
print(f" ❌ Error: {e}")
save_state(state)
print("Scan complete.")
if __name__ == "__main__":
while True:
main()
time.sleep(600) # 10 minutes

146
tools/record_pool_depth.py Normal file
View File

@ -0,0 +1,146 @@
import os
import time
import csv
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from web3 import Web3
from dotenv import load_dotenv
# --- CONFIGURATION ---
POOL_ADDRESS = '0xC6962004f452bE9203591991D15f6b388e09E8D0' # ETH/USDC 0.05% Arbitrum
INTERVAL_SECONDS = 15 * 60 # 15 minutes
RANGE_PCT = 10.0 # Total scan range +/- 10%
STEP_PCT = 0.1 # Resolution step 0.1%
TVL_USD_BASELINE = Decimal('74530000') # Baseline TVL for concentration calculation
# Token Details
D0 = 18 # WETH
D1 = 6 # USDC
getcontext().prec = 60
load_dotenv()
# Setup Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("DEPTH_MONITOR")
# Ensure logs directory exists
os.makedirs('logs', exist_ok=True)
CSV_FILE = 'logs/pool_liquidity_depth.csv'
# ABI for Uniswap V3 Pool
POOL_ABI = [
{'inputs': [], 'name': 'liquidity', 'outputs': [{'internalType': 'uint128', 'name': '', 'type': 'uint128'}], 'stateMutability': 'view', 'type': 'function'},
{'inputs': [], 'name': 'slot0', 'outputs': [{'internalType': 'uint160', 'name': 'sqrtPriceX96', 'type': 'uint160'}, {'internalType': 'int24', 'name': 'tick', 'type': 'int24'}], 'stateMutability': 'view', 'type': 'function'},
{'inputs': [{'internalType': 'int24', 'name': 'tick', 'type': 'int24'}], 'name': 'ticks', 'outputs': [{'internalType': 'uint128', 'name': 'liquidityGross', 'type': 'uint128'}, {'internalType': 'int128', 'name': 'liquidityNet', 'type': 'int128'}], 'stateMutability': 'view', 'type': 'function'}
]
def get_price_from_tick(tick):
return (Decimal('1.0001') ** Decimal(str(tick))) * (Decimal('10') ** Decimal(str(D0 - D1)))
def get_liquidity_at_offsets(pool_contract, current_tick, current_liquidity):
"""
Samples liquidity at various price offsets.
Note: This samples initialized ticks to calculate L at specific price points.
"""
results = []
# Tick spacing for 0.05% pools is 10.
# 0.1% price move is approx 10 ticks.
ticks_per_step = 10
total_steps = int(RANGE_PCT / STEP_PCT)
# --- SCAN DOWN ---
l_running = Decimal(current_liquidity)
for i in range(1, total_steps + 1):
target_tick = current_tick - (i * ticks_per_step)
# Traverse ticks between previous and current target to update liquidity
for t in range(current_tick - (i-1)*ticks_per_step, target_tick - 1, -1):
data = pool_contract.functions.ticks(t).call()
if data[0] > 0: # initialized
l_running -= Decimal(data[1])
offset_pct = -round(i * STEP_PCT, 2)
px = get_price_from_tick(target_tick)
results.append({'offset': offset_pct, 'price': px, 'liquidity': l_running})
# --- SCAN UP ---
l_running = Decimal(current_liquidity)
for i in range(1, total_steps + 1):
target_tick = current_tick + (i * ticks_per_step)
for t in range(current_tick + (i-1)*ticks_per_step + 1, target_tick + 1):
data = pool_contract.functions.ticks(t).call()
if data[0] > 0:
l_running += Decimal(data[1])
offset_pct = round(i * STEP_PCT, 2)
px = get_price_from_tick(target_tick)
results.append({'offset': offset_pct, 'price': px, 'liquidity': l_running})
# Add center point
results.append({'offset': 0.0, 'price': get_price_from_tick(current_tick), 'liquidity': Decimal(current_liquidity)})
return sorted(results, key=lambda x: x['offset'])
def main():
rpc_url = os.environ.get('MAINNET_RPC_URL')
if not rpc_url:
logger.error("MAINNET_RPC_URL not found in .env")
return
w3 = Web3(Web3.HTTPProvider(rpc_url))
pool = w3.eth.contract(address=Web3.to_checksum_address(POOL_ADDRESS), abi=POOL_ABI)
# Initialize CSV if it doesn't exist
file_exists = os.path.isfile(CSV_FILE)
logger.info(f"Starting Depth Monitor for {POOL_ADDRESS}")
logger.info(f"Scan Range: +/-{RANGE_PCT}% | Resolution: {STEP_PCT}% | Interval: {INTERVAL_SECONDS/60}m")
while True:
try:
# 1. Fetch State
l_active = pool.functions.liquidity().call()
s0 = pool.functions.slot0().call()
curr_tick = s0[1]
curr_price = get_price_from_tick(curr_tick)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 2. Map Depth
depth_data = get_liquidity_at_offsets(pool, curr_tick, l_active)
# 3. Calculate Concentration & Save
with open(CSV_FILE, 'a', newline='') as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow(['timestamp', 'ref_price', 'offset_pct', 'target_price', 'liquidity', 'concentration'])
file_exists = True
for row in depth_data:
# L_full baseline for THIS specific price point
# Corrected L_full: (TVL * 10^6) / (2 * sqrtP)
# sqrtP_norm = sqrt(Price) / 10^((D0-D1)/2)
sqrtP_norm = row['price'].sqrt() / (Decimal('10') ** (Decimal(str(D0 - D1)) / Decimal('2')))
l_full = (TVL_USD_BASELINE * (Decimal('10')**Decimal('6'))) / (Decimal('2') * sqrtP_norm)
conc = row['liquidity'] / l_full
writer.writerow([
timestamp,
f"{curr_price:.4f}",
row['offset'],
f"{row['price']:.4f}",
f"{row['liquidity']:.0f}",
f"{conc:.2f}"
])
logger.info(f"Recorded depth snapshot at {curr_price:.2f}. Next in {INTERVAL_SECONDS/60}m.")
time.sleep(INTERVAL_SECONDS)
except Exception as e:
logger.error(f"Error in monitor loop: {e}")
time.sleep(60)
if __name__ == "__main__":
main()

349
tools/universal_swapper.py Normal file
View File

@ -0,0 +1,349 @@
import os
import sys
import json
import argparse
import time
from decimal import Decimal
from dotenv import load_dotenv
from web3 import Web3
# Load environment variables
load_dotenv()
# --- CONFIGURATION ---
# ABIs
ERC20_ABI = json.loads('''
[
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
]
''')
WETH_ABI = json.loads('''
[
{"inputs": [], "name": "deposit", "outputs": [], "stateMutability": "payable", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]
''')
# SwapRouter01 (With Deadline in struct) - e.g. Arbitrum 0xE592..., BSC
SWAP_ROUTER_01_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint200"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
# SwapRouter02 (NO Deadline in struct) - e.g. Base 0x2626...
SWAP_ROUTER_02_ABI = json.loads('''
[
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct IV3SwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
]
''')
CHAIN_CONFIG = {
"ARBITRUM": {
"rpc_env": "MAINNET_RPC_URL",
"chain_id": 42161,
"router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", # SwapRouter02
"abi_version": 2,
"tokens": {
"USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"WETH": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"ETH": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # Alias to WETH for wrapping
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 500
},
"BASE": {
"rpc_env": "BASE_RPC_URL",
"chain_id": 8453,
"router": "0x2626664c2603336E57B271c5C0b26F421741e481", # SwapRouter02
"abi_version": 2,
"tokens": {
"USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"WETH": "0x4200000000000000000000000000000000000006",
"ETH": "0x4200000000000000000000000000000000000006", # Alias to WETH for wrapping
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 500
},
"BASE_AERO": {
"rpc_env": "BASE_RPC_URL",
"chain_id": 8453,
"router": "0xbe6D8f0D397708D99755B7857067757f97174d7d", # Aerodrome Slipstream SwapRouter
"abi_version": 1, # Router requires deadline (Standard SwapRouter01 style)
"tokens": {
"USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"WETH": "0x4200000000000000000000000000000000000006",
"ETH": "0x4200000000000000000000000000000000000006",
"CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
},
"default_fee": 1 # TickSpacing 1 (0.01%)
},
"BSC": {
"rpc_env": "BNB_RPC_URL",
"chain_id": 56,
"router": "0x1b81D678ffb9C0263b24A97847620C99d213eB14", # PancakeSwap V3
"abi_version": 1,
"tokens": {
"USDC": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
"WBNB": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"BNB": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" # Alias to WBNB for wrapping
},
"default_fee": 500
}
}
def get_web3(chain_name):
config = CHAIN_CONFIG.get(chain_name.upper())
if not config:
raise ValueError(f"Unsupported chain: {chain_name}")
rpc_url = os.environ.get(config["rpc_env"])
if not rpc_url:
raise ValueError(f"RPC URL not found in environment for {config['rpc_env']}")
w3 = Web3(Web3.HTTPProvider(rpc_url))
if not w3.is_connected():
raise ConnectionError(f"Failed to connect to {chain_name} RPC")
return w3, config
def approve_token(w3, token_contract, spender_address, amount, private_key, my_address):
"""
Checks allowance and approves if necessary.
Robust gas handling.
"""
allowance = token_contract.functions.allowance(my_address, spender_address).call()
if allowance >= amount:
print(f"Token already approved (Allowance: {allowance})")
return True
print(f"Approving token... (Current: {allowance}, Needed: {amount})")
# Build tx base
tx_params = {
'from': my_address,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Determine Gas Strategy
try:
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
# EIP-1559
base_fee = latest_block['baseFeePerGas']
priority_fee = w3.to_wei(0.1, 'gwei') # Conservative priority
tx_params['maxFeePerGas'] = int(base_fee * 1.5) + priority_fee
tx_params['maxPriorityFeePerGas'] = priority_fee
else:
# Legacy
tx_params['gasPrice'] = w3.eth.gas_price
except Exception as e:
print(f"Error determining gas strategy: {e}. Fallback to w3.eth.gas_price")
tx_params['gasPrice'] = w3.eth.gas_price
# Build transaction
tx = token_contract.functions.approve(spender_address, 2**256 - 1).build_transaction(tx_params)
# Sign and send
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Approval Tx sent: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt.status == 1:
print("Approval successful.")
return True
else:
print("Approval failed.")
return False
def wrap_eth(w3, weth_address, amount_wei, private_key, my_address):
"""
Wraps native ETH/BNB to WETH/WBNB.
"""
print(f"Wrapping native token to wrapped version...")
weth_contract = w3.eth.contract(address=weth_address, abi=WETH_ABI)
tx_params = {
'from': my_address,
'value': amount_wei,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Gas logic (Simplified)
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
base_fee = latest_block['baseFeePerGas']
tx_params['maxFeePerGas'] = int(base_fee * 1.5) + w3.to_wei(0.1, 'gwei')
tx_params['maxPriorityFeePerGas'] = w3.to_wei(0.1, 'gwei')
else:
tx_params['gasPrice'] = w3.eth.gas_price
tx = weth_contract.functions.deposit().build_transaction(tx_params)
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Wrapping Tx sent: {tx_hash.hex()}")
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Wrapping successful.")
def execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_readable, fee_tier=None, slippage_pct=0.5):
"""
Main function to execute swap.
"""
chain_name = chain_name.upper()
token_in_sym = token_in_sym.upper()
token_out_sym = token_out_sym.upper()
w3, config = get_web3(chain_name)
# Get private key
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
if not private_key:
raise ValueError("MAIN_WALLET_PRIVATE_KEY not found in environment variables")
account = w3.eth.account.from_key(private_key)
my_address = account.address
print(f"Connected to {chain_name} as {my_address}")
# Validate tokens
if token_in_sym not in config["tokens"]:
raise ValueError(f"Token {token_in_sym} not supported on {chain_name}")
if token_out_sym not in config["tokens"]:
raise ValueError(f"Token {token_out_sym} not supported on {chain_name}")
token_in_addr = config["tokens"][token_in_sym]
token_out_addr = config["tokens"][token_out_sym]
router_addr = config["router"]
abi_ver = config.get("abi_version", 1)
# Initialize Contracts
token_in_contract = w3.eth.contract(address=token_in_addr, abi=ERC20_ABI)
token_out_contract = w3.eth.contract(address=token_out_addr, abi=ERC20_ABI)
router_abi = SWAP_ROUTER_01_ABI if abi_ver == 1 else SWAP_ROUTER_02_ABI
router_contract = w3.eth.contract(address=router_addr, abi=router_abi)
# Decimals (ETH/BNB and their wrapped versions all use 18)
decimals_in = 18 if token_in_sym in ["ETH", "BNB"] else token_in_contract.functions.decimals().call()
amount_in_wei = int(Decimal(str(amount_in_readable)) * Decimal(10)**decimals_in)
print(f"Preparing to swap {amount_in_readable} {token_in_sym} -> {token_out_sym}")
# Handle Native Wrap
if token_in_sym in ["ETH", "BNB"]:
# Check native balance
native_balance = w3.eth.get_balance(my_address)
if native_balance < amount_in_wei:
raise ValueError(f"Insufficient native balance. Have {native_balance / 10**18}, need {amount_in_readable}")
# Check if we already have enough wrapped token
w_balance = token_in_contract.functions.balanceOf(my_address).call()
if w_balance < amount_in_wei:
wrap_eth(w3, token_in_addr, amount_in_wei - w_balance, private_key, my_address)
else:
# Check Token Balance
balance = token_in_contract.functions.balanceOf(my_address).call()
if balance < amount_in_wei:
raise ValueError(f"Insufficient balance. Have {balance / 10**decimals_in} {token_in_sym}, need {amount_in_readable}")
# Approve
approve_token(w3, token_in_contract, router_addr, amount_in_wei, private_key, my_address)
# Prepare Swap Params
used_fee = fee_tier if fee_tier else config["default_fee"]
amount_out_min = 0
if abi_ver == 1:
# Router 01 (Deadline in struct)
params = (
token_in_addr,
token_out_addr,
used_fee,
my_address,
int(time.time()) + 120, # deadline
amount_in_wei,
amount_out_min,
0 # sqrtPriceLimitX96
)
else:
# Router 02 (No Deadline in struct)
params = (
token_in_addr,
token_out_addr,
used_fee,
my_address,
# No deadline here
amount_in_wei,
amount_out_min,
0 # sqrtPriceLimitX96
)
print(f"Swapping... Fee Tier: {used_fee} | ABI: V{abi_ver}")
# Build Tx
tx_build = {
'from': my_address,
'nonce': w3.eth.get_transaction_count(my_address),
}
# Estimate Gas
try:
gas_estimate = router_contract.functions.exactInputSingle(params).estimate_gas(tx_build)
tx_build['gas'] = int(gas_estimate * 1.2)
except Exception as e:
print(f"Gas estimation failed: {e}. Using default gas limit (500k).")
tx_build['gas'] = 500000
# Add Gas Price (Same robust logic as approve)
if chain_name == "BSC":
tx_build['gasPrice'] = w3.eth.gas_price
else:
try:
latest_block = w3.eth.get_block('latest')
if 'baseFeePerGas' in latest_block:
base_fee = latest_block['baseFeePerGas']
priority_fee = w3.to_wei(0.1, 'gwei')
tx_build['maxFeePerGas'] = int(base_fee * 1.5) + priority_fee
tx_build['maxPriorityFeePerGas'] = priority_fee
else:
tx_build['gasPrice'] = w3.eth.gas_price
except:
tx_build['gasPrice'] = w3.eth.gas_price
# Sign and Send
tx_func = router_contract.functions.exactInputSingle(params)
tx = tx_func.build_transaction(tx_build)
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Swap Tx Sent: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt.status == 1:
print("Swap Successful!")
else:
print("Swap Failed!")
# print(receipt) # verbose
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Universal Swapper for Arbitrum, Base, BSC")
parser.add_argument("chain", help="Chain name (ARBITRUM, BASE, BSC)")
parser.add_argument("token_in", help="Token to sell (e.g. USDC)")
parser.add_argument("token_out", help="Token to buy (e.g. WETH)")
parser.add_argument("amount", help="Amount to swap", type=float)
parser.add_argument("--fee", help="Fee tier (e.g. 500, 3000)", type=int)
args = parser.parse_args()
try:
execute_swap(args.chain, args.token_in, args.token_out, args.amount, args.fee)
except Exception as e:
print(f"Error: {e}")