working version, before optimalization

This commit is contained in:
2026-01-06 09:47:49 +01:00
parent c29dc2c8ac
commit a166d33012
36 changed files with 5394 additions and 901 deletions

95
GEMINI.md Normal file
View File

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

82
clp_abis.py Normal file
View File

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

View File

@ -8,7 +8,9 @@ STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
# --- DEFAULT STRATEGY ---
DEFAULT_STRATEGY = {
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
"MONITOR_INTERVAL_SECONDS": 300, # Manager loop & sync interval
"LOG_INTERVAL_SECONDS": 300, # Hedger console logging interval
"RANGE_MODE": "AUTO", # Options: "AUTO" (BB-based), "FIXED" (RANGE_WIDTH_PCT)
"CLOSE_POSITION_ENABLED": True, # Allow the bot to automatically close out-of-range positions
"OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior
@ -19,22 +21,22 @@ DEFAULT_STRATEGY = {
"VALUE_REFERENCE": "USD", # Base currency for all calculations
# Range Settings
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting
"RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
"SLIPPAGE_TOLERANCE": Decimal("0.05"), # Max allowed slippage for swaps and minting
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction)
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction), "FIXED" (Initial Delta)
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
# Unified Hedger Settings
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
"LEVERAGE": 2, # 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_CLOSE_START": Decimal("10.0"), # Distance (pct) from edge to start closing logic
"ZONE_CLOSE_END": Decimal("11.0"), # Distance (pct) from edge to finish closing logic
"ZONE_TOP_HEDGE_START": Decimal("10.0"), # Distance (pct) from top edge to adjust hedging
"PRICE_BUFFER_PCT": Decimal("0.0015"), # Buffer for limit order pricing (0.15%)
"PRICE_BUFFER_PCT": Decimal("0.0025"), # Buffer for limit order pricing (0.15%)
"MIN_ORDER_VALUE_USD": Decimal("10.0"), # Minimum order size allowed by Hyperliquid
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.2"), # Expansion factor for thresholds
"MIN_TIME_BETWEEN_TRADES": 60, # Cooldown (seconds) between rebalance trades
@ -73,8 +75,9 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500,
"TARGET_INVESTMENT_AMOUNT": 1000,
"HEDGE_STRATEGY": "BOTTOM",
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
"RANGE_WIDTH_PCT": Decimal("0.0075"),
},
"UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
@ -127,6 +130,34 @@ CLP_PROFILES = {
"TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10")
},
"AERODROME_BASE_CL": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 100, # TickSpacing 100 pool (0xb2cc...)
"RANGE_WIDTH_PCT": Decimal("0.075"),
"TARGET_INVESTMENT_AMOUNT": 200,
"HEDGE_STRATEGY": "FIXED",
},
"AERODROME_WETH-USDC_008": {
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC Stable",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 1, # TickSpacing 1 pool (0xdbc6...)
"RANGE_WIDTH_PCT": Decimal("0.0075"),
"TARGET_INVESTMENT_AMOUNT": 3000,
"HEDGE_STRATEGY": "FIXED",
}
}

View File

