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 ---
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 "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 "OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior "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 "VALUE_REFERENCE": "USD", # Base currency for all calculations
# Range Settings # Range Settings
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price) "RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting "SLIPPAGE_TOLERANCE": Decimal("0.05"), # Max allowed slippage for swaps and minting
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions "TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings # 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 "MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
# Unified Hedger Settings # Unified Hedger Settings
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
"LEVERAGE": 5, # Leverage to use on Hyperliquid "LEVERAGE": 5, # Leverage to use on Hyperliquid
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range "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_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_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 "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 "MIN_ORDER_VALUE_USD": Decimal("10.0"), # Minimum order size allowed by Hyperliquid
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.2"), # Expansion factor for thresholds "DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.2"), # Expansion factor for thresholds
"MIN_TIME_BETWEEN_TRADES": 60, # Cooldown (seconds) between rebalance trades "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 "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 "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 "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) "MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking "SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced) "ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
@ -72,6 +75,9 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC "TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500, "POOL_FEE": 500,
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
"RANGE_WIDTH_PCT": Decimal("0.0075"),
}, },
"UNISWAP_wide": { "UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide", "NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
@ -98,6 +104,7 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT "TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", "WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100, "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"), "RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000, "TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"), "MIN_HEDGE_THRESHOLD": Decimal("0.015"),
@ -123,6 +130,34 @@ CLP_PROFILES = {
"TARGET_INVESTMENT_AMOUNT": 200, "TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD", "VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10") "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) project_root = os.path.dirname(current_dir)
sys.path.append(project_root) sys.path.append(project_root)
# Import local modules # Ensure root logger is clean
try: logging.getLogger().handlers.clear()
from logging_utils import setup_logging logging.basicConfig(level=logging.INFO)
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)
from eth_account import Account from eth_account import Account
from hyperliquid.exchange import Exchange from hyperliquid.exchange import Exchange
from hyperliquid.info import Info from hyperliquid.info import Info
from hyperliquid.utils import constants 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 # Load environment variables
dotenv_path = os.path.join(current_dir, '.env') dotenv_path = os.path.join(current_dir, '.env')
@ -213,7 +208,7 @@ class HyperliquidStrategy:
else: # >=5% range else: # >=5% range
return Decimal("0.075") # Standard for wide ranges 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), # Note: current_short_size here is virtual (just for this specific strategy),
# but the unified hedger will use the 'target_short' output primarily. # but the unified hedger will use the 'target_short' output primarily.
@ -221,17 +216,33 @@ class HyperliquidStrategy:
# --- ASYMMETRIC COMPENSATION --- # --- ASYMMETRIC COMPENSATION ---
adj_pct = Decimal("0.0") adj_pct = Decimal("0.0")
range_width = self.high_range - self.low_range
if range_width > 0: if strategy_type == "ASYMMETRIC":
dist = current_price - self.entry_price range_width = self.high_range - self.low_range
half_width = range_width / Decimal("2")
norm_dist = dist / half_width if range_width > 0:
max_boost = self.get_compensation_boost() dist = current_price - self.entry_price
adj_pct = -norm_dist * max_boost half_width = range_width / Decimal("2")
adj_pct = max(-max_boost, min(max_boost, adj_pct)) 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) adjusted_target_short = raw_target_short * (Decimal("1.0") + adj_pct)
diff = adjusted_target_short - abs(current_short_size) diff = adjusted_target_short - abs(current_short_size)
@ -273,12 +284,23 @@ class UnifiedHedger:
# Market Data Cache # Market Data Cache
self.last_prices = {} 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_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List) # Shadow Orders (Global List)
self.shadow_orders = [] 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() self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}") logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -286,6 +308,7 @@ class UnifiedHedger:
def _init_coin_configs(self): def _init_coin_configs(self):
"""Pre-load configuration for known coins from CLP_PROFILES.""" """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(): for profile_key, profile_data in CLP_PROFILES.items():
symbol = profile_data.get("COIN_SYMBOL") symbol = profile_data.get("COIN_SYMBOL")
if symbol: if symbol:
@ -297,6 +320,18 @@ class UnifiedHedger:
# Update with Profile Specifics # Update with Profile Specifics
self.coin_configs[symbol].update(profile_data) 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: def _get_sz_decimals(self, coin: str) -> int:
try: try:
meta = self.info.meta() 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]['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]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN') 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: except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.") logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -478,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms, "start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)), "pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 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 "hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic "entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN') "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}") 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: except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}") logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -652,7 +699,7 @@ class UnifiedHedger:
price = to_decimal(mids[coin]) price = to_decimal(mids[coin])
self.last_prices[coin] = price self.last_prices[coin] = price
# Update Price History # Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = [] if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price) self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0) 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 if coin not in self.last_prices: continue
price = self.last_prices[coin] 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 Logic
calc = strat.calculate_rebalance(price, Decimal("0")) calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates: 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': # --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# If Closing, we want target to be 0 for this strategy # Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
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 aggregates[coin]['is_closing'] = True
# Do not add to target_short
else: 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]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct'] 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: if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True 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) # Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots) self.check_shadow_orders(l2_snapshots)
@ -706,255 +796,256 @@ class UnifiedHedger:
for coin in coins_to_process: 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}) 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 if price == 0: continue
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short) target_short_abs = data['target_short']
target_position = -target_short_abs # We want to be Short, so negative size target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0")) current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
diff_abs = abs(diff) diff_abs = abs(diff)
# Thresholds # Thresholds
config = self.coin_configs.get(coin, {}) config = self.coin_configs.get(coin, {})
min_thresh = config.get("min_threshold", Decimal("0.008")) min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
# Volatility Multiplier
vol_pct = self.calculate_volatility(coin) vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005") 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") 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")) base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult) thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct) dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
# FORCE EDGE CLEANUP if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True) if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
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
# Check Trigger
action_needed = diff_abs > dynamic_thresh action_needed = diff_abs > dynamic_thresh
# Determine Intent (Moved UP for Order Logic)
is_buy_bool = diff > 0 is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL" side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders # Manage Existing Orders
existing_orders = orders_map.get(coin, []) existing_orders = orders_map.get(coin, [])
force_taker_retry = False 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 order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015")) price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders: for o in existing_orders:
o_oid = o['oid'] o_oid = o['oid']
o_price = to_decimal(o['limitPx']) 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)) 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) 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 order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > maker_timeout: if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.") logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
continue continue
# Fishing Timeout Check if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
if enable_fishing and is_same_side and order_age_sec > fishing_timeout: logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
force_taker_retry = True 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 order_matched = True
if int(time.time()) % 10 == 0: if int(time.time()) % 15 == 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") logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break break
else: else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})") logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
# --- EXECUTION LOGIC --- # Determine Urgency / Bypass Cooldown
if not order_matched: bypass_cooldown = False
if action_needed or force_taker_retry: 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 bypass_cooldown = False
force_maker = False force_taker_retry = False # Disable taker retry from fishing
# 0. Forced Taker Retry (Fishing Timeout) # --- ASYMMETRIC HEDGE CHECK ---
if force_taker_retry: is_asymmetric_blocked = False
bypass_cooldown = True p_mid_asym = Decimal("0")
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker") # strategy_type already fetched above
# 1. Urgent Closing -> Taker if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
elif data.get('is_closing', False): total_L_asym = Decimal("0")
bypass_cooldown = True for k_strat, strat_inst in self.strategies.items():
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit") if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
# 2. Ghost/Cleanup -> Maker gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
elif data.get('contributors', 0) == 0: if gamma_asym > 0:
if time.time() - self.startup_time > 5: p_mid_asym = price - (diff_abs / gamma_asym)
force_maker = True if not data.get('is_at_edge', False) and price >= p_mid_asym:
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce") is_asymmetric_blocked = True
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.")
# --- 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) 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) if bypass_cooldown or (time.time() - last_trade > min_time):
can_trade = False if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
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)
levels = l2_snapshots[coin]['levels'] 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']) logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
ask = to_decimal(levels[1][0]['px']) 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 logger.info("Sleeping 10s for position update...")
create_shadow = False 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) # --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo) 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: # Update all active strategies for this coin in JSON
# If Force Maker -> Alo if total_L_log > 0 and price > 0:
# Else if Urgent -> Ioc for k_strat, strat_inst in self.strategies.items():
# Else if Enable Fishing -> Alo if self.strategy_states[k_strat]['coin'] != coin: continue
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
# Let's clarify: # CLP Value Calc
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo. def get_clp_value(p, s):
# New logic: if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
# If bypass_cooldown -> Ioc if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
# Else -> Alo (Fishing) return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
if bypass_cooldown and not force_maker: clp_curr_val = get_clp_value(price, strat_inst)
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"
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) # USE TRACKED HEDGE ENTRY PRICE
if oid: h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
self.last_trade_times[coin] = time.time() if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
# 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)
else: else:
# Cooldown log hedge_pnl_curr = Decimal("0")
pass
else: fee_close_curr = (target_size * price) * Decimal("0.000432")
# Action NOT needed uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# 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'])
# --- IDLE LOGGING (Restored Format) --- # Retrieve Realized PnL & Fees from State
# Calculate aggregate Gamma to estimate triggers realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
# Gamma = 0.5 * Sum(L) * P^-1.5 realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
# 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
if total_L > 0 and price > 0: # Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5"))) tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
if gamma > 0:
# Equilibrium Price (Diff = 0)
p_mid = price + (diff / gamma)
# Triggers cur_hl_cost = realized_fees + fee_close_curr
p_buy = price + (dynamic_thresh + diff) / gamma
p_sell = price - (dynamic_thresh - diff) / gamma
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 "" pad = " " if coin == "BNB" else ""
adj_val = data.get('adj_pct', Decimal("0")) * 100
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0")) unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl_total = Decimal("0") closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees_total = Decimal("0") fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
for k, s_state in self.strategy_states.items(): total_pnl = (closed_pnl - fees) + unrealized
if s_state['coin'] == coin:
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
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 "" # Recalculate for logging (including bounds)
tot_pnl_pad = " " if total_pnl >= 0 else "" 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}") # Use Custom Fixed Target if exists
else: target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
if int(time.time()) % 30 == 0: 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})") logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else: else:
if int(time.time()) % 30 == 0: logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1)) 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) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
# --- ABIs --- from clp_abis import (
# (Kept minimal for brevity, normally would load from files) NONFUNGIBLE_POSITION_MANAGER_ABI,
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads(''' UNISWAP_V3_POOL_ABI,
[ ERC20_ABI,
{"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"}, UNISWAP_V3_FACTORY_ABI,
{"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"}, AERODROME_FACTORY_ABI,
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, AERODROME_POOL_ABI,
{"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"}, AERODROME_NPM_ABI,
{"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"}, SWAP_ROUTER_ABI,
{"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"}, WETH9_ABI
{"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_config import get_current_config, STATUS_FILE from clp_config import get_current_config, STATUS_FILE
from tools.universal_swapper import execute_swap
# --- GET ACTIVE DEX CONFIG --- # --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_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 --- # --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60) MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True) 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) TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000) INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01")) 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")) SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30) 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 --- # --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"] 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"] UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1) # Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"] 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': if pool_address == '0x0000000000000000000000000000000000000000':
return None, None 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 { return {
"token0_address": token0_address, "token1_address": token1_address, "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: 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. 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) token0 = clean_address(token0)
token1 = clean_address(token1) 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 of Token0 in terms of Token1
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1) price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
swap_call = None chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
token_in, token_out = None, None
amount_in = 0
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: if deficit0 > 0 and bal1 > amount1_needed:
# Need T0 (ETH), Have extra T1 (USDC) # Need T0 (ETH), Have extra T1 (USDC)
# Swap T1 -> T0 # Swap T1 -> T0
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus1 = bal1 - amount1_needed surplus1 = bal1 - amount1_needed
if surplus1 >= amount_in_needed: if surplus1 >= amount_in_needed:
token_in, token_out = token1, token0 # Get Symbols
amount_in = amount_in_needed 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}") logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
else: else:
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}") 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 surplus0 = bal0 - amount0_needed
if surplus0 >= amount_in_needed: if surplus0 >= amount_in_needed:
token_in, token_out = token0, token1 token_in_sym = token0_c.functions.symbol().call().upper()
amount_in = amount_in_needed 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}") logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
else: else:
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}") logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
if token_in and amount_in > 0: if token_in_sym and amount_in_float > 0:
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...") 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 return False
except Exception as e:
params = ( logger.error(f"❌ Universal Swap Failed: {e}")
token_in, token_out, POOL_FEE, account.address, return False
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
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}") logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False 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)) amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
# 3. Mint # 3. Mint
params = ( base_params = [
token0, token1, POOL_FEE, token0, token1, POOL_FEE,
tick_lower, tick_upper, tick_lower, tick_upper,
amount0, amount1, amount0, amount1,
amount0_min, amount1_min, amount0_min, amount1_min,
account.address, account.address,
int(time.time()) + 180 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") 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) save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}") 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 --- # --- MAIN LOOP ---
def main(): 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...") logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
load_dotenv(override=True) load_dotenv(override=True)
@ -722,9 +810,23 @@ def main():
logger.info(f"👤 Wallet: {account.address}") logger.info(f"👤 Wallet: {account.address}")
# Contracts # 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_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) router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
while True: while True:
@ -817,7 +919,21 @@ def main():
pnl_unrealized = current_pos_value_usd - initial_value pnl_unrealized = current_pos_value_usd - initial_value
total_pnl_usd = pnl_unrealized + total_fees_usd 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}") logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
# --- KPI LOGGING --- # --- KPI LOGGING ---
@ -845,6 +961,32 @@ def main():
log_kpi_snapshot(snapshot) 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: if not in_range and CLOSE_POSITION_ENABLED:
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)") logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
update_position_status(token_id, "CLOSING") update_position_status(token_id, "CLOSING")
@ -875,14 +1017,73 @@ def main():
fee = POOL_FEE fee = POOL_FEE
pool_addr = factory.functions.getPool(token0, token1, fee).call() 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) pool_data = get_pool_dynamic_data(pool_c)
if pool_data: if pool_data:
tick = pool_data['tick'] tick = pool_data['tick']
# Define Range (+/- 2.5%)
# log(1.025) / log(1.0001) approx 247 tick delta # --- PRE-CALCULATE ESSENTIALS ---
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001)) # 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 # Fetch actual tick spacing from pool
tick_spacing = pool_c.functions.tickSpacing().call() tick_spacing = pool_c.functions.tickSpacing().call()
@ -893,28 +1094,10 @@ def main():
# Calculate Amounts # Calculate Amounts
# Target Value logic # 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 # Determine Investment Value in Token1 terms
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC)) 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") investment_val_token1 = Decimal("0")
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX": 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']) 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): 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) minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
if minted: if minted:
# Calculate entry price and amounts for JSON compatibility # --- DISABLE FORCE MODE AFTER FIRST MINT ---
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1) 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_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1)) fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
@ -970,6 +1176,8 @@ def main():
"range_lower": round(r_lower, 4), "range_lower": round(r_lower, 4),
"token0_decimals": d0, "token0_decimals": d0,
"token1_decimals": d1, "token1_decimals": d1,
"range_mode": current_range_mode,
"range_width_initial": float(active_range_width),
"timestamp_open": int(time.time()), "timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S") "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. | | `telegram_monitor.py` | Telegram bot for notifications. |
| `{TARGET_DEX}_status.json` | **Critical:** Shared state file acting as the database between Manager and Hedger. | | `{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.** | | `.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. | | `tools/` | Utility scripts, including the Git Agent for auto-backups. |
| `logs/` | Detailed logs for all processes. | | `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`) ### Components
Required variables for operation: * **`tests/backtest/backtester.py`**: Event-driven engine mocking Web3/Hyperliquid interactions.
```env * **`tests/backtest/mocks.py`**: Stateful simulator handling balance tracking, V3 tick math, and fee accrual.
# Blockchain * **`tests/backtest/grid_search.py`**: Optimization runner to test parameter combinations (Range Width, Hedging Threshold).
MAINNET_RPC_URL=... # Arbitrum * **`tests/backtest/analyze_results.py`**: Helper to interpret simulation CSV results.
BNB_RPC_URL=... # BNB Chain
BASE_RPC_URL=... # Base
MAIN_WALLET_PRIVATE_KEY=...
MAIN_WALLET_ADDRESS=...
# Hyperliquid ### Progress Status (Jan 1, 2026)
HEDGER_PRIVATE_KEY=... # Usually same as Main Wallet or specialized sub-account * **Completed:**
* Simulation loop runs end-to-end (Mint -> Accrue Fees -> Close).
# Telegram * Fixed Mock Pricing logic (handling inverted T0/T1 pairs like USDT/WBNB).
TELEGRAM_BOT_TOKEN=... * Implemented realistic Fee Accrual based on Trade Volume + Market Share.
TELEGRAM_CHAT_ID=... * Verified "In Range" detection and position lifecycle.
TELEGRAM_MONITOR_ENABLED=True * **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.
### Strategy Config (`clp_config.py`) * **Final Optimization:** Run the `grid_search.py` with the corrected Market Share (0.02%) and lower thresholds to find the profitable "Sweet Spot".
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
```
## Logic Details ## Logic Details

View File

@ -201,9 +201,9 @@
{ {
"type": "AUTOMATIC", "type": "AUTOMATIC",
"token_id": 6164702, "token_id": 6164702,
"status": "OPEN", "status": "CLOSED",
"target_value": 981.88, "target_value": 993.41,
"entry_price": 846.4517, "entry_price": 866.3337,
"amount0_initial": 490.942, "amount0_initial": 490.942,
"amount1_initial": 0.58, "amount1_initial": 0.58,
"liquidity": "8220443727732589279738", "liquidity": "8220443727732589279738",
@ -212,7 +212,471 @@
"token0_decimals": 18, "token0_decimals": 18,
"token1_decimals": 18, "token1_decimals": 18,
"timestamp_open": 1767164052, "timestamp_open": 1767164052,
"hedge_TotPnL": -0.026171, "hedge_TotPnL": -3.587319,
"hedge_fees_paid": 0.097756 "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", "type": "AUTOMATIC",
"token_id": 5182179, "token_id": 5182179,
"status": "OPEN", "status": "CLOSED",
"target_value": 1993.84, "target_value": 1993.84,
"entry_price": 2969.9855, "entry_price": 2969.9855,
"amount0_initial": 0.3347, "amount0_initial": 0.3347,
@ -299,7 +299,607 @@
"token0_decimals": 18, "token0_decimals": 18,
"token1_decimals": 6, "token1_decimals": 6,
"timestamp_open": 1766968369, "timestamp_open": 1766968369,
"hedge_TotPnL": -5.078135, "hedge_TotPnL": -62.166433,
"hedge_fees_paid": 2.029157 "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 ---
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 "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 "OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior "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 "VALUE_REFERENCE": "USD", # Base currency for all calculations
# Range Settings # Range Settings
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price) "RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting "SLIPPAGE_TOLERANCE": Decimal("0.03"), # Max allowed slippage for swaps and minting
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions "TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings # 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 "MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
# Unified Hedger Settings # Unified Hedger Settings
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
"LEVERAGE": 5, # Leverage to use on Hyperliquid "LEVERAGE": 5, # Leverage to use on Hyperliquid
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range "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_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 "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 "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 "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) "MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking "SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced) "ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
@ -72,6 +75,9 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC "TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500, "POOL_FEE": 500,
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
"RANGE_WIDTH_PCT": Decimal("0.0075"),
}, },
"UNISWAP_wide": { "UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide", "NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
@ -98,6 +104,7 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT "TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", "WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100, "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"), "RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000, "TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"), "MIN_HEDGE_THRESHOLD": Decimal("0.015"),
@ -123,6 +130,34 @@ CLP_PROFILES = {
"TARGET_INVESTMENT_AMOUNT": 200, "TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD", "VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10") "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) project_root = os.path.dirname(current_dir)
sys.path.append(project_root) sys.path.append(project_root)
# Import local modules # Ensure root logger is clean
try: logging.getLogger().handlers.clear()
from logging_utils import setup_logging logging.basicConfig(level=logging.INFO)
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)
from eth_account import Account from eth_account import Account
from hyperliquid.exchange import Exchange from hyperliquid.exchange import Exchange
from hyperliquid.info import Info from hyperliquid.info import Info
from hyperliquid.utils import constants 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 # Load environment variables
dotenv_path = os.path.join(current_dir, '.env') dotenv_path = os.path.join(current_dir, '.env')
@ -213,7 +208,7 @@ class HyperliquidStrategy:
else: # >=5% range else: # >=5% range
return Decimal("0.075") # Standard for wide ranges 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), # Note: current_short_size here is virtual (just for this specific strategy),
# but the unified hedger will use the 'target_short' output primarily. # but the unified hedger will use the 'target_short' output primarily.
@ -221,17 +216,33 @@ class HyperliquidStrategy:
# --- ASYMMETRIC COMPENSATION --- # --- ASYMMETRIC COMPENSATION ---
adj_pct = Decimal("0.0") adj_pct = Decimal("0.0")
range_width = self.high_range - self.low_range
if range_width > 0: if strategy_type == "ASYMMETRIC":
dist = current_price - self.entry_price range_width = self.high_range - self.low_range
half_width = range_width / Decimal("2")
norm_dist = dist / half_width if range_width > 0:
max_boost = self.get_compensation_boost() dist = current_price - self.entry_price
adj_pct = -norm_dist * max_boost half_width = range_width / Decimal("2")
adj_pct = max(-max_boost, min(max_boost, adj_pct)) 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) adjusted_target_short = raw_target_short * (Decimal("1.0") + adj_pct)
diff = adjusted_target_short - abs(current_short_size) diff = adjusted_target_short - abs(current_short_size)
@ -273,12 +284,23 @@ class UnifiedHedger:
# Market Data Cache # Market Data Cache
self.last_prices = {} 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_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List) # Shadow Orders (Global List)
self.shadow_orders = [] 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() self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}") logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -286,6 +308,7 @@ class UnifiedHedger:
def _init_coin_configs(self): def _init_coin_configs(self):
"""Pre-load configuration for known coins from CLP_PROFILES.""" """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(): for profile_key, profile_data in CLP_PROFILES.items():
symbol = profile_data.get("COIN_SYMBOL") symbol = profile_data.get("COIN_SYMBOL")
if symbol: if symbol:
@ -297,6 +320,18 @@ class UnifiedHedger:
# Update with Profile Specifics # Update with Profile Specifics
self.coin_configs[symbol].update(profile_data) 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: def _get_sz_decimals(self, coin: str) -> int:
try: try:
meta = self.info.meta() 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]['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]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN') 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: except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.") logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -478,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms, "start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)), "pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 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 "hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic "entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN') "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}") 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: except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}") logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -652,7 +699,7 @@ class UnifiedHedger:
price = to_decimal(mids[coin]) price = to_decimal(mids[coin])
self.last_prices[coin] = price self.last_prices[coin] = price
# Update Price History # Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = [] if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price) self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0) 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 if coin not in self.last_prices: continue
price = self.last_prices[coin] 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 Logic
calc = strat.calculate_rebalance(price, Decimal("0")) calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates: 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': # --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# If Closing, we want target to be 0 for this strategy # Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
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 aggregates[coin]['is_closing'] = True
# Do not add to target_short
else: 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]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct'] 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: if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True 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) # Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots) self.check_shadow_orders(l2_snapshots)
@ -706,255 +796,256 @@ class UnifiedHedger:
for coin in coins_to_process: 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}) 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 if price == 0: continue
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short) target_short_abs = data['target_short']
target_position = -target_short_abs # We want to be Short, so negative size target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0")) current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
diff_abs = abs(diff) diff_abs = abs(diff)
# Thresholds # Thresholds
config = self.coin_configs.get(coin, {}) config = self.coin_configs.get(coin, {})
min_thresh = config.get("min_threshold", Decimal("0.008")) min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
# Volatility Multiplier
vol_pct = self.calculate_volatility(coin) vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005") 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") 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")) base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult) thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct) dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
# FORCE EDGE CLEANUP if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True) if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
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
# Check Trigger
action_needed = diff_abs > dynamic_thresh action_needed = diff_abs > dynamic_thresh
# Determine Intent (Moved UP for Order Logic)
is_buy_bool = diff > 0 is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL" side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders # Manage Existing Orders
existing_orders = orders_map.get(coin, []) existing_orders = orders_map.get(coin, [])
force_taker_retry = False 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 order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015")) price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders: for o in existing_orders:
o_oid = o['oid'] o_oid = o['oid']
o_price = to_decimal(o['limitPx']) 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)) 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) 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 order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > maker_timeout: if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.") logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
continue continue
# Fishing Timeout Check if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
if enable_fishing and is_same_side and order_age_sec > fishing_timeout: logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
force_taker_retry = True 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 order_matched = True
if int(time.time()) % 10 == 0: if int(time.time()) % 15 == 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") logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break break
else: else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})") logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
# --- EXECUTION LOGIC --- # Determine Urgency / Bypass Cooldown
if not order_matched: bypass_cooldown = False
if action_needed or force_taker_retry: 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 bypass_cooldown = False
force_maker = False force_taker_retry = False # Disable taker retry from fishing
# 0. Forced Taker Retry (Fishing Timeout) # --- ASYMMETRIC HEDGE CHECK ---
if force_taker_retry: is_asymmetric_blocked = False
bypass_cooldown = True p_mid_asym = Decimal("0")
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker") # strategy_type already fetched above
# 1. Urgent Closing -> Taker if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
elif data.get('is_closing', False): total_L_asym = Decimal("0")
bypass_cooldown = True for k_strat, strat_inst in self.strategies.items():
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit") if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
# 2. Ghost/Cleanup -> Maker gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
elif data.get('contributors', 0) == 0: if gamma_asym > 0:
if time.time() - self.startup_time > 5: p_mid_asym = price - (diff_abs / gamma_asym)
force_maker = True if not data.get('is_at_edge', False) and price >= p_mid_asym:
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce") is_asymmetric_blocked = True
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.")
# --- 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) 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) if bypass_cooldown or (time.time() - last_trade > min_time):
can_trade = False if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
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)
levels = l2_snapshots[coin]['levels'] 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']) logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
ask = to_decimal(levels[1][0]['px']) 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 logger.info("Sleeping 10s for position update...")
create_shadow = False 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) # --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo) 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: # Update all active strategies for this coin in JSON
# If Force Maker -> Alo if total_L_log > 0 and price > 0:
# Else if Urgent -> Ioc for k_strat, strat_inst in self.strategies.items():
# Else if Enable Fishing -> Alo if self.strategy_states[k_strat]['coin'] != coin: continue
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
# Let's clarify: # CLP Value Calc
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo. def get_clp_value(p, s):
# New logic: if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
# If bypass_cooldown -> Ioc if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
# Else -> Alo (Fishing) return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
if bypass_cooldown and not force_maker: clp_curr_val = get_clp_value(price, strat_inst)
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"
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) # USE TRACKED HEDGE ENTRY PRICE
if oid: h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
self.last_trade_times[coin] = time.time() if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
# 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)
else: else:
# Cooldown log hedge_pnl_curr = Decimal("0")
pass
else: fee_close_curr = (target_size * price) * Decimal("0.000432")
# Action NOT needed uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# 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'])
# --- IDLE LOGGING (Restored Format) --- # Retrieve Realized PnL & Fees from State
# Calculate aggregate Gamma to estimate triggers realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
# Gamma = 0.5 * Sum(L) * P^-1.5 realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
# 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
if total_L > 0 and price > 0: # Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5"))) tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
if gamma > 0:
# Equilibrium Price (Diff = 0)
p_mid = price + (diff / gamma)
# Triggers cur_hl_cost = realized_fees + fee_close_curr
p_buy = price + (dynamic_thresh + diff) / gamma
p_sell = price - (dynamic_thresh - diff) / gamma
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 "" pad = " " if coin == "BNB" else ""
adj_val = data.get('adj_pct', Decimal("0")) * 100
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0")) unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl_total = Decimal("0") closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees_total = Decimal("0") fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
for k, s_state in self.strategy_states.items(): total_pnl = (closed_pnl - fees) + unrealized
if s_state['coin'] == coin:
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
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 "" # Recalculate for logging (including bounds)
tot_pnl_pad = " " if total_pnl >= 0 else "" 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}") # Use Custom Fixed Target if exists
else: target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
if int(time.time()) % 30 == 0: 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})") logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else: else:
if int(time.time()) % 30 == 0: logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1)) 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) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
# --- ABIs --- from clp_abis import (
# (Kept minimal for brevity, normally would load from files) NONFUNGIBLE_POSITION_MANAGER_ABI,
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads(''' UNISWAP_V3_POOL_ABI,
[ ERC20_ABI,
{"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"}, UNISWAP_V3_FACTORY_ABI,
{"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"}, AERODROME_FACTORY_ABI,
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, AERODROME_POOL_ABI,
{"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"}, AERODROME_NPM_ABI,
{"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"}, SWAP_ROUTER_ABI,
{"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"}, WETH9_ABI
{"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_config import get_current_config, STATUS_FILE from clp_config import get_current_config, STATUS_FILE
from tools.universal_swapper import execute_swap
# --- GET ACTIVE DEX CONFIG --- # --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_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 --- # --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60) MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True) 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) TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000) INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01")) 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")) SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30) 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 --- # --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"] 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"] UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1) # Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"] 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': if pool_address == '0x0000000000000000000000000000000000000000':
return None, None 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 { return {
"token0_address": token0_address, "token1_address": token1_address, "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: 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. 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) token0 = clean_address(token0)
token1 = clean_address(token1) 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 of Token0 in terms of Token1
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1) price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
swap_call = None chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
token_in, token_out = None, None
amount_in = 0
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: if deficit0 > 0 and bal1 > amount1_needed:
# Need T0 (ETH), Have extra T1 (USDC) # Need T0 (ETH), Have extra T1 (USDC)
# Swap T1 -> T0 # Swap T1 -> T0
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
surplus1 = bal1 - amount1_needed surplus1 = bal1 - amount1_needed
if surplus1 >= amount_in_needed: if surplus1 >= amount_in_needed:
token_in, token_out = token1, token0 # Get Symbols
amount_in = amount_in_needed 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}") logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
else: else:
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}") 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 surplus0 = bal0 - amount0_needed
if surplus0 >= amount_in_needed: if surplus0 >= amount_in_needed:
token_in, token_out = token0, token1 token_in_sym = token0_c.functions.symbol().call().upper()
amount_in = amount_in_needed 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}") logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
else: else:
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}") logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
if token_in and amount_in > 0: if token_in_sym and amount_in_float > 0:
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...") 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 return False
except Exception as e:
params = ( logger.error(f"❌ Universal Swap Failed: {e}")
token_in, token_out, POOL_FEE, account.address, return False
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
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}") logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False 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)) amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
# 3. Mint # 3. Mint
params = ( base_params = [
token0, token1, POOL_FEE, token0, token1, POOL_FEE,
tick_lower, tick_upper, tick_lower, tick_upper,
amount0, amount1, amount0, amount1,
amount0_min, amount1_min, amount0_min, amount1_min,
account.address, account.address,
int(time.time()) + 180 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") 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) save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}") 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 --- # --- MAIN LOOP ---
def main(): 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...") logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
load_dotenv(override=True) load_dotenv(override=True)
@ -722,9 +810,23 @@ def main():
logger.info(f"👤 Wallet: {account.address}") logger.info(f"👤 Wallet: {account.address}")
# Contracts # 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_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) router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
while True: while True:
@ -817,7 +919,21 @@ def main():
pnl_unrealized = current_pos_value_usd - initial_value pnl_unrealized = current_pos_value_usd - initial_value
total_pnl_usd = pnl_unrealized + total_fees_usd 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}") logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
# --- KPI LOGGING --- # --- KPI LOGGING ---
@ -845,6 +961,32 @@ def main():
log_kpi_snapshot(snapshot) 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: if not in_range and CLOSE_POSITION_ENABLED:
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)") logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
update_position_status(token_id, "CLOSING") update_position_status(token_id, "CLOSING")
@ -875,14 +1017,73 @@ def main():
fee = POOL_FEE fee = POOL_FEE
pool_addr = factory.functions.getPool(token0, token1, fee).call() 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) pool_data = get_pool_dynamic_data(pool_c)
if pool_data: if pool_data:
tick = pool_data['tick'] tick = pool_data['tick']
# Define Range (+/- 2.5%)
# log(1.025) / log(1.0001) approx 247 tick delta # --- PRE-CALCULATE ESSENTIALS ---
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001)) # 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 # Fetch actual tick spacing from pool
tick_spacing = pool_c.functions.tickSpacing().call() tick_spacing = pool_c.functions.tickSpacing().call()
@ -893,28 +1094,10 @@ def main():
# Calculate Amounts # Calculate Amounts
# Target Value logic # 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 # Determine Investment Value in Token1 terms
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC)) 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") investment_val_token1 = Decimal("0")
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX": 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']) 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): 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) minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
if minted: if minted:
# Calculate entry price and amounts for JSON compatibility # --- DISABLE FORCE MODE AFTER FIRST MINT ---
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1) 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_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1)) fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
@ -970,6 +1176,8 @@ def main():
"range_lower": round(r_lower, 4), "range_lower": round(r_lower, 4),
"token0_decimals": d0, "token0_decimals": d0,
"token1_decimals": d1, "token1_decimals": d1,
"range_mode": current_range_mode,
"range_width_initial": float(active_range_width),
"timestamp_open": int(time.time()), "timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S") "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) logger.addHandler(console_handler)
# File 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.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter()) file_handler.addFilter(UnixMsLogFilter())
file_formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s') 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_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID', '') TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID', '')
TELEGRAM_CHECK_INTERVAL = int(os.getenv('TELEGRAM_CHECK_INTERVAL_SECONDS', '60')) 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_STATE_FILE = os.getenv('TELEGRAM_STATE_FILE', 'telegram_monitor_state.json')
TELEGRAM_TIMEOUT = int(os.getenv('TELEGRAM_TIMEOUT_SECONDS', '10')) 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: class TelegramNotifier:
"""Handles Telegram API communication""" """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}")