@ -14,14 +14,9 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(project_root)
# Import local modules
try:
from logging_utils import setup_logging
except ImportError:
setup_logging = None
# Ensure root logger is clean if we can't use setup_logging
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
# Ensure root logger is clean
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
from eth_account import Account
from hyperliquid.exchange import Exchange
@ -233,10 +228,12 @@ class HyperliquidStrategy:
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
raw_target_short = pool_delta
# --- BOTTOM STRATEGY LOGIC ---
if strategy_type == "BOTTOM":
# --- 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")
@ -287,13 +284,23 @@ class UnifiedHedger:
# Market Data Cache
self.last_prices = {}
self.price_history = {} # Symbol -> List[Decimal]
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
self.last_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List)
self.shadow_orders = []
# State: Emergency Close Hysteresis
# Map: (file_path, token_id) -> bool
self.emergency_close_active = {}
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
self.custom_fixed_targets = {}
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
self.hedge_entry_prices = {}
self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -455,6 +462,7 @@ class UnifiedHedger:
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -506,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN')
}
# Initial hedge entry price is the CLP entry price
self.hedge_entry_prices[key] = entry_price
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
# Ensure JSON has these fields initialized
update_position_stats(key[0], key[1], {
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
})
except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -680,7 +699,7 @@ class UnifiedHedger:
price = to_decimal(mids[coin])
self.last_prices[coin] = price
# Update Price History
# Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
@ -701,13 +720,50 @@ class UnifiedHedger:
if coin not in aggregates:
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
if status == 'CLOSING':
# If Closing, we want target to be 0 for this strategy
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
is_active_hysteresis = self.emergency_close_active.get(key, False)
if is_active_hysteresis:
# CHECK RESET CONDITION
if strategy_type == "FIXED":
# Reset at 75% of range (from Bottom)
range_width = strat.high_range - strat.low_range
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
else:
reset_threshold = strat.high_range * Decimal("0.999")
if price < reset_threshold:
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
self.emergency_close_active[key] = False
is_active_hysteresis = False
# Capture NEW Dynamic Fixed Target and Entry Price
if strategy_type == "FIXED":
dynamic_delta = strat.get_pool_delta(price)
self.custom_fixed_targets[key] = dynamic_delta
self.hedge_entry_prices[key] = price
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
if not is_active_hysteresis:
# CHECK TRIGGER CONDITION
if price >= strat.high_range:
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
self.emergency_close_active[key] = True
is_active_hysteresis = True
# Reset entry price when closed
self.hedge_entry_prices[key] = Decimal("0")
if status == 'CLOSING' or is_active_hysteresis:
# If Closing OR Hysteresis Active, target is 0
aggregates[coin]['is_closing'] = True
# Do not add to target_short
else:
aggregates[coin]['target_short'] += calc['target_short']
# Use custom fixed target if exists, else standard calc
if strategy_type == "FIXED" and key in self.custom_fixed_targets:
aggregates[coin]['target_short'] += self.custom_fixed_targets[key]
else:
aggregates[coin]['target_short'] += calc['target_short']
aggregates[coin]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct']
@ -875,25 +931,69 @@ class UnifiedHedger:
if existing_orders and not order_matched:
for o in existing_orders: self.cancel_order(coin, o['oid'])
# --- THROTTLED STATUS LOGGING ---
# --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
total_L_log = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_log += strat_inst.L
# Update all active strategies for this coin in JSON
if total_L_log > 0 and price > 0:
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
# CLP Value Calc
def get_clp_value(p, s):
if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
clp_curr_val = get_clp_value(price, strat_inst)
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
# USE TRACKED HEDGE ENTRY PRICE
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
else:
hedge_pnl_curr = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# Retrieve Realized PnL & Fees from State
realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
cur_hl_cost = realized_fees + fee_close_curr
# 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)
monitor_interval = config.get("MONITOR_INTERVAL_SECONDS", 60)
log_interval = config.get("LOG_INTERVAL_SECONDS", 300)
if now - last_log >= monitor_interval:
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")
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
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) # Corrected equilibrium formula
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 ""
@ -903,7 +1003,45 @@ class UnifiedHedger:
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
total_pnl = (closed_pnl - fees) + unrealized
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} | TotPnL: {total_pnl:.2f}")
# 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
# Recalculate for logging (including bounds)
clp_curr_val = get_clp_value(price, strat_inst)
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
hedge_pnl_low = (h_entry_px - strat_inst.low_range) * target_size
hedge_pnl_high = (h_entry_px - strat_inst.high_range) * target_size
fee_open = (target_size * h_entry_px) * Decimal("0.000144")
else:
hedge_pnl_curr = hedge_pnl_low = hedge_pnl_high = Decimal("0")
fee_open = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
fee_close_low = (target_size * strat_inst.low_range) * Decimal("0.000432")
fee_close_high = (target_size * strat_inst.high_range) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr - (fee_open + fee_close_curr) + uni_fees
tot_low = (clp_low_val - strat_inst.target_value) + hedge_pnl_low - (fee_open + fee_close_low) + uni_fees
tot_high = (clp_high_val - strat_inst.target_value) + hedge_pnl_high - (fee_open + fee_close_high) + uni_fees
cur_hl_cost = fee_open + fee_close_curr
# ID or Range to distinguish
strat_id = str(k_strat[1]) # Token ID
logger.info(f"[FIXED] {coin} #{strat_id} | TotPnL: {tot_curr:+.2f} | Down: {tot_low:+.2f} | Up: {tot_high:+.2f} (Inc: Fees ${uni_fees:.2f}, HL Cost ${cur_hl_cost:.2f})")
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid_log:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {data.get('adj_pct',0)*100:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f} | HedgePnL: {total_pnl:.2f}")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:

View File

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

View File

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

Binary file not shown.

View File

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

View File

@ -508,7 +508,7 @@
{
"type": "AUTOMATIC",
"token_id": 6176727,
"status": "OPEN",
"status": "CLOSED",
"target_value": 990.64,
"entry_price": 867.5401,
"amount0_initial": 490.6325,
@ -520,9 +520,163 @@
"token1_decimals": 18,
"timestamp_open": 1767336634,
"time_open": "02.01.26 07:50:34",
"hedge_TotPnL": 0.241595,
"hedge_fees_paid": 0.364602,
"clp_fees": 0.22,
"clp_TotPnL": -0.73
"hedge_TotPnL": -5.442897,
"hedge_fees_paid": 0.85406,
"clp_fees": 0.91,
"clp_TotPnL": 1.88,
"timestamp_close": 1767347410,
"time_close": "02.01.26 10:50:10"
},
{
"type": "AUTOMATIC",
"token_id": 6177360,
"status": "CLOSED",
"target_value": 993.15,
"entry_price": 870.8428,
"amount0_initial": 493.1411,
"amount1_initial": 0.5742,
"liquidity": "8638221415835012765221",
"range_upper": 874.2456,
"range_lower": 867.4533,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767347607,
"time_open": "02.01.26 10:53:27",
"hedge_TotPnL": 1.294631,
"hedge_fees_paid": 1.047541,
"clp_fees": 1.24,
"clp_TotPnL": -1.66,
"timestamp_close": 1767363551,
"time_close": "02.01.26 15:19:11"
},
{
"type": "AUTOMATIC",
"token_id": 6185008,
"status": "CLOSED",
"target_value": 992.5,
"entry_price": 867.1064,
"amount0_initial": 492.4924,
"amount1_initial": 0.5766,
"liquidity": "8651147821199061055073",
"range_upper": 870.4946,
"range_lower": 863.7315,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767363751,
"time_open": "02.01.26 15:22:31",
"hedge_TotPnL": -2.674848,
"hedge_fees_paid": 0.393713,
"clp_fees": 0.32,
"clp_TotPnL": 1.28,
"timestamp_close": 1767364835,
"time_close": "02.01.26 15:40:35"
},
{
"type": "AUTOMATIC",
"token_id": 6185867,
"status": "CLOSED",
"target_value": 996.08,
"entry_price": 875.5579,
"amount0_initial": 496.0791,
"amount1_initial": 0.5711,
"liquidity": "8640396990671870185711",
"range_upper": 878.9791,
"range_lower": 872.15,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767365969,
"time_open": "02.01.26 15:59:29",
"hedge_TotPnL": 2.830618,
"hedge_fees_paid": 1.060949,
"clp_fees": 0.53,
"clp_TotPnL": -2.97,
"timestamp_close": 1767367303,
"time_close": "02.01.26 16:21:43"
},
{
"type": "AUTOMATIC",
"token_id": 6186334,
"status": "CLOSED",
"target_value": 996.88,
"entry_price": 873.9834,
"amount0_initial": 496.8714,
"amount1_initial": 0.5721,
"liquidity": "8655088063104153413073",
"range_upper": 877.3984,
"range_lower": 870.5816,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767367883,
"time_open": "02.01.26 16:31:23",
"hedge_TotPnL": -3.585575,
"hedge_fees_paid": 0.57548,
"clp_fees": 0.13,
"clp_TotPnL": 1.1,
"timestamp_close": 1767368416,
"time_close": "02.01.26 16:40:16"
},
{
"type": "AUTOMATIC",
"token_id": 6186613,
"status": "CLOSED",
"target_value": 993.42,
"entry_price": 880.2984,
"amount0_initial": 493.4134,
"amount1_initial": 0.568,
"liquidity": "8594082766621558309079",
"range_upper": 883.7381,
"range_lower": 876.8721,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767368652,
"time_open": "02.01.26 16:44:12",
"hedge_TotPnL": -4.384157,
"hedge_fees_paid": 0.627319,
"clp_fees": 0.92,
"clp_TotPnL": 1.89,
"timestamp_close": 1767371545,
"time_close": "02.01.26 17:32:25"
},
{
"type": "AUTOMATIC",
"token_id": 6187096,
"status": "CLOSED",
"target_value": 996.91,
"entry_price": 885.9501,
"amount0_initial": 496.8996,
"amount1_initial": 0.5644,
"liquidity": "2271526539550158344821",
"range_upper": 899.159,
"range_lower": 872.9353,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767372001,
"time_open": "02.01.26 17:40:01",
"hedge_TotPnL": 7.60386,
"hedge_fees_paid": 0.283695,
"clp_fees": 1.38,
"clp_TotPnL": -9.61,
"timestamp_close": 1767424468,
"time_close": "03.01.26 08:14:28"
},
{
"type": "AUTOMATIC",
"token_id": 6203530,
"status": "CLOSED",
"target_value": 998.23,
"entry_price": 872.0628,
"amount0_initial": 500.0027,
"amount1_initial": 0.5713,
"liquidity": "2292568457223553397610",
"range_upper": 885.0647,
"range_lower": 859.252,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767424756,
"time_open": "03.01.26 08:19:16",
"hedge_TotPnL": -2.47676,
"hedge_fees_paid": 0.287962,
"clp_fees": 0.19,
"clp_TotPnL": 1.92
}
]

View File

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

82
florida/clp_abis.py Normal file
View File

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

View File

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

View File

@ -14,20 +14,15 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(project_root)
# Import local modules
try:
from logging_utils import setup_logging
except ImportError:
setup_logging = None
# Ensure root logger is clean if we can't use setup_logging
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
# Ensure root logger is clean
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.INFO)
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY, TARGET_DEX
# Load environment variables
dotenv_path = os.path.join(current_dir, '.env')
@ -233,7 +228,21 @@ class HyperliquidStrategy:
adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct))
raw_target_short = pool_delta
# --- 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")
adjusted_target_short = raw_target_short * (Decimal("1.0") + adj_pct)
diff = adjusted_target_short - abs(current_short_size)
@ -275,13 +284,23 @@ class UnifiedHedger:
# Market Data Cache
self.last_prices = {}
self.price_history = {} # Symbol -> List[Decimal]
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
self.last_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List)
self.shadow_orders = []
# State: Emergency Close Hysteresis
# Map: (file_path, token_id) -> bool
self.emergency_close_active = {}
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
self.custom_fixed_targets = {}
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
self.hedge_entry_prices = {}
self.startup_time = time.time()
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
@ -289,6 +308,7 @@ class UnifiedHedger:
def _init_coin_configs(self):
"""Pre-load configuration for known coins from CLP_PROFILES."""
# 1. Load all profiles (order depends on dict iteration)
for profile_key, profile_data in CLP_PROFILES.items():
symbol = profile_data.get("COIN_SYMBOL")
if symbol:
@ -300,6 +320,18 @@ class UnifiedHedger:
# Update with Profile Specifics
self.coin_configs[symbol].update(profile_data)
# 2. Force overwrite with TARGET_DEX profile to ensure precedence
target_profile = CLP_PROFILES.get(TARGET_DEX)
if target_profile:
symbol = target_profile.get("COIN_SYMBOL")
if symbol:
if symbol not in self.coin_configs:
self.coin_configs[symbol] = DEFAULT_STRATEGY.copy()
self.coin_configs[symbol]["sz_decimals"] = 4
logger.info(f"Overwriting config for {symbol} using TARGET_DEX: {TARGET_DEX}")
self.coin_configs[symbol].update(target_profile)
def _get_sz_decimals(self, coin: str) -> int:
try:
meta = self.info.meta()
@ -430,6 +462,7 @@ class UnifiedHedger:
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
except Exception as e:
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
@ -481,12 +514,23 @@ class UnifiedHedger:
"start_time": start_time_ms,
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
"entry_price": entry_price, # Store for fishing logic
"status": position_data.get('status', 'OPEN')
}
# Initial hedge entry price is the CLP entry price
self.hedge_entry_prices[key] = entry_price
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
# Ensure JSON has these fields initialized
update_position_stats(key[0], key[1], {
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
})
except Exception as e:
logger.error(f"Failed to init strategy {key[1]}: {e}")
@ -584,297 +628,6 @@ class UnifiedHedger:
except Exception as e:
logger.error(f"Failed to update closed PnL/Fees for {coin}: {e}")
def run(self):
logger.info("Starting Unified Hedger Loop...")
self.update_coin_decimals()
# --- LOG SETTINGS ON START ---
logger.info("=== HEDGER CONFIGURATION ===")
for symbol, config in self.coin_configs.items():
logger.info(f"--- {symbol} ---")
for k, v in config.items():
logger.info(f" {k}: {v}")
logger.info("============================")
def run_tick(self):
"""
Executes one iteration of the hedger logic.
"""
# 1. API Backoff
if time.time() < self.api_backoff_until:
return
# 2. Update Strategies
if not self.scan_strategies():
logger.warning("Strategy scan failed (read error). Skipping execution tick.")
return
# 3. Fetch Market Data (Centralized)
try:
mids = self.info.all_mids()
user_state = self.info.user_state(self.vault_address or self.account.address)
open_orders = self.get_open_orders()
l2_snapshots = {} # Cache for snapshots
except Exception as e:
logger.error(f"API Error fetching data: {e}")
return
# Map Open Orders
orders_map = {}
for o in open_orders:
c = o['coin']
if c not in orders_map: orders_map[c] = []
orders_map[c].append(o)
# Parse User State
account_value = Decimal("0")
if "marginSummary" in user_state and "accountValue" in user_state["marginSummary"]:
account_value = to_decimal(user_state["marginSummary"]["accountValue"])
# Map current positions
current_positions = {} # Coin -> Size
current_pnls = {} # Coin -> Unrealized PnL
current_entry_pxs = {} # Coin -> Entry Price (NEW)
for pos in user_state["assetPositions"]:
c = pos["position"]["coin"]
s = to_decimal(pos["position"]["szi"])
u = to_decimal(pos["position"]["unrealizedPnl"])
e = to_decimal(pos["position"]["entryPx"])
current_positions[c] = s
current_pnls[c] = u
current_entry_pxs[c] = e
# 4. Aggregate Targets
# Coin -> { 'target_short': Decimal, 'contributors': int, 'is_at_edge': bool }
aggregates = {}
# First, update all prices from mids for active coins
for coin in self.active_coins:
if coin in mids:
price = to_decimal(mids[coin])
self.last_prices[coin] = price
# Update Price History
if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
for key, strat in self.strategies.items():
coin = self.strategy_states[key]['coin']
status = self.strategy_states[key].get('status', 'OPEN')
if coin not in self.last_prices: continue
price = self.last_prices[coin]
# Get Config & Strategy Type
config = self.coin_configs.get(coin, {})
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
# Calc Logic
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates:
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
if status == 'CLOSING':
# If Closing, we want target to be 0 for this strategy
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
aggregates[coin]['is_closing'] = True
# Do not add to target_short
else:
aggregates[coin]['target_short'] += calc['target_short']
aggregates[coin]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct']
# Check Edge Proximity for Cleanup
config = self.coin_configs.get(coin, {})
enable_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
cleanup_margin = config.get("EDGE_CLEANUP_MARGIN_PCT", Decimal("0.02"))
if enable_cleanup:
dist_bottom_pct = (price - strat.low_range) / strat.low_range
dist_top_pct = (strat.high_range - price) / strat.high_range
range_width_pct = (strat.high_range - strat.low_range) / strat.low_range
safety_margin_pct = range_width_pct * cleanup_margin
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True
if dist_bottom_pct < safety_margin_pct:
aggregates[coin]['is_at_bottom_edge'] = True
# Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots)
# 5. Execute Per Coin
# Union of coins with Active Strategies OR Active Positions
coins_to_process = set(aggregates.keys())
for c, pos in current_positions.items():
if abs(pos) > 0: coins_to_process.add(c)
for coin in coins_to_process:
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
price = self.last_prices.get(coin, Decimal("0"))
if price == 0: continue
target_short_abs = data['target_short']
target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos
diff_abs = abs(diff)
# Thresholds
config = self.coin_configs.get(coin, {})
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005")
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
action_needed = diff_abs > dynamic_thresh
is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders
existing_orders = orders_map.get(coin, [])
force_taker_retry = False
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side']
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)
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid)
continue
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
self.cancel_order(coin, o_oid)
force_taker_retry = True
continue
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
order_matched = True
if int(time.time()) % 15 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
# Determine Urgency / Bypass Cooldown
bypass_cooldown = False
force_maker = False
if not order_matched and (action_needed or force_taker_retry):
if force_taker_retry: bypass_cooldown = True
elif data.get('is_closing', False): bypass_cooldown = True
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5: force_maker = True
else: continue # Skip startup ghost positions
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
# Prevent IOC for BUYs at bottom edge
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
bypass_cooldown = True
# --- ASYMMETRIC HEDGE CHECK ---
is_asymmetric_blocked = False
p_mid_asym = Decimal("0")
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
total_L_asym = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
if gamma_asym > 0:
p_mid_asym = price - (diff_abs / gamma_asym)
if not data.get('is_at_edge', False) and price >= p_mid_asym:
is_asymmetric_blocked = True
# --- EXECUTION ---
if not order_matched and not is_asymmetric_blocked:
if action_needed or force_taker_retry:
last_trade = self.last_trade_times.get(coin, 0)
min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60)
if bypass_cooldown or (time.time() - last_trade > min_time):
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
levels = l2_snapshots[coin]['levels']
if 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"
logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
if order_type == "Ioc":
shadow_price = bid if is_buy_bool else ask
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
logger.info("Sleeping 10s for position update...")
time.sleep(10)
self._update_closed_pnl(coin)
else:
# Idle Cleanup
if existing_orders and not order_matched:
for o in existing_orders: self.cancel_order(coin, o['oid'])
# --- THROTTLED STATUS LOGGING ---
now = time.time()
last_log = self.last_idle_log_times.get(coin, 0)
monitor_interval = config.get("MONITOR_INTERVAL_SECONDS", 60)
if now - last_log >= monitor_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")
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
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) # Corrected equilibrium formula
p_buy = price + (dynamic_thresh + diff) / gamma_log
p_sell = price - (dynamic_thresh - diff) / gamma_log
pad = " " if coin == "BNB" else ""
unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
total_pnl = (closed_pnl - fees) + unrealized
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} | TotPnL: {total_pnl:.2f}")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
def run(self):
logger.info("Starting Unified Hedger Loop...")
self.update_coin_decimals()
@ -889,7 +642,411 @@ class UnifiedHedger:
while True:
try:
self.run_tick()
# 1. API Backoff
if time.time() < self.api_backoff_until:
time.sleep(1)
continue
# 2. Update Strategies
if not self.scan_strategies():
logger.warning("Strategy scan failed (read error). Skipping execution tick.")
time.sleep(1)
continue
# 3. Fetch Market Data (Centralized)
try:
mids = self.info.all_mids()
user_state = self.info.user_state(self.vault_address or self.account.address)
open_orders = self.get_open_orders()
l2_snapshots = {} # Cache for snapshots
except Exception as e:
logger.error(f"API Error fetching data: {e}")
time.sleep(1)
continue
# Map Open Orders
orders_map = {}
for o in open_orders:
c = o['coin']
if c not in orders_map: orders_map[c] = []
orders_map[c].append(o)
# Parse User State
account_value = Decimal("0")
if "marginSummary" in user_state and "accountValue" in user_state["marginSummary"]:
account_value = to_decimal(user_state["marginSummary"]["accountValue"])
# Map current positions
current_positions = {} # Coin -> Size
current_pnls = {} # Coin -> Unrealized PnL
current_entry_pxs = {} # Coin -> Entry Price (NEW)
for pos in user_state["assetPositions"]:
c = pos["position"]["coin"]
s = to_decimal(pos["position"]["szi"])
u = to_decimal(pos["position"]["unrealizedPnl"])
e = to_decimal(pos["position"]["entryPx"])
current_positions[c] = s
current_pnls[c] = u
current_entry_pxs[c] = e
# 4. Aggregate Targets
# Coin -> { 'target_short': Decimal, 'contributors': int, 'is_at_edge': bool }
aggregates = {}
# First, update all prices from mids for active coins
for coin in self.active_coins:
if coin in mids:
price = to_decimal(mids[coin])
self.last_prices[coin] = price
# Update Price History (Fast)
if coin not in self.price_history: self.price_history[coin] = []
self.price_history[coin].append(price)
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
for key, strat in self.strategies.items():
coin = self.strategy_states[key]['coin']
status = self.strategy_states[key].get('status', 'OPEN')
if coin not in self.last_prices: continue
price = self.last_prices[coin]
# Get Config & Strategy Type
config = self.coin_configs.get(coin, {})
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
# Calc Logic
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
if coin not in aggregates:
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
is_active_hysteresis = self.emergency_close_active.get(key, False)
if is_active_hysteresis:
# CHECK RESET CONDITION
if strategy_type == "FIXED":
# Reset at 75% of range (from Bottom)
range_width = strat.high_range - strat.low_range
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
else:
reset_threshold = strat.high_range * Decimal("0.999")
if price < reset_threshold:
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
self.emergency_close_active[key] = False
is_active_hysteresis = False
# Capture NEW Dynamic Fixed Target and Entry Price
if strategy_type == "FIXED":
dynamic_delta = strat.get_pool_delta(price)
self.custom_fixed_targets[key] = dynamic_delta
self.hedge_entry_prices[key] = price
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
if not is_active_hysteresis:
# CHECK TRIGGER CONDITION
if price >= strat.high_range:
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
self.emergency_close_active[key] = True
is_active_hysteresis = True
# Reset entry price when closed
self.hedge_entry_prices[key] = Decimal("0")
if status == 'CLOSING' or is_active_hysteresis:
# If Closing OR Hysteresis Active, target is 0
aggregates[coin]['is_closing'] = True
else:
# Use custom fixed target if exists, else standard calc
if strategy_type == "FIXED" and key in self.custom_fixed_targets:
aggregates[coin]['target_short'] += self.custom_fixed_targets[key]
else:
aggregates[coin]['target_short'] += calc['target_short']
aggregates[coin]['contributors'] += 1
aggregates[coin]['adj_pct'] = calc['adj_pct']
# Check Edge Proximity for Cleanup
config = self.coin_configs.get(coin, {})
enable_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
cleanup_margin = config.get("EDGE_CLEANUP_MARGIN_PCT", Decimal("0.02"))
if enable_cleanup:
dist_bottom_pct = (price - strat.low_range) / strat.low_range
dist_top_pct = (strat.high_range - price) / strat.high_range
range_width_pct = (strat.high_range - strat.low_range) / strat.low_range
safety_margin_pct = range_width_pct * cleanup_margin
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True
if dist_bottom_pct < safety_margin_pct:
aggregates[coin]['is_at_bottom_edge'] = True
# Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots)
# 5. Execute Per Coin
# Union of coins with Active Strategies OR Active Positions
coins_to_process = set(aggregates.keys())
for c, pos in current_positions.items():
if abs(pos) > 0: coins_to_process.add(c)
for coin in coins_to_process:
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
price = self.last_prices.get(coin, Decimal("0"))
if price == 0: continue
target_short_abs = data['target_short']
target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos
diff_abs = abs(diff)
# Thresholds
config = self.coin_configs.get(coin, {})
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005")
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
action_needed = diff_abs > dynamic_thresh
is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders
existing_orders = orders_map.get(coin, [])
force_taker_retry = False
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side']
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)
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid)
continue
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
self.cancel_order(coin, o_oid)
force_taker_retry = True
continue
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
order_matched = True
if int(time.time()) % 15 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
# Determine Urgency / Bypass Cooldown
bypass_cooldown = False
force_maker = False
if not order_matched and (action_needed or force_taker_retry):
if force_taker_retry: bypass_cooldown = True
elif data.get('is_closing', False): bypass_cooldown = True
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5: force_maker = True
else: continue # Skip startup ghost positions
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
# Prevent IOC for BUYs at bottom edge
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
bypass_cooldown = True
# --- BOTTOM STRATEGY SAFEGUARDS ---
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
if strategy_type == "BOTTOM":
# strict: "do not use taker orders... except only on very bottom"
if not data.get('is_at_bottom_edge', False):
bypass_cooldown = False
force_taker_retry = False # Disable taker retry from fishing
# --- ASYMMETRIC HEDGE CHECK ---
is_asymmetric_blocked = False
p_mid_asym = Decimal("0")
# strategy_type already fetched above
if strategy_type == "ASYMMETRIC" and is_buy_bool and not bypass_cooldown:
total_L_asym = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_asym += strat_inst.L
gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5")))
if gamma_asym > 0:
p_mid_asym = price - (diff_abs / gamma_asym)
if not data.get('is_at_edge', False) and price >= p_mid_asym:
is_asymmetric_blocked = True
# --- EXECUTION ---
if not order_matched and not is_asymmetric_blocked:
if action_needed or force_taker_retry:
last_trade = self.last_trade_times.get(coin, 0)
min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60)
if bypass_cooldown or (time.time() - last_trade > min_time):
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
levels = l2_snapshots[coin]['levels']
if 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"
logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
if order_type == "Ioc":
shadow_price = bid if is_buy_bool else ask
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
logger.info("Sleeping 10s for position update...")
time.sleep(10)
self._update_closed_pnl(coin)
else:
# Idle Cleanup
if existing_orders and not order_matched:
for o in existing_orders: self.cancel_order(coin, o['oid'])
# --- REAL-TIME PnL CALCULATION & JSON UPDATE (1s) ---
total_L_log = Decimal("0")
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] == coin:
total_L_log += strat_inst.L
# Update all active strategies for this coin in JSON
if total_L_log > 0 and price > 0:
for k_strat, strat_inst in self.strategies.items():
if self.strategy_states[k_strat]['coin'] != coin: continue
# CLP Value Calc
def get_clp_value(p, s):
if p <= s.low_range: return s.L * (p * (1/s.low_range.sqrt() - 1/s.high_range.sqrt()))
if p >= s.high_range: return s.L * (s.high_range.sqrt() - s.low_range.sqrt())
return s.L * (2*p.sqrt() - s.low_range.sqrt() - p/s.high_range.sqrt())
clp_curr_val = get_clp_value(price, strat_inst)
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
# USE TRACKED HEDGE ENTRY PRICE
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
else:
hedge_pnl_curr = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
# Retrieve Realized PnL & Fees from State
realized_pnl = to_decimal(self.strategy_states[k_strat].get('hedge_TotPnL', 0))
realized_fees = to_decimal(self.strategy_states[k_strat].get('fees', 0))
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
cur_hl_cost = realized_fees + fee_close_curr
# 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 ""
unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
total_pnl = (closed_pnl - fees) + unrealized
# 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
# Recalculate for logging (including bounds)
clp_curr_val = get_clp_value(price, strat_inst)
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
# Use Custom Fixed Target if exists
target_size = self.custom_fixed_targets.get(k_strat, strat_inst.get_pool_delta(strat_inst.entry_price))
h_entry_px = self.hedge_entry_prices.get(k_strat, strat_inst.entry_price)
if h_entry_px > 0:
hedge_pnl_curr = (h_entry_px - price) * target_size
hedge_pnl_low = (h_entry_px - strat_inst.low_range) * target_size
hedge_pnl_high = (h_entry_px - strat_inst.high_range) * target_size
fee_open = (target_size * h_entry_px) * Decimal("0.000144")
else:
hedge_pnl_curr = hedge_pnl_low = hedge_pnl_high = Decimal("0")
fee_open = Decimal("0")
fee_close_curr = (target_size * price) * Decimal("0.000432")
fee_close_low = (target_size * strat_inst.low_range) * Decimal("0.000432")
fee_close_high = (target_size * strat_inst.high_range) * Decimal("0.000432")
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr - (fee_open + fee_close_curr) + uni_fees
tot_low = (clp_low_val - strat_inst.target_value) + hedge_pnl_low - (fee_open + fee_close_low) + uni_fees
tot_high = (clp_high_val - strat_inst.target_value) + hedge_pnl_high - (fee_open + fee_close_high) + uni_fees
cur_hl_cost = fee_open + fee_close_curr
# ID or Range to distinguish
strat_id = str(k_strat[1]) # Token ID
logger.info(f"[FIXED] {coin} #{strat_id} | TotPnL: {tot_curr:+.2f} | Down: {tot_low:+.2f} | Up: {tot_high:+.2f} (Inc: Fees ${uni_fees:.2f}, HL Cost ${cur_hl_cost:.2f})")
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid_log:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {data.get('adj_pct',0)*100:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f} | HedgePnL: {total_pnl:.2f}")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else:
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))
except KeyboardInterrupt:

File diff suppressed because it is too large Load Diff

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,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,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')

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}")