Compare commits
14 Commits
clp-optima
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a166d33012 | |||
| c29dc2c8ac | |||
| 138e98fc49 | |||
| 30aad78285 | |||
| f1946102fb | |||
| e37369608b | |||
| 9eb1abcfc8 | |||
| b2a11ca449 | |||
| 6ac0f4478f | |||
| 5cc14d485a | |||
| b1a86b81fc | |||
| 95f514b563 | |||
| c5b148ed6c | |||
| 5cdf71515b |
95
GEMINI.md
Normal file
95
GEMINI.md
Normal 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
82
clp_abis.py
Normal 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"}
|
||||
]
|
||||
''')
|
||||
@ -8,7 +8,9 @@ STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
|
||||
|
||||
# --- DEFAULT STRATEGY ---
|
||||
DEFAULT_STRATEGY = {
|
||||
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
|
||||
"MONITOR_INTERVAL_SECONDS": 300, # Manager loop & sync interval
|
||||
"LOG_INTERVAL_SECONDS": 300, # Hedger console logging interval
|
||||
"RANGE_MODE": "AUTO", # Options: "AUTO" (BB-based), "FIXED" (RANGE_WIDTH_PCT)
|
||||
"CLOSE_POSITION_ENABLED": True, # Allow the bot to automatically close out-of-range positions
|
||||
"OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
|
||||
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior
|
||||
@ -19,21 +21,22 @@ DEFAULT_STRATEGY = {
|
||||
"VALUE_REFERENCE": "USD", # Base currency for all calculations
|
||||
|
||||
# Range Settings
|
||||
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price)
|
||||
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting
|
||||
"RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
|
||||
"SLIPPAGE_TOLERANCE": Decimal("0.05"), # Max allowed slippage for swaps and minting
|
||||
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
|
||||
|
||||
# Hedging Settings
|
||||
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction), "FIXED" (Initial Delta)
|
||||
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
|
||||
|
||||
# Unified Hedger Settings
|
||||
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
|
||||
|
||||
"LEVERAGE": 5, # Leverage to use on Hyperliquid
|
||||
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range
|
||||
"ZONE_CLOSE_START": Decimal("10.0"), # Distance (pct) from edge to start closing logic
|
||||
"ZONE_CLOSE_END": Decimal("11.0"), # Distance (pct) from edge to finish closing logic
|
||||
"ZONE_TOP_HEDGE_START": Decimal("10.0"), # Distance (pct) from top edge to adjust hedging
|
||||
"PRICE_BUFFER_PCT": Decimal("0.0015"), # Buffer for limit order pricing (0.15%)
|
||||
"PRICE_BUFFER_PCT": Decimal("0.0025"), # Buffer for limit order pricing (0.15%)
|
||||
"MIN_ORDER_VALUE_USD": Decimal("10.0"), # Minimum order size allowed by Hyperliquid
|
||||
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.2"), # Expansion factor for thresholds
|
||||
"MIN_TIME_BETWEEN_TRADES": 60, # Cooldown (seconds) between rebalance trades
|
||||
@ -45,7 +48,7 @@ DEFAULT_STRATEGY = {
|
||||
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
|
||||
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
|
||||
"ENABLE_EDGE_CLEANUP": True, # Force rebalances when price is at range boundaries
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.02"), # % of range width used for edge detection
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.03"), # % of range width used for edge detection
|
||||
"MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
|
||||
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
|
||||
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
|
||||
@ -72,6 +75,9 @@ CLP_PROFILES = {
|
||||
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
||||
"POOL_FEE": 500,
|
||||
"TARGET_INVESTMENT_AMOUNT": 3000,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
"RANGE_WIDTH_PCT": Decimal("0.0075"),
|
||||
},
|
||||
"UNISWAP_wide": {
|
||||
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
|
||||
@ -98,6 +104,7 @@ CLP_PROFILES = {
|
||||
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
|
||||
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
"POOL_FEE": 100,
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.1875"), # 0.1875 only for asymmetric shedge % of range width used for edge detection
|
||||
"RANGE_WIDTH_PCT": Decimal("0.004"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 1000,
|
||||
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
|
||||
@ -123,6 +130,34 @@ CLP_PROFILES = {
|
||||
"TARGET_INVESTMENT_AMOUNT": 200,
|
||||
"VALUE_REFERENCE": "USD",
|
||||
"RANGE_WIDTH_PCT": Decimal("0.10")
|
||||
},
|
||||
"AERODROME_BASE_CL": {
|
||||
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
|
||||
"COIN_SYMBOL": "ETH",
|
||||
"RPC_ENV_VAR": "BASE_RPC_URL",
|
||||
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
|
||||
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
|
||||
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
|
||||
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
|
||||
"POOL_FEE": 100, # TickSpacing 100 pool (0xb2cc...)
|
||||
"RANGE_WIDTH_PCT": Decimal("0.075"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 200,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
},
|
||||
"AERODROME_WETH-USDC_008": {
|
||||
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC Stable",
|
||||
"COIN_SYMBOL": "ETH",
|
||||
"RPC_ENV_VAR": "BASE_RPC_URL",
|
||||
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
|
||||
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
|
||||
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
|
||||
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
|
||||
"POOL_FEE": 1, # TickSpacing 1 pool (0xdbc6...)
|
||||
"RANGE_WIDTH_PCT": Decimal("0.0075"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 3000,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
457
clp_hedger.py
457
clp_hedger.py
@ -14,20 +14,15 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(current_dir)
|
||||
sys.path.append(project_root)
|
||||
|
||||
# Import local modules
|
||||
try:
|
||||
from logging_utils import setup_logging
|
||||
except ImportError:
|
||||
setup_logging = None
|
||||
# Ensure root logger is clean if we can't use setup_logging
|
||||
logging.getLogger().handlers.clear()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# Ensure root logger is clean
|
||||
logging.getLogger().handlers.clear()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
from eth_account import Account
|
||||
from hyperliquid.exchange import Exchange
|
||||
from hyperliquid.info import Info
|
||||
from hyperliquid.utils import constants
|
||||
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY
|
||||
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY, TARGET_DEX
|
||||
|
||||
# Load environment variables
|
||||
dotenv_path = os.path.join(current_dir, '.env')
|
||||
@ -213,7 +208,7 @@ class HyperliquidStrategy:
|
||||
else: # >=5% range
|
||||
return Decimal("0.075") # Standard for wide ranges
|
||||
|
||||
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict:
|
||||
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal, strategy_type: str = "ASYMMETRIC") -> Dict:
|
||||
# Note: current_short_size here is virtual (just for this specific strategy),
|
||||
# but the unified hedger will use the 'target_short' output primarily.
|
||||
|
||||
@ -221,6 +216,8 @@ class HyperliquidStrategy:
|
||||
|
||||
# --- ASYMMETRIC COMPENSATION ---
|
||||
adj_pct = Decimal("0.0")
|
||||
|
||||
if strategy_type == "ASYMMETRIC":
|
||||
range_width = self.high_range - self.low_range
|
||||
|
||||
if range_width > 0:
|
||||
@ -231,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)
|
||||
@ -273,12 +284,23 @@ class UnifiedHedger:
|
||||
|
||||
# Market Data Cache
|
||||
self.last_prices = {}
|
||||
self.price_history = {} # Symbol -> List[Decimal]
|
||||
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
|
||||
self.last_trade_times = {} # Symbol -> timestamp
|
||||
self.last_idle_log_times = {} # Symbol -> timestamp
|
||||
|
||||
# Shadow Orders (Global List)
|
||||
self.shadow_orders = []
|
||||
|
||||
# State: Emergency Close Hysteresis
|
||||
# Map: (file_path, token_id) -> bool
|
||||
self.emergency_close_active = {}
|
||||
|
||||
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
|
||||
self.custom_fixed_targets = {}
|
||||
|
||||
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
|
||||
self.hedge_entry_prices = {}
|
||||
|
||||
self.startup_time = time.time()
|
||||
|
||||
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
|
||||
@ -286,6 +308,7 @@ class UnifiedHedger:
|
||||
|
||||
def _init_coin_configs(self):
|
||||
"""Pre-load configuration for known coins from CLP_PROFILES."""
|
||||
# 1. Load all profiles (order depends on dict iteration)
|
||||
for profile_key, profile_data in CLP_PROFILES.items():
|
||||
symbol = profile_data.get("COIN_SYMBOL")
|
||||
if symbol:
|
||||
@ -297,6 +320,18 @@ class UnifiedHedger:
|
||||
# Update with Profile Specifics
|
||||
self.coin_configs[symbol].update(profile_data)
|
||||
|
||||
# 2. Force overwrite with TARGET_DEX profile to ensure precedence
|
||||
target_profile = CLP_PROFILES.get(TARGET_DEX)
|
||||
if target_profile:
|
||||
symbol = target_profile.get("COIN_SYMBOL")
|
||||
if symbol:
|
||||
if symbol not in self.coin_configs:
|
||||
self.coin_configs[symbol] = DEFAULT_STRATEGY.copy()
|
||||
self.coin_configs[symbol]["sz_decimals"] = 4
|
||||
|
||||
logger.info(f"Overwriting config for {symbol} using TARGET_DEX: {TARGET_DEX}")
|
||||
self.coin_configs[symbol].update(target_profile)
|
||||
|
||||
def _get_sz_decimals(self, coin: str) -> int:
|
||||
try:
|
||||
meta = self.info.meta()
|
||||
@ -427,6 +462,7 @@ class UnifiedHedger:
|
||||
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
|
||||
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
|
||||
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
|
||||
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
|
||||
@ -478,12 +514,23 @@ class UnifiedHedger:
|
||||
"start_time": start_time_ms,
|
||||
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
|
||||
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
|
||||
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
|
||||
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
|
||||
"entry_price": entry_price, # Store for fishing logic
|
||||
"status": position_data.get('status', 'OPEN')
|
||||
}
|
||||
|
||||
# Initial hedge entry price is the CLP entry price
|
||||
self.hedge_entry_prices[key] = entry_price
|
||||
|
||||
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
|
||||
|
||||
# Ensure JSON has these fields initialized
|
||||
update_position_stats(key[0], key[1], {
|
||||
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
|
||||
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to init strategy {key[1]}: {e}")
|
||||
|
||||
@ -652,7 +699,7 @@ class UnifiedHedger:
|
||||
price = to_decimal(mids[coin])
|
||||
self.last_prices[coin] = price
|
||||
|
||||
# Update Price History
|
||||
# Update Price History (Fast)
|
||||
if coin not in self.price_history: self.price_history[coin] = []
|
||||
self.price_history[coin].append(price)
|
||||
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
|
||||
@ -663,17 +710,58 @@ class UnifiedHedger:
|
||||
if coin not in self.last_prices: continue
|
||||
price = self.last_prices[coin]
|
||||
|
||||
# Get Config & Strategy Type
|
||||
config = self.coin_configs.get(coin, {})
|
||||
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
|
||||
|
||||
# Calc Logic
|
||||
calc = strat.calculate_rebalance(price, Decimal("0"))
|
||||
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
|
||||
|
||||
if coin not in aggregates:
|
||||
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
|
||||
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
|
||||
|
||||
if status == 'CLOSING':
|
||||
# If Closing, we want target to be 0 for this strategy
|
||||
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
|
||||
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
|
||||
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
|
||||
|
||||
is_active_hysteresis = self.emergency_close_active.get(key, False)
|
||||
|
||||
if is_active_hysteresis:
|
||||
# CHECK RESET CONDITION
|
||||
if strategy_type == "FIXED":
|
||||
# Reset at 75% of range (from Bottom)
|
||||
range_width = strat.high_range - strat.low_range
|
||||
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
|
||||
else:
|
||||
reset_threshold = strat.high_range * Decimal("0.999")
|
||||
|
||||
if price < reset_threshold:
|
||||
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
|
||||
self.emergency_close_active[key] = False
|
||||
is_active_hysteresis = False
|
||||
|
||||
# Capture NEW Dynamic Fixed Target and Entry Price
|
||||
if strategy_type == "FIXED":
|
||||
dynamic_delta = strat.get_pool_delta(price)
|
||||
self.custom_fixed_targets[key] = dynamic_delta
|
||||
self.hedge_entry_prices[key] = price
|
||||
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
|
||||
|
||||
if not is_active_hysteresis:
|
||||
# CHECK TRIGGER CONDITION
|
||||
if price >= strat.high_range:
|
||||
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
|
||||
self.emergency_close_active[key] = True
|
||||
is_active_hysteresis = True
|
||||
# Reset entry price when closed
|
||||
self.hedge_entry_prices[key] = Decimal("0")
|
||||
|
||||
if status == 'CLOSING' or is_active_hysteresis:
|
||||
# If Closing OR Hysteresis Active, target is 0
|
||||
aggregates[coin]['is_closing'] = True
|
||||
# Do not add to target_short
|
||||
else:
|
||||
# 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']
|
||||
|
||||
@ -693,6 +781,8 @@ class UnifiedHedger:
|
||||
|
||||
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
|
||||
aggregates[coin]['is_at_edge'] = True
|
||||
if dist_bottom_pct < safety_margin_pct:
|
||||
aggregates[coin]['is_at_bottom_edge'] = True
|
||||
|
||||
# Check Shadow Orders (Pre-Execution)
|
||||
self.check_shadow_orders(l2_snapshots)
|
||||
@ -706,255 +796,256 @@ class UnifiedHedger:
|
||||
for coin in coins_to_process:
|
||||
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
|
||||
|
||||
price = self.last_prices.get(coin, Decimal("0")) # FIX: Explicitly get price for this coin
|
||||
price = self.last_prices.get(coin, Decimal("0"))
|
||||
if price == 0: continue
|
||||
|
||||
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short)
|
||||
target_position = -target_short_abs # We want to be Short, so negative size
|
||||
|
||||
target_short_abs = data['target_short']
|
||||
target_position = -target_short_abs
|
||||
current_pos = current_positions.get(coin, Decimal("0"))
|
||||
|
||||
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
|
||||
diff = target_position - current_pos
|
||||
diff_abs = abs(diff)
|
||||
|
||||
# Thresholds
|
||||
config = self.coin_configs.get(coin, {})
|
||||
min_thresh = config.get("min_threshold", Decimal("0.008"))
|
||||
|
||||
# Volatility Multiplier
|
||||
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
|
||||
vol_pct = self.calculate_volatility(coin)
|
||||
base_vol = Decimal("0.0005")
|
||||
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
|
||||
|
||||
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
|
||||
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
|
||||
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
|
||||
|
||||
# FORCE EDGE CLEANUP
|
||||
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
|
||||
if data['is_at_edge'] and enable_edge_cleanup:
|
||||
if dynamic_thresh > min_thresh:
|
||||
# logger.info(f"[EDGE] {coin} forced to min threshold.")
|
||||
dynamic_thresh = min_thresh
|
||||
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
|
||||
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
|
||||
|
||||
# Check Trigger
|
||||
action_needed = diff_abs > dynamic_thresh
|
||||
|
||||
# Determine Intent (Moved UP for Order Logic)
|
||||
is_buy_bool = diff > 0
|
||||
side_str = "BUY" if is_buy_bool else "SELL"
|
||||
|
||||
# Manage Existing Orders
|
||||
existing_orders = orders_map.get(coin, [])
|
||||
force_taker_retry = False
|
||||
|
||||
# Fishing Config
|
||||
enable_fishing = config.get("ENABLE_FISHING", False)
|
||||
fishing_timeout = config.get("FISHING_TIMEOUT_FALLBACK", 30)
|
||||
|
||||
# Check Existing Orders for compatibility
|
||||
order_matched = False
|
||||
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
|
||||
|
||||
for o in existing_orders:
|
||||
o_oid = o['oid']
|
||||
o_price = to_decimal(o['limitPx'])
|
||||
o_side = o['side'] # 'B' or 'A'
|
||||
o_side = o['side']
|
||||
o_timestamp = o.get('timestamp', int(time.time()*1000))
|
||||
|
||||
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
|
||||
|
||||
# Price Check (within buffer)
|
||||
dist_pct = abs(price - o_price) / price
|
||||
|
||||
# Maker Timeout Check (General)
|
||||
maker_timeout = config.get("MAKER_ORDER_TIMEOUT", 300)
|
||||
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
|
||||
|
||||
if is_same_side and order_age_sec > maker_timeout:
|
||||
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.")
|
||||
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
|
||||
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
|
||||
self.cancel_order(coin, o_oid)
|
||||
continue
|
||||
|
||||
# Fishing Timeout Check
|
||||
if enable_fishing and is_same_side and order_age_sec > fishing_timeout:
|
||||
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
|
||||
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
|
||||
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
|
||||
self.cancel_order(coin, o_oid)
|
||||
force_taker_retry = True
|
||||
continue # Do not mark matched, let it flow to execution
|
||||
continue
|
||||
|
||||
if is_same_side and dist_pct < price_buffer_pct:
|
||||
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
|
||||
order_matched = True
|
||||
if int(time.time()) % 10 == 0:
|
||||
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%) | Age: {order_age_sec:.1f}s")
|
||||
if int(time.time()) % 15 == 0:
|
||||
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
|
||||
break
|
||||
else:
|
||||
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
|
||||
self.cancel_order(coin, o_oid)
|
||||
|
||||
# --- EXECUTION LOGIC ---
|
||||
if not order_matched:
|
||||
if action_needed or force_taker_retry:
|
||||
# Determine Urgency / Bypass Cooldown
|
||||
bypass_cooldown = False
|
||||
force_maker = False
|
||||
|
||||
# 0. Forced Taker Retry (Fishing Timeout)
|
||||
if force_taker_retry:
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker")
|
||||
|
||||
# 1. Urgent Closing -> Taker
|
||||
elif data.get('is_closing', False):
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit")
|
||||
|
||||
# 2. Ghost/Cleanup -> Maker
|
||||
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
|
||||
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce")
|
||||
else:
|
||||
logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)")
|
||||
continue # Skip execution for this coin
|
||||
if time.time() - self.startup_time > 5: force_maker = True
|
||||
else: continue # Skip startup ghost positions
|
||||
|
||||
# Large Hedge Check (Only Force Taker if AT EDGE)
|
||||
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
|
||||
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
|
||||
# Prevent IOC for BUYs at bottom edge
|
||||
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
|
||||
elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
|
||||
# Large hedge but safe zone -> Maker is fine, but maybe log it
|
||||
logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.")
|
||||
|
||||
# --- 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)
|
||||
|
||||
min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60)
|
||||
can_trade = False
|
||||
if bypass_cooldown:
|
||||
can_trade = True
|
||||
elif time.time() - last_trade > min_time_trade:
|
||||
can_trade = True
|
||||
|
||||
if can_trade:
|
||||
# Get Orderbook for Price
|
||||
if coin not in l2_snapshots:
|
||||
l2_snapshots[coin] = self.info.l2_snapshot(coin)
|
||||
|
||||
if bypass_cooldown or (time.time() - last_trade > min_time):
|
||||
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
|
||||
levels = l2_snapshots[coin]['levels']
|
||||
if not levels[0] or not levels[1]: continue
|
||||
|
||||
bid = to_decimal(levels[0][0]['px'])
|
||||
ask = to_decimal(levels[1][0]['px'])
|
||||
|
||||
# Price logic
|
||||
create_shadow = False
|
||||
|
||||
# Decide Order Type: Taker (Ioc) or Maker (Alo)
|
||||
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo)
|
||||
|
||||
# Logic:
|
||||
# If Force Maker -> Alo
|
||||
# Else if Urgent -> Ioc
|
||||
# Else if Enable Fishing -> Alo
|
||||
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
|
||||
|
||||
# Let's clarify:
|
||||
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo.
|
||||
# New logic:
|
||||
# If bypass_cooldown -> Ioc
|
||||
# Else -> Alo (Fishing)
|
||||
|
||||
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"
|
||||
create_shadow = True
|
||||
else:
|
||||
# Fishing / Standard Maker
|
||||
exec_price = bid if is_buy_bool else ask
|
||||
order_type = "Alo"
|
||||
|
||||
logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f} | Type: {order_type}")
|
||||
|
||||
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()
|
||||
|
||||
# Shadow Order
|
||||
if create_shadow:
|
||||
if order_type == "Ioc":
|
||||
shadow_price = bid if is_buy_bool else ask
|
||||
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
|
||||
self.shadow_orders.append({
|
||||
'coin': coin,
|
||||
'side': side_str,
|
||||
'price': shadow_price,
|
||||
'expires_at': time.time() + shadow_timeout
|
||||
})
|
||||
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
|
||||
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
|
||||
|
||||
# UPDATED: Sleep for API Lag (Phase 5.1)
|
||||
logger.info("Sleeping 10s to allow position update...")
|
||||
logger.info("Sleeping 10s for position update...")
|
||||
time.sleep(10)
|
||||
|
||||
# --- UPDATE CLOSED PnL FROM API ---
|
||||
self._update_closed_pnl(coin)
|
||||
else:
|
||||
# Cooldown log
|
||||
pass
|
||||
# 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:
|
||||
# Action NOT needed
|
||||
# Cleanup any dangling orders
|
||||
if existing_orders:
|
||||
for o in existing_orders:
|
||||
logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})")
|
||||
self.cancel_order(coin, o['oid'])
|
||||
hedge_pnl_curr = Decimal("0")
|
||||
|
||||
# --- IDLE LOGGING (Restored Format) ---
|
||||
# Calculate aggregate Gamma to estimate triggers
|
||||
# Gamma = 0.5 * Sum(L) * P^-1.5
|
||||
# We need Sum(L) for this coin.
|
||||
total_L = Decimal("0")
|
||||
# We need to re-iterate or cache L.
|
||||
# Simpler: Just re-sum L from active strats for this coin.
|
||||
for key, strat in self.strategies.items():
|
||||
if self.strategy_states[key]['coin'] == coin:
|
||||
total_L += strat.L
|
||||
fee_close_curr = (target_size * price) * Decimal("0.000432")
|
||||
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
|
||||
|
||||
if total_L > 0 and price > 0:
|
||||
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5")))
|
||||
if gamma > 0:
|
||||
# Equilibrium Price (Diff = 0)
|
||||
p_mid = price + (diff / gamma)
|
||||
# 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))
|
||||
|
||||
# Triggers
|
||||
p_buy = price + (dynamic_thresh + diff) / gamma
|
||||
p_sell = price - (dynamic_thresh - diff) / gamma
|
||||
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
|
||||
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
|
||||
|
||||
if int(time.time()) % 30 == 0:
|
||||
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 ""
|
||||
adj_val = data.get('adj_pct', Decimal("0")) * 100
|
||||
|
||||
# PnL Calc
|
||||
unrealized = current_pnls.get(coin, Decimal("0"))
|
||||
closed_pnl_total = Decimal("0")
|
||||
fees_total = Decimal("0")
|
||||
for k, s_state in self.strategy_states.items():
|
||||
if s_state['coin'] == coin:
|
||||
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
|
||||
fees_total += s_state.get('fees', Decimal("0"))
|
||||
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
|
||||
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
|
||||
total_pnl = (closed_pnl - fees) + unrealized
|
||||
|
||||
total_pnl = (closed_pnl_total - fees_total) + unrealized
|
||||
# Log individual strategy PnL
|
||||
if strategy_type == "FIXED":
|
||||
for k_strat, strat_inst in self.strategies.items():
|
||||
if self.strategy_states[k_strat]['coin'] != coin: continue
|
||||
|
||||
pnl_pad = " " if unrealized >= 0 else ""
|
||||
tot_pnl_pad = " " if total_pnl >= 0 else ""
|
||||
# Recalculate for logging (including bounds)
|
||||
clp_curr_val = get_clp_value(price, strat_inst)
|
||||
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
|
||||
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
|
||||
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {adj_val:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f}{pnl_pad} | TotPnL: {total_pnl:.2f}{tot_pnl_pad}")
|
||||
# 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:
|
||||
if int(time.time()) % 30 == 0:
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
|
||||
else:
|
||||
if int(time.time()) % 30 == 0:
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
|
||||
|
||||
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))
|
||||
|
||||
|
||||
422
clp_manager.py
422
clp_manager.py
@ -63,65 +63,34 @@ formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# --- ABIs ---
|
||||
# (Kept minimal for brevity, normally would load from files)
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||
[
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
|
||||
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_POOL_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
ERC20_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_FACTORY_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
SWAP_ROUTER_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
WETH9_ABI = json.loads('''
|
||||
[
|
||||
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
|
||||
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
from clp_abis import (
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI,
|
||||
UNISWAP_V3_POOL_ABI,
|
||||
ERC20_ABI,
|
||||
UNISWAP_V3_FACTORY_ABI,
|
||||
AERODROME_FACTORY_ABI,
|
||||
AERODROME_POOL_ABI,
|
||||
AERODROME_NPM_ABI,
|
||||
SWAP_ROUTER_ABI,
|
||||
WETH9_ABI
|
||||
)
|
||||
|
||||
from clp_config import get_current_config, STATUS_FILE
|
||||
from tools.universal_swapper import execute_swap
|
||||
|
||||
# --- GET ACTIVE DEX CONFIG ---
|
||||
CONFIG = get_current_config()
|
||||
|
||||
DEX_TO_CHAIN = {
|
||||
"UNISWAP_V3": "ARBITRUM",
|
||||
"UNISWAP_wide": "ARBITRUM",
|
||||
"PANCAKESWAP_BNB": "BSC",
|
||||
"WETH_CBBTC_BASE": "BASE",
|
||||
"UNISWAP_BASE_CL": "BASE",
|
||||
"AERODROME_BASE_CL": "BASE",
|
||||
"AERODROME_WETH-USDC_008": "BASE"
|
||||
}
|
||||
|
||||
# --- CONFIGURATION FROM STRATEGY ---
|
||||
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
|
||||
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
|
||||
@ -130,11 +99,77 @@ REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", Tr
|
||||
TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
|
||||
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
|
||||
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01"))
|
||||
RANGE_MODE = CONFIG.get("RANGE_MODE", "FIXED")
|
||||
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
|
||||
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
|
||||
|
||||
# --- AUTO RANGE HELPERS ---
|
||||
|
||||
def get_market_indicators() -> Optional[Dict]:
|
||||
file_path = os.path.join("market_data", "indicators.json")
|
||||
if not os.path.exists(file_path):
|
||||
return None
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check Freshness (5m)
|
||||
last_updated_str = data.get("last_updated")
|
||||
if not last_updated_str: return None
|
||||
|
||||
last_updated = datetime.fromisoformat(last_updated_str)
|
||||
if (datetime.now() - last_updated).total_seconds() > 300:
|
||||
logger.warning("⚠️ Market indicators file is stale (>5m).")
|
||||
return None
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading indicators: {e}")
|
||||
return None
|
||||
|
||||
def calculate_dynamic_range_pct(coin: str) -> Optional[Decimal]:
|
||||
indicators = get_market_indicators()
|
||||
if not indicators: return None
|
||||
|
||||
# Normalize symbols (Hyperliquid uses ETH, BNB while DEX uses WETH, WBNB)
|
||||
symbol_map = {"WETH": "ETH", "WBNB": "BNB"}
|
||||
lookup_coin = symbol_map.get(coin.upper(), coin.upper())
|
||||
|
||||
coin_data = indicators.get("data", {}).get(lookup_coin)
|
||||
if not coin_data: return None
|
||||
|
||||
try:
|
||||
price = Decimal(str(coin_data["current_price"]))
|
||||
bb12 = coin_data["bb"]["12h"]
|
||||
bb_low = Decimal(str(bb12["lower"]))
|
||||
bb_high = Decimal(str(bb12["upper"]))
|
||||
ma88 = Decimal(str(coin_data["ma"]["88"]))
|
||||
|
||||
# Condition 2: Price inside BB 12h
|
||||
if not (bb_low <= price <= bb_high):
|
||||
logger.warning(f"⚖️ AUTO: Price {price:.2f} is outside BB 12h ({bb_low:.2f} - {bb_high:.2f}). Skipping AUTO.")
|
||||
return None
|
||||
|
||||
# Condition 3: MA 88 inside BB 12h
|
||||
if not (bb_low <= ma88 <= bb_high):
|
||||
logger.warning(f"⚖️ AUTO: MA 88 {ma88:.2f} is outside BB 12h. Skipping AUTO.")
|
||||
return None
|
||||
|
||||
# Calculation: Max distance to BB edge
|
||||
dist_low = abs(price - bb_low)
|
||||
dist_high = abs(price - bb_high)
|
||||
max_dist = max(dist_low, dist_high)
|
||||
|
||||
range_pct = max_dist / price
|
||||
return range_pct
|
||||
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
logger.error(f"Error in dynamic range calc: {e}")
|
||||
return None
|
||||
|
||||
# --- CONFIGURATION CONSTANTS ---
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
|
||||
# Router address not strictly needed for Manager if using universal_swapper, but kept for ref
|
||||
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
|
||||
# Arbitrum WETH/USDC (or generic T0/T1)
|
||||
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
|
||||
@ -311,7 +346,8 @@ def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int
|
||||
if pool_address == '0x0000000000000000000000000000000000000000':
|
||||
return None, None
|
||||
|
||||
pool_contract = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
|
||||
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
|
||||
pool_contract = w3.eth.contract(address=pool_address, abi=pool_abi)
|
||||
|
||||
return {
|
||||
"token0_address": token0_address, "token1_address": token1_address,
|
||||
@ -397,6 +433,7 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
|
||||
def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount, token0: str, token1: str, amount0_needed: int, amount1_needed: int, sqrt_price_x96: int, d0: int, d1: int) -> bool:
|
||||
"""
|
||||
Checks balances, wraps ETH if needed, and swaps ONLY the required surplus to meet deposit requirements.
|
||||
Uses universal_swapper for the swap execution.
|
||||
"""
|
||||
token0 = clean_address(token0)
|
||||
token1 = clean_address(token1)
|
||||
@ -444,12 +481,12 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
# Price of Token0 in terms of Token1
|
||||
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
|
||||
|
||||
swap_call = None
|
||||
token_in, token_out = None, None
|
||||
amount_in = 0
|
||||
chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
|
||||
|
||||
buffer_multiplier = Decimal("1.02") # 2% buffer for slippage/price moves
|
||||
token_in_sym, token_out_sym = None, None
|
||||
amount_in_float = 0.0
|
||||
|
||||
buffer_multiplier = Decimal("1.03")
|
||||
if deficit0 > 0 and bal1 > amount1_needed:
|
||||
# Need T0 (ETH), Have extra T1 (USDC)
|
||||
# Swap T1 -> T0
|
||||
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
surplus1 = bal1 - amount1_needed
|
||||
|
||||
if surplus1 >= amount_in_needed:
|
||||
token_in, token_out = token1, token0
|
||||
amount_in = amount_in_needed
|
||||
# Get Symbols
|
||||
token_in_sym = token1_c.functions.symbol().call().upper()
|
||||
token_out_sym = token0_c.functions.symbol().call().upper()
|
||||
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d1))
|
||||
|
||||
logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
|
||||
else:
|
||||
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}")
|
||||
@ -479,38 +519,46 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
surplus0 = bal0 - amount0_needed
|
||||
|
||||
if surplus0 >= amount_in_needed:
|
||||
token_in, token_out = token0, token1
|
||||
amount_in = amount_in_needed
|
||||
token_in_sym = token0_c.functions.symbol().call().upper()
|
||||
token_out_sym = token1_c.functions.symbol().call().upper()
|
||||
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d0))
|
||||
|
||||
logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
|
||||
else:
|
||||
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
|
||||
|
||||
if token_in and amount_in > 0:
|
||||
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...")
|
||||
if token_in_sym and amount_in_float > 0:
|
||||
logger.info(f"🔄 Delegating Swap to Universal Swapper: {amount_in_float} {token_in_sym} -> {token_out_sym} on {chain_name}...")
|
||||
try:
|
||||
# Use Standard Fee (500) if configured fee is weird (like 1 for Aerodrome tickSpacing)
|
||||
# This ensures the standard router finds a valid pool (WETH/USDC 0.05%)
|
||||
swap_fee = POOL_FEE if POOL_FEE >= 100 else 500
|
||||
|
||||
if not ensure_allowance(w3, account, token_in, UNISWAP_V3_SWAP_ROUTER_ADDRESS, amount_in):
|
||||
return False
|
||||
# Call Universal Swapper
|
||||
execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_float, fee_tier=swap_fee)
|
||||
|
||||
params = (
|
||||
token_in, token_out, POOL_FEE, account.address,
|
||||
int(time.time()) + 120,
|
||||
amount_in,
|
||||
0, # amountOutMin (Market swap for rebalance)
|
||||
0
|
||||
)
|
||||
# Wait for node indexing
|
||||
logger.info("⏳ Waiting for balance update...")
|
||||
time.sleep(2)
|
||||
|
||||
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
|
||||
# Retry check loop
|
||||
for i in range(3):
|
||||
bal0 = token0_c.functions.balanceOf(account.address).call()
|
||||
bal1 = token1_c.functions.balanceOf(account.address).call()
|
||||
# If we are strictly >= needed, great.
|
||||
|
||||
if bal0 >= amount0_needed and bal1 >= amount1_needed:
|
||||
logger.info("✅ Balances sufficient.")
|
||||
return True
|
||||
else:
|
||||
|
||||
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
|
||||
@ -531,14 +579,20 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
|
||||
amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
|
||||
|
||||
# 3. Mint
|
||||
params = (
|
||||
base_params = [
|
||||
token0, token1, POOL_FEE,
|
||||
tick_lower, tick_upper,
|
||||
amount0, amount1,
|
||||
amount0_min, amount1_min,
|
||||
account.address,
|
||||
int(time.time()) + 180
|
||||
)
|
||||
]
|
||||
|
||||
# Aerodrome Slipstream expects sqrtPriceX96 as the last parameter
|
||||
if "AERODROME" in os.environ.get("TARGET_DEX", "").upper():
|
||||
base_params.append(0) # sqrtPriceX96
|
||||
|
||||
params = tuple(base_params)
|
||||
|
||||
receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position")
|
||||
|
||||
@ -696,9 +750,43 @@ def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
|
||||
save_status_data(data)
|
||||
logger.info(f"💾 Updated Position {token_id} status to {status}")
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
|
||||
# --- REAL-TIME ORACLE HELPER ---
|
||||
def get_realtime_price(coin: str) -> Optional[Decimal]:
|
||||
"""Fetches current mid-price directly from Hyperliquid API (low latency)."""
|
||||
try:
|
||||
url = "https://api.hyperliquid.xyz/info"
|
||||
response = requests.post(url, json={"type": "allMids"}, timeout=2)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Hyperliquid symbols are usually clean (ETH, BNB)
|
||||
# Map common variations just in case
|
||||
target = coin.upper().replace("WETH", "ETH").replace("WBNB", "BNB")
|
||||
|
||||
if target in data:
|
||||
return Decimal(data[target])
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Failed to fetch realtime Oracle price: {e}")
|
||||
return None
|
||||
|
||||
# --- MAIN LOOP ---
|
||||
|
||||
def main():
|
||||
# --- ARGUMENT PARSING ---
|
||||
parser = argparse.ArgumentParser(description="Uniswap CLP Manager")
|
||||
parser.add_argument("--force", type=float, help="Force open a position with specific range width (e.g., 0.75), ignoring AUTO safe checks.")
|
||||
args = parser.parse_args()
|
||||
|
||||
force_mode_active = False
|
||||
force_width_pct = Decimal("0")
|
||||
|
||||
if args.force:
|
||||
force_mode_active = True
|
||||
force_width_pct = Decimal(str(args.force)) / 100 # Convert 0.75 -> 0.0075
|
||||
logger.warning(f"🚨 FORCE MODE ACTIVE: Will bypass safe checks for FIRST position with width {args.force}%")
|
||||
|
||||
logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
|
||||
load_dotenv(override=True)
|
||||
|
||||
@ -722,9 +810,23 @@ def main():
|
||||
logger.info(f"👤 Wallet: {account.address}")
|
||||
|
||||
# Contracts
|
||||
target_dex_name = os.environ.get("TARGET_DEX", "").upper()
|
||||
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
|
||||
logger.info("✈️ Using Aerodrome NPM ABI")
|
||||
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=AERODROME_NPM_ABI)
|
||||
else:
|
||||
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
|
||||
|
||||
factory_addr = npm.functions.factory().call()
|
||||
factory = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
|
||||
|
||||
# Select Factory ABI based on DEX type
|
||||
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
|
||||
logger.info("✈️ Using Aerodrome Factory ABI (tickSpacing instead of fee)")
|
||||
factory_abi = AERODROME_FACTORY_ABI
|
||||
else:
|
||||
factory_abi = UNISWAP_V3_FACTORY_ABI
|
||||
|
||||
factory = w3.eth.contract(address=factory_addr, abi=factory_abi)
|
||||
router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
|
||||
|
||||
while True:
|
||||
@ -817,7 +919,21 @@ def main():
|
||||
pnl_unrealized = current_pos_value_usd - initial_value
|
||||
total_pnl_usd = pnl_unrealized + total_fees_usd
|
||||
|
||||
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})"
|
||||
# --- PERSIST PERFORMANCE TO JSON ---
|
||||
update_position_status(token_id, "OPEN", {
|
||||
"clp_fees": round(float(total_fees_usd), 2),
|
||||
"clp_TotPnL": round(float(total_pnl_usd), 2)
|
||||
})
|
||||
|
||||
# Calculate Fees/h
|
||||
fees_per_h_str = "0.00"
|
||||
ts_open = active_auto_pos.get('timestamp_open')
|
||||
if ts_open:
|
||||
hours_open = (time.time() - ts_open) / 3600
|
||||
if hours_open > 0.01:
|
||||
fees_per_h_str = f"{float(total_fees_usd) / hours_open:.2f}"
|
||||
|
||||
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f} | ${fees_per_h_str}/h)"
|
||||
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
|
||||
|
||||
# --- KPI LOGGING ---
|
||||
@ -845,6 +961,32 @@ def main():
|
||||
|
||||
log_kpi_snapshot(snapshot)
|
||||
|
||||
# --- REPOSITION LOGIC ---
|
||||
pos_range_mode = active_auto_pos.get("range_mode", RANGE_MODE)
|
||||
|
||||
if pos_range_mode == "AUTO" and CLOSE_POSITION_ENABLED:
|
||||
coin_for_dynamic = pos_details['token0_symbol'] if not is_t0_stable else pos_details['token1_symbol']
|
||||
new_range_width = calculate_dynamic_range_pct(coin_for_dynamic)
|
||||
|
||||
if new_range_width:
|
||||
# Use initial width from JSON, or current config width as fallback
|
||||
old_range_width = Decimal(str(active_auto_pos.get("range_width_initial", RANGE_WIDTH_PCT)))
|
||||
|
||||
# Condition A: Difference > 20%
|
||||
width_diff_pct = abs(new_range_width - old_range_width) / old_range_width
|
||||
|
||||
# Condition B: Profit > 0.1%
|
||||
profit_pct = total_pnl_usd / initial_value
|
||||
|
||||
logger.info(f"📊 AUTO Check: CurRange {old_range_width*100:.2f}%, NewRange {new_range_width*100:.2f}% | Diff {width_diff_pct*100:.1f}% | Profit {profit_pct*100:.2f}%")
|
||||
|
||||
if width_diff_pct > 0.20 and profit_pct > 0.001:
|
||||
logger.warning(f"🔄 REPOSITION TRIGGERED: Width Diff {width_diff_pct*100:.1f}%, Profit {profit_pct*100:.2f}%")
|
||||
# Set in_range to False to force the closing logic below
|
||||
in_range = False
|
||||
else:
|
||||
logger.warning(f"⚖️ AUTO Check Skipped: Market indicators for {coin_for_dynamic} are stale or conditions not met.")
|
||||
|
||||
if not in_range and CLOSE_POSITION_ENABLED:
|
||||
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
|
||||
update_position_status(token_id, "CLOSING")
|
||||
@ -875,14 +1017,73 @@ def main():
|
||||
fee = POOL_FEE
|
||||
|
||||
pool_addr = factory.functions.getPool(token0, token1, fee).call()
|
||||
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
|
||||
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
|
||||
pool_c = w3.eth.contract(address=pool_addr, abi=pool_abi)
|
||||
pool_data = get_pool_dynamic_data(pool_c)
|
||||
|
||||
if pool_data:
|
||||
tick = pool_data['tick']
|
||||
# Define Range (+/- 2.5%)
|
||||
# log(1.025) / log(1.0001) approx 247 tick delta
|
||||
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001))
|
||||
|
||||
# --- PRE-CALCULATE ESSENTIALS ---
|
||||
# Fetch Decimals & Symbols immediately (Required for Oracle Check)
|
||||
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
d0 = t0_c.functions.decimals().call()
|
||||
d1 = t1_c.functions.decimals().call()
|
||||
|
||||
t0_sym = t0_c.functions.symbol().call().upper()
|
||||
t1_sym = t1_c.functions.symbol().call().upper()
|
||||
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
|
||||
|
||||
is_t1_stable = any(s in t1_sym for s in stable_symbols)
|
||||
is_t0_stable = any(s in t0_sym for s in stable_symbols)
|
||||
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
|
||||
# Define coin_sym early for Guard Rails
|
||||
coin_sym = CONFIG.get("COIN_SYMBOL", "ETH")
|
||||
|
||||
# --- ORACLE GUARD RAIL ---
|
||||
# Protect against Pool/Oracle divergence (Manipulation/Depeg/Lag)
|
||||
if not force_mode_active:
|
||||
oracle_price = get_realtime_price(coin_sym)
|
||||
|
||||
if oracle_price:
|
||||
pool_price_dec = price_0_in_1 if is_t1_stable else (Decimal("1") / price_0_in_1)
|
||||
divergence = abs(pool_price_dec - oracle_price) / oracle_price
|
||||
|
||||
if divergence > Decimal("0.0025"): # 0.25% Tolerance
|
||||
logger.warning(f"⚠️ Price Divergence! Pool: {pool_price_dec:.2f} vs Oracle: {oracle_price:.2f} (Diff: {divergence*100:.2f}%). Aborting.")
|
||||
time.sleep(10)
|
||||
continue
|
||||
else:
|
||||
logger.warning("⚠️ Could not fetch Oracle price. Proceeding with caution (or consider aborting).")
|
||||
|
||||
# --- DYNAMIC RANGE CALCULATION ---
|
||||
active_range_width = RANGE_WIDTH_PCT
|
||||
current_range_mode = RANGE_MODE
|
||||
|
||||
# 1. PRIORITY: Force Mode
|
||||
if force_mode_active:
|
||||
logger.warning(f"🚨 FORCE OVERRIDE: Using forced width {force_width_pct*100:.2f}% (Ignoring safe checks)")
|
||||
active_range_width = force_width_pct
|
||||
current_range_mode = "FIXED"
|
||||
|
||||
# 2. AUTO Mode (Only if not forced)
|
||||
elif RANGE_MODE == "AUTO":
|
||||
dynamic_width = calculate_dynamic_range_pct(coin_sym)
|
||||
if dynamic_width:
|
||||
active_range_width = dynamic_width
|
||||
logger.info(f"⚖️ AUTO Range Activated: {active_range_width*100:.4f}%")
|
||||
else:
|
||||
logger.info(f"⛔ AUTO conditions not met. Waiting for safe entry...")
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
continue # Skip logic
|
||||
|
||||
# 3. FIXED Mode (Default Fallback) is already set by initial active_range_width
|
||||
|
||||
# Define Range
|
||||
tick_delta = int(math.log(1 + float(active_range_width)) / math.log(1.0001))
|
||||
|
||||
# Fetch actual tick spacing from pool
|
||||
tick_spacing = pool_c.functions.tickSpacing().call()
|
||||
@ -893,28 +1094,10 @@ def main():
|
||||
|
||||
# Calculate Amounts
|
||||
# Target Value logic
|
||||
d0 = 18 # Default WETH (Corrected below if needed, but we rely on raw logic)
|
||||
# Actually, we should fetch decimals from contract to be safe, but config assumes standard.
|
||||
|
||||
# Fetch Decimals for precision
|
||||
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
d0 = t0_c.functions.decimals().call()
|
||||
d1 = t1_c.functions.decimals().call()
|
||||
|
||||
# Determine Investment Value in Token1 terms
|
||||
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC))
|
||||
|
||||
# Check which is stable
|
||||
t0_sym = t0_c.functions.symbol().call().upper()
|
||||
t1_sym = t1_c.functions.symbol().call().upper()
|
||||
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
|
||||
|
||||
is_t1_stable = any(s in t1_sym for s in stable_symbols)
|
||||
is_t0_stable = any(s in t0_sym for s in stable_symbols)
|
||||
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
|
||||
investment_val_token1 = Decimal("0")
|
||||
|
||||
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX":
|
||||
@ -939,10 +1122,33 @@ def main():
|
||||
amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_token1, d0, d1, pool_data['sqrtPriceX96'])
|
||||
|
||||
if check_and_swap_for_deposit(w3, router, account, token0, token1, amt0, amt1, pool_data['sqrtPriceX96'], d0, d1):
|
||||
# --- STALE DATA PROTECTION (Pre-Mint) ---
|
||||
# Check if price moved significantly during calculation/swap
|
||||
pre_mint_data = get_pool_dynamic_data(pool_c)
|
||||
if pre_mint_data:
|
||||
tick_diff = abs(pre_mint_data['tick'] - pool_data['tick'])
|
||||
# 13 ticks ~ 0.13% price move. Abort if volatile.
|
||||
if tick_diff > 13:
|
||||
logger.warning(f"⚠️ Price moved too much ({tick_diff} ticks) during setup/swap. Aborting mint to prevent bad entry.")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
|
||||
if minted:
|
||||
# Calculate entry price and amounts for JSON compatibility
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
# --- DISABLE FORCE MODE AFTER FIRST MINT ---
|
||||
if force_mode_active:
|
||||
logger.info("🛑 FORCE MODE CONSUMED: Returning to standard AUTO checks for future positions.")
|
||||
force_mode_active = False
|
||||
|
||||
# --- RE-FETCH PRICE FOR ACCURATE ENTRY DATA (Post-Mint) ---
|
||||
fresh_pool_data = get_pool_dynamic_data(pool_c)
|
||||
if fresh_pool_data:
|
||||
fresh_tick = fresh_pool_data['tick']
|
||||
price_0_in_1 = price_from_tick(fresh_tick, d0, d1)
|
||||
logger.info(f"🔄 Refreshed Entry Tick: {fresh_tick} (Was: {pool_data['tick']})")
|
||||
else:
|
||||
price_0_in_1 = price_from_tick(pool_data['tick'], d0, d1)
|
||||
|
||||
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
|
||||
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
|
||||
|
||||
@ -970,6 +1176,8 @@ def main():
|
||||
"range_lower": round(r_lower, 4),
|
||||
"token0_decimals": d0,
|
||||
"token1_decimals": d1,
|
||||
"range_mode": current_range_mode,
|
||||
"range_width_initial": float(active_range_width),
|
||||
"timestamp_open": int(time.time()),
|
||||
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
|
||||
}
|
||||
|
||||
104
doc/CLP_HEDGING_SIMULATIONS_2026.md
Normal file
104
doc/CLP_HEDGING_SIMULATIONS_2026.md
Normal 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 | |
|
||||
BIN
doc/CLP_HEDGING_SIMULATIONS_2026_GRID.md
Normal file
BIN
doc/CLP_HEDGING_SIMULATIONS_2026_GRID.md
Normal file
Binary file not shown.
44
florida/AERODROME_BASE_CL_status.json
Normal file
44
florida/AERODROME_BASE_CL_status.json
Normal 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
|
||||
}
|
||||
]
|
||||
@ -43,76 +43,29 @@ The system consists of three independent Python processes that coordinate via sh
|
||||
| `telegram_monitor.py` | Telegram bot for notifications. |
|
||||
| `{TARGET_DEX}_status.json` | **Critical:** Shared state file acting as the database between Manager and Hedger. |
|
||||
| `.env` | Stores secrets (Private Keys, RPCs). **Do not commit.** |
|
||||
| `tests/backtest/` | **New:** Professional Backtesting & Optimization Framework. |
|
||||
| `tools/` | Utility scripts, including the Git Agent for auto-backups. |
|
||||
| `logs/` | Detailed logs for all processes. |
|
||||
|
||||
## Configuration
|
||||
## Backtesting Framework (Jan 2026 Update)
|
||||
A robust simulation engine has been implemented to validate strategies before capital commitment.
|
||||
|
||||
### Environment Variables (`.env`)
|
||||
Required variables for operation:
|
||||
```env
|
||||
# Blockchain
|
||||
MAINNET_RPC_URL=... # Arbitrum
|
||||
BNB_RPC_URL=... # BNB Chain
|
||||
BASE_RPC_URL=... # Base
|
||||
MAIN_WALLET_PRIVATE_KEY=...
|
||||
MAIN_WALLET_ADDRESS=...
|
||||
### Components
|
||||
* **`tests/backtest/backtester.py`**: Event-driven engine mocking Web3/Hyperliquid interactions.
|
||||
* **`tests/backtest/mocks.py`**: Stateful simulator handling balance tracking, V3 tick math, and fee accrual.
|
||||
* **`tests/backtest/grid_search.py`**: Optimization runner to test parameter combinations (Range Width, Hedging Threshold).
|
||||
* **`tests/backtest/analyze_results.py`**: Helper to interpret simulation CSV results.
|
||||
|
||||
# Hyperliquid
|
||||
HEDGER_PRIVATE_KEY=... # Usually same as Main Wallet or specialized sub-account
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=...
|
||||
TELEGRAM_CHAT_ID=...
|
||||
TELEGRAM_MONITOR_ENABLED=True
|
||||
```
|
||||
|
||||
### Strategy Config (`clp_config.py`)
|
||||
Key parameters controlling the bot's behavior:
|
||||
* `TARGET_DEX`: Selects the active chain/profile (e.g., "UNISWAP_V3", "PANCAKESWAP_BNB").
|
||||
* `RANGE_WIDTH_PCT`: Width of the LP position (e.g., `0.05` for +/- 5%).
|
||||
* `TARGET_INVESTMENT_AMOUNT`: Notional size of the position in USD.
|
||||
* `SLIPPAGE_TOLERANCE`: Max slippage for minting/swapping.
|
||||
|
||||
## Usage
|
||||
|
||||
The system is designed to run continuously. It is recommended to use a process manager like `pm2` or `systemd`, or simply run in separate terminal tabs.
|
||||
|
||||
1. **Start the Manager:**
|
||||
```bash
|
||||
python clp_manager.py
|
||||
```
|
||||
* *Action:* Will check for existing positions. If none, it prepares to open one based on config.
|
||||
|
||||
2. **Start the Hedger:**
|
||||
```bash
|
||||
python clp_hedger.py
|
||||
```
|
||||
* *Action:* Will read the position created by the Manager and open a corresponding short on Hyperliquid.
|
||||
|
||||
3. **Start Monitoring (Optional):**
|
||||
```bash
|
||||
python telegram_monitor.py
|
||||
```
|
||||
|
||||
## Development & Git Agent
|
||||
|
||||
This project uses a custom **Git Agent** (`tools/git_agent.py`) for automated version control and backups.
|
||||
|
||||
* **Auto-Backup:** Runs hourly (if configured) to create backup branches (e.g., `backup-2025-01-01-12`).
|
||||
* **Manual Commit:**
|
||||
```bash
|
||||
python tools/git_agent.py --backup
|
||||
```
|
||||
* **Status:**
|
||||
```bash
|
||||
python tools/git_agent.py --status
|
||||
```
|
||||
* **Restoration:**
|
||||
To restore a file from a backup branch:
|
||||
```bash
|
||||
git checkout backup-BRANCH-NAME -- path/to/file.py
|
||||
```
|
||||
### Progress Status (Jan 1, 2026)
|
||||
* **Completed:**
|
||||
* Simulation loop runs end-to-end (Mint -> Accrue Fees -> Close).
|
||||
* Fixed Mock Pricing logic (handling inverted T0/T1 pairs like USDT/WBNB).
|
||||
* Implemented realistic Fee Accrual based on Trade Volume + Market Share.
|
||||
* Verified "In Range" detection and position lifecycle.
|
||||
* **Pending / Next Steps:**
|
||||
* **Hedger PnL Verification:** Simulation showed 0.0 Hedging Fees because the price volatility in the 1-day sample was too low to trigger the 10% rebalance threshold. We are lowering thresholds to 1% to force activity and verify costs.
|
||||
* **NAV Calculation:** Refine "Total PnL" to include Unrealized PnL from both LP and Hedge to handle Impermanent Loss correctly.
|
||||
* **Final Optimization:** Run the `grid_search.py` with the corrected Market Share (0.02%) and lower thresholds to find the profitable "Sweet Spot".
|
||||
|
||||
## Logic Details
|
||||
|
||||
|
||||
@ -201,9 +201,9 @@
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6164702,
|
||||
"status": "OPEN",
|
||||
"target_value": 981.88,
|
||||
"entry_price": 846.4517,
|
||||
"status": "CLOSED",
|
||||
"target_value": 993.41,
|
||||
"entry_price": 866.3337,
|
||||
"amount0_initial": 490.942,
|
||||
"amount1_initial": 0.58,
|
||||
"liquidity": "8220443727732589279738",
|
||||
@ -212,7 +212,471 @@
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767164052,
|
||||
"hedge_TotPnL": -0.026171,
|
||||
"hedge_fees_paid": 0.097756
|
||||
"hedge_TotPnL": -3.587319,
|
||||
"hedge_fees_paid": 0.723066,
|
||||
"clp_fees": 1.75,
|
||||
"clp_TotPnL": 0.31,
|
||||
"timestamp_close": 1767189814,
|
||||
"time_close": "31.12.25 15:03:34"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6166625,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.7,
|
||||
"entry_price": 873.896,
|
||||
"amount0_initial": 496.6816,
|
||||
"amount1_initial": 0.5722,
|
||||
"liquidity": "8653989263919246133281",
|
||||
"range_upper": 877.3107,
|
||||
"range_lower": 870.4946,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767190229,
|
||||
"time_open": "31.12.25 15:10:29",
|
||||
"hedge_TotPnL": 4.004047,
|
||||
"hedge_fees_paid": 0.807563,
|
||||
"clp_fees": 0.34,
|
||||
"clp_TotPnL": -3.96,
|
||||
"timestamp_close": 1767191809,
|
||||
"time_close": "31.12.25 15:36:49"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6166939,
|
||||
"status": "CLOSED",
|
||||
"target_value": 999.11,
|
||||
"entry_price": 866.9331,
|
||||
"amount0_initial": 500.0004,
|
||||
"amount1_initial": 0.5757,
|
||||
"liquidity": "8709690098157915483248",
|
||||
"range_upper": 870.3205,
|
||||
"range_lower": 863.5588,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767192966,
|
||||
"time_open": "31.12.25 15:56:06",
|
||||
"hedge_TotPnL": 1.064447,
|
||||
"hedge_fees_paid": 0.927408,
|
||||
"clp_fees": 0.3,
|
||||
"clp_TotPnL": -2.71,
|
||||
"timestamp_close": 1767193991,
|
||||
"time_close": "31.12.25 16:13:11"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6167093,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.69,
|
||||
"entry_price": 864.077,
|
||||
"amount0_initial": 500.0128,
|
||||
"amount1_initial": 0.5748,
|
||||
"liquidity": "8702875143941291654654",
|
||||
"range_upper": 867.4533,
|
||||
"range_lower": 860.7139,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767194522,
|
||||
"time_open": "31.12.25 16:22:02",
|
||||
"hedge_TotPnL": -3.013382,
|
||||
"hedge_fees_paid": 0.814047,
|
||||
"clp_fees": 0.95,
|
||||
"clp_TotPnL": -2.76,
|
||||
"timestamp_close": 1767199352,
|
||||
"time_close": "31.12.25 17:42:32"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6167590,
|
||||
"status": "CLOSED",
|
||||
"target_value": 998.37,
|
||||
"entry_price": 861.6611,
|
||||
"amount0_initial": 498.363,
|
||||
"amount1_initial": 0.5803,
|
||||
"liquidity": "8729751956580574272932",
|
||||
"range_upper": 865.028,
|
||||
"range_lower": 858.3074,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767200083,
|
||||
"time_open": "31.12.25 17:54:43",
|
||||
"hedge_TotPnL": -4.720271,
|
||||
"hedge_fees_paid": 1.311938,
|
||||
"clp_fees": 1.95,
|
||||
"clp_TotPnL": 2.92,
|
||||
"timestamp_close": 1767217535,
|
||||
"time_close": "31.12.25 22:45:35"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6168553,
|
||||
"status": "CLOSED",
|
||||
"target_value": 991.55,
|
||||
"entry_price": 865.4606,
|
||||
"amount0_initial": 491.5385,
|
||||
"amount1_initial": 0.5777,
|
||||
"liquidity": "8651067937842123260294",
|
||||
"range_upper": 868.8423,
|
||||
"range_lower": 862.092,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767217854,
|
||||
"time_open": "31.12.25 22:50:54",
|
||||
"hedge_TotPnL": -3.016562,
|
||||
"hedge_fees_paid": 0.460066,
|
||||
"clp_fees": 0.58,
|
||||
"clp_TotPnL": 1.55,
|
||||
"timestamp_close": 1767229894,
|
||||
"time_close": "01.01.26 02:11:34"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6169279,
|
||||
"status": "CLOSED",
|
||||
"target_value": 993.04,
|
||||
"entry_price": 869.1899,
|
||||
"amount0_initial": 493.031,
|
||||
"amount1_initial": 0.5753,
|
||||
"liquidity": "8645470844979366936741",
|
||||
"range_upper": 872.5862,
|
||||
"range_lower": 865.8068,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767230090,
|
||||
"time_open": "01.01.26 02:14:50",
|
||||
"hedge_TotPnL": -1.709208,
|
||||
"hedge_fees_paid": 0.300063,
|
||||
"clp_fees": 0.22,
|
||||
"clp_TotPnL": 1.19,
|
||||
"timestamp_close": 1767232654,
|
||||
"time_close": "01.01.26 02:57:34"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6169469,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.5,
|
||||
"entry_price": 873.4592,
|
||||
"amount0_initial": 496.4932,
|
||||
"amount1_initial": 0.5724,
|
||||
"liquidity": "8654359631059929427298",
|
||||
"range_upper": 876.8721,
|
||||
"range_lower": 870.0595,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767233101,
|
||||
"time_open": "01.01.26 03:05:01",
|
||||
"hedge_TotPnL": 0.379026,
|
||||
"hedge_fees_paid": 0.415621,
|
||||
"clp_fees": 0.4,
|
||||
"clp_TotPnL": -3.21,
|
||||
"timestamp_close": 1767238291,
|
||||
"time_close": "01.01.26 04:31:31"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6169789,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.05,
|
||||
"entry_price": 869.103,
|
||||
"amount0_initial": 500.0117,
|
||||
"amount1_initial": 0.5707,
|
||||
"liquidity": "8672126155624077647253",
|
||||
"range_upper": 872.4989,
|
||||
"range_lower": 865.7203,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767238369,
|
||||
"time_open": "01.01.26 04:32:49",
|
||||
"hedge_TotPnL": 3.954178,
|
||||
"hedge_fees_paid": 0.765854,
|
||||
"clp_fees": 0.21,
|
||||
"clp_TotPnL": -2.7,
|
||||
"timestamp_close": 1767242596,
|
||||
"time_close": "01.01.26 05:43:16"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6170135,
|
||||
"status": "CLOSED",
|
||||
"target_value": 998.15,
|
||||
"entry_price": 862.6094,
|
||||
"amount0_initial": 500.001,
|
||||
"amount1_initial": 0.5775,
|
||||
"liquidity": "8723056935772169247603",
|
||||
"range_upper": 865.98,
|
||||
"range_lower": 859.252,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767243101,
|
||||
"time_open": "01.01.26 05:51:41",
|
||||
"hedge_TotPnL": 2.409614,
|
||||
"hedge_fees_paid": 1.355821,
|
||||
"clp_fees": 0.64,
|
||||
"clp_TotPnL": -2.48,
|
||||
"timestamp_close": 1767254432,
|
||||
"time_close": "01.01.26 09:00:32"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6170841,
|
||||
"status": "CLOSED",
|
||||
"target_value": 998.62,
|
||||
"entry_price": 859.8536,
|
||||
"amount0_initial": 498.6144,
|
||||
"amount1_initial": 0.5815,
|
||||
"liquidity": "8741115554990437903852",
|
||||
"range_upper": 863.2134,
|
||||
"range_lower": 856.5069,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767254603,
|
||||
"time_open": "01.01.26 09:03:23",
|
||||
"hedge_TotPnL": -4.244326,
|
||||
"hedge_fees_paid": 1.827099,
|
||||
"clp_fees": 2.59,
|
||||
"clp_TotPnL": 3.56,
|
||||
"timestamp_close": 1767308203,
|
||||
"time_close": "01.01.26 23:56:43"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6175190,
|
||||
"status": "CLOSED",
|
||||
"target_value": 998.54,
|
||||
"entry_price": 863.8179,
|
||||
"amount0_initial": 498.5396,
|
||||
"amount1_initial": 0.5788,
|
||||
"liquidity": "8720378230633469405596",
|
||||
"range_upper": 867.1932,
|
||||
"range_lower": 860.4557,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767308440,
|
||||
"time_open": "02.01.26 00:00:40",
|
||||
"hedge_TotPnL": 2.712563,
|
||||
"hedge_fees_paid": 0.819224,
|
||||
"clp_fees": 0.62,
|
||||
"clp_TotPnL": -2.49,
|
||||
"timestamp_close": 1767320335,
|
||||
"time_close": "02.01.26 03:18:55"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6175868,
|
||||
"status": "CLOSED",
|
||||
"target_value": 991.53,
|
||||
"entry_price": 860.2836,
|
||||
"amount0_initial": 491.5252,
|
||||
"amount1_initial": 0.5812,
|
||||
"liquidity": "8676952736685102236300",
|
||||
"range_upper": 863.6451,
|
||||
"range_lower": 856.9352,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767320584,
|
||||
"time_open": "02.01.26 03:23:04",
|
||||
"hedge_TotPnL": -1.782405,
|
||||
"hedge_fees_paid": 0.312615,
|
||||
"clp_fees": 0.11,
|
||||
"clp_TotPnL": 1.09,
|
||||
"timestamp_close": 1767323453,
|
||||
"time_close": "02.01.26 04:10:53"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6176051,
|
||||
"status": "CLOSED",
|
||||
"target_value": 997.7,
|
||||
"entry_price": 863.7315,
|
||||
"amount0_initial": 497.694,
|
||||
"amount1_initial": 0.5789,
|
||||
"liquidity": "8713457799891424871655",
|
||||
"range_upper": 867.1064,
|
||||
"range_lower": 860.3697,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767324323,
|
||||
"time_open": "02.01.26 04:25:23",
|
||||
"hedge_TotPnL": -3.840822,
|
||||
"hedge_fees_paid": 0.892717,
|
||||
"clp_fees": 0.65,
|
||||
"clp_TotPnL": 1.63,
|
||||
"timestamp_close": 1767335965,
|
||||
"time_close": "02.01.26 07:39:25"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6176727,
|
||||
"status": "CLOSED",
|
||||
"target_value": 990.64,
|
||||
"entry_price": 867.5401,
|
||||
"amount0_initial": 490.6325,
|
||||
"amount1_initial": 0.5764,
|
||||
"liquidity": "8632807640200638943476",
|
||||
"range_upper": 870.9299,
|
||||
"range_lower": 864.1634,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767336634,
|
||||
"time_open": "02.01.26 07:50:34",
|
||||
"hedge_TotPnL": -5.442897,
|
||||
"hedge_fees_paid": 0.85406,
|
||||
"clp_fees": 0.91,
|
||||
"clp_TotPnL": 1.88,
|
||||
"timestamp_close": 1767347410,
|
||||
"time_close": "02.01.26 10:50:10"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6177360,
|
||||
"status": "CLOSED",
|
||||
"target_value": 993.15,
|
||||
"entry_price": 870.8428,
|
||||
"amount0_initial": 493.1411,
|
||||
"amount1_initial": 0.5742,
|
||||
"liquidity": "8638221415835012765221",
|
||||
"range_upper": 874.2456,
|
||||
"range_lower": 867.4533,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767347607,
|
||||
"time_open": "02.01.26 10:53:27",
|
||||
"hedge_TotPnL": 1.294631,
|
||||
"hedge_fees_paid": 1.047541,
|
||||
"clp_fees": 1.24,
|
||||
"clp_TotPnL": -1.66,
|
||||
"timestamp_close": 1767363551,
|
||||
"time_close": "02.01.26 15:19:11"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6185008,
|
||||
"status": "CLOSED",
|
||||
"target_value": 992.5,
|
||||
"entry_price": 867.1064,
|
||||
"amount0_initial": 492.4924,
|
||||
"amount1_initial": 0.5766,
|
||||
"liquidity": "8651147821199061055073",
|
||||
"range_upper": 870.4946,
|
||||
"range_lower": 863.7315,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767363751,
|
||||
"time_open": "02.01.26 15:22:31",
|
||||
"hedge_TotPnL": -2.674848,
|
||||
"hedge_fees_paid": 0.393713,
|
||||
"clp_fees": 0.32,
|
||||
"clp_TotPnL": 1.28,
|
||||
"timestamp_close": 1767364835,
|
||||
"time_close": "02.01.26 15:40:35"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6185867,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.08,
|
||||
"entry_price": 875.5579,
|
||||
"amount0_initial": 496.0791,
|
||||
"amount1_initial": 0.5711,
|
||||
"liquidity": "8640396990671870185711",
|
||||
"range_upper": 878.9791,
|
||||
"range_lower": 872.15,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767365969,
|
||||
"time_open": "02.01.26 15:59:29",
|
||||
"hedge_TotPnL": 2.830618,
|
||||
"hedge_fees_paid": 1.060949,
|
||||
"clp_fees": 0.53,
|
||||
"clp_TotPnL": -2.97,
|
||||
"timestamp_close": 1767367303,
|
||||
"time_close": "02.01.26 16:21:43"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6186334,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.88,
|
||||
"entry_price": 873.9834,
|
||||
"amount0_initial": 496.8714,
|
||||
"amount1_initial": 0.5721,
|
||||
"liquidity": "8655088063104153413073",
|
||||
"range_upper": 877.3984,
|
||||
"range_lower": 870.5816,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767367883,
|
||||
"time_open": "02.01.26 16:31:23",
|
||||
"hedge_TotPnL": -3.585575,
|
||||
"hedge_fees_paid": 0.57548,
|
||||
"clp_fees": 0.13,
|
||||
"clp_TotPnL": 1.1,
|
||||
"timestamp_close": 1767368416,
|
||||
"time_close": "02.01.26 16:40:16"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6186613,
|
||||
"status": "CLOSED",
|
||||
"target_value": 993.42,
|
||||
"entry_price": 880.2984,
|
||||
"amount0_initial": 493.4134,
|
||||
"amount1_initial": 0.568,
|
||||
"liquidity": "8594082766621558309079",
|
||||
"range_upper": 883.7381,
|
||||
"range_lower": 876.8721,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767368652,
|
||||
"time_open": "02.01.26 16:44:12",
|
||||
"hedge_TotPnL": -4.384157,
|
||||
"hedge_fees_paid": 0.627319,
|
||||
"clp_fees": 0.92,
|
||||
"clp_TotPnL": 1.89,
|
||||
"timestamp_close": 1767371545,
|
||||
"time_close": "02.01.26 17:32:25"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6187096,
|
||||
"status": "CLOSED",
|
||||
"target_value": 996.91,
|
||||
"entry_price": 885.9501,
|
||||
"amount0_initial": 496.8996,
|
||||
"amount1_initial": 0.5644,
|
||||
"liquidity": "2271526539550158344821",
|
||||
"range_upper": 899.159,
|
||||
"range_lower": 872.9353,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767372001,
|
||||
"time_open": "02.01.26 17:40:01",
|
||||
"hedge_TotPnL": 7.60386,
|
||||
"hedge_fees_paid": 0.283695,
|
||||
"clp_fees": 1.38,
|
||||
"clp_TotPnL": -9.61,
|
||||
"timestamp_close": 1767424468,
|
||||
"time_close": "03.01.26 08:14:28"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 6203530,
|
||||
"status": "CLOSED",
|
||||
"target_value": 998.23,
|
||||
"entry_price": 872.0628,
|
||||
"amount0_initial": 500.0027,
|
||||
"amount1_initial": 0.5713,
|
||||
"liquidity": "2292568457223553397610",
|
||||
"range_upper": 885.0647,
|
||||
"range_lower": 859.252,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 18,
|
||||
"timestamp_open": 1767424756,
|
||||
"time_open": "03.01.26 08:19:16",
|
||||
"hedge_TotPnL": -2.47676,
|
||||
"hedge_fees_paid": 0.287962,
|
||||
"clp_fees": 0.19,
|
||||
"clp_TotPnL": 1.92
|
||||
}
|
||||
]
|
||||
@ -288,7 +288,7 @@
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5182179,
|
||||
"status": "OPEN",
|
||||
"status": "CLOSED",
|
||||
"target_value": 1993.84,
|
||||
"entry_price": 2969.9855,
|
||||
"amount0_initial": 0.3347,
|
||||
@ -299,7 +299,607 @@
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1766968369,
|
||||
"hedge_TotPnL": -5.078135,
|
||||
"hedge_fees_paid": 2.029157
|
||||
"hedge_TotPnL": -62.166433,
|
||||
"hedge_fees_paid": 2.587208,
|
||||
"clp_fees": 23.23,
|
||||
"clp_TotPnL": 47.64,
|
||||
"timestamp_close": 1767371501,
|
||||
"time_close": "02.01.26 17:31:41"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5190205,
|
||||
"status": "CLOSED",
|
||||
"target_value": 1990.81,
|
||||
"entry_price": 3136.9306,
|
||||
"amount0_initial": 0.2991,
|
||||
"amount1_initial": 1052.4842,
|
||||
"liquidity": "3750669047237424",
|
||||
"range_upper": 3165.289,
|
||||
"range_lower": 3105.7192,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767371848,
|
||||
"time_open": "02.01.26 17:37:28",
|
||||
"hedge_TotPnL": 9.40878,
|
||||
"hedge_fees_paid": 0.538036,
|
||||
"clp_fees": 6.41,
|
||||
"clp_TotPnL": -9.78,
|
||||
"timestamp_close": 1767379841,
|
||||
"time_close": "02.01.26 19:50:41"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5190713,
|
||||
"status": "CLOSED",
|
||||
"target_value": 995.94,
|
||||
"entry_price": 3103.8564,
|
||||
"amount0_initial": 0.1562,
|
||||
"amount1_initial": 511.0986,
|
||||
"liquidity": "610494695009033",
|
||||
"range_upper": 3193.9038,
|
||||
"range_lower": 3010.9236,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767379916,
|
||||
"time_open": "02.01.26 19:51:56",
|
||||
"hedge_TotPnL": 0.4043,
|
||||
"hedge_fees_paid": 0.277557,
|
||||
"clp_fees": 1.83,
|
||||
"clp_TotPnL": 0.77
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5191754,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2986.13,
|
||||
"entry_price": 3095.7973,
|
||||
"amount0_initial": 0.4577,
|
||||
"amount1_initial": 1569.095,
|
||||
"liquidity": "8270626895418999",
|
||||
"range_upper": 3115.0499,
|
||||
"range_lower": 3074.8183,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767430984,
|
||||
"time_open": "03.01.26 10:03:04",
|
||||
"hedge_TotPnL": -10.166,
|
||||
"hedge_fees_paid": 7.014425,
|
||||
"clp_fees": 8.24,
|
||||
"clp_TotPnL": 12.66,
|
||||
"timestamp_close": 1767475160,
|
||||
"time_close": "03.01.26 22:19:20"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5192638,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2994.04,
|
||||
"entry_price": 3125.0335,
|
||||
"amount0_initial": 0.46,
|
||||
"amount1_initial": 1559.9,
|
||||
"range_upper": 3140.069,
|
||||
"range_lower": 3108.8263,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767482267,
|
||||
"time_open": "04.01.26 00:17:47",
|
||||
"clp_fees": 2998.49,
|
||||
"clp_TotPnL": 4.45,
|
||||
"hedge_TotPnL": -9.78996,
|
||||
"hedge_fees_paid": 0.868435,
|
||||
"timestamp_close": 1767485229,
|
||||
"time_close": "04.01.26 01:07:09"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5192707,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2999.37,
|
||||
"entry_price": 3146.0405,
|
||||
"amount0_initial": 0.4767,
|
||||
"amount1_initial": 1500.0,
|
||||
"range_upper": 3158.9651,
|
||||
"range_lower": 3130.6634,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767485383,
|
||||
"time_open": "04.01.26 01:09:43",
|
||||
"clp_fees": 4.35,
|
||||
"clp_TotPnL": -0.63,
|
||||
"timestamp_close": 1767488126,
|
||||
"time_close": "04.01.26 01:55:26"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5192787,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2989.41,
|
||||
"entry_price": 3158.9651,
|
||||
"amount0_initial": 0.4715,
|
||||
"amount1_initial": 1500.0,
|
||||
"range_upper": 3171.6256,
|
||||
"range_lower": 3143.2105,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767488200,
|
||||
"time_open": "04.01.26 01:56:40",
|
||||
"clp_fees": 2.57,
|
||||
"clp_TotPnL": -10.59,
|
||||
"hedge_TotPnL": 3.058716,
|
||||
"hedge_fees_paid": 3.334872,
|
||||
"timestamp_close": 1767491374,
|
||||
"time_close": "04.01.26 02:49:34"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5192872,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2974.66,
|
||||
"entry_price": 3142.8962,
|
||||
"amount0_initial": 0.4692,
|
||||
"amount1_initial": 1500.0,
|
||||
"range_upper": 3155.8079,
|
||||
"range_lower": 3127.5344,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767491446,
|
||||
"time_open": "04.01.26 02:50:46",
|
||||
"clp_fees": 6.14,
|
||||
"clp_TotPnL": 25.34,
|
||||
"hedge_TotPnL": -1.09566,
|
||||
"hedge_fees_paid": 3.319216,
|
||||
"timestamp_close": 1767507012,
|
||||
"time_close": "04.01.26 07:10:12"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5193076,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2927.37,
|
||||
"entry_price": 3155.8079,
|
||||
"amount0_initial": 0.4522,
|
||||
"amount1_initial": 1500.0,
|
||||
"range_upper": 3168.4557,
|
||||
"range_lower": 3140.069,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767507083,
|
||||
"time_open": "04.01.26 07:11:23",
|
||||
"clp_fees": 2.91,
|
||||
"clp_TotPnL": -36.11,
|
||||
"hedge_TotPnL": 0.0,
|
||||
"hedge_fees_paid": 1.655925,
|
||||
"timestamp_close": 1767511297,
|
||||
"time_close": "04.01.26 08:21:37"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5193158,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2970.4,
|
||||
"entry_price": 3139.755,
|
||||
"amount0_initial": 0.477,
|
||||
"amount1_initial": 1500.0,
|
||||
"range_upper": 3152.6538,
|
||||
"range_lower": 3124.4086,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767511369,
|
||||
"time_open": "04.01.26 08:22:49",
|
||||
"clp_fees": 11.2,
|
||||
"clp_TotPnL": 0.27,
|
||||
"hedge_TotPnL": -1.76453,
|
||||
"hedge_fees_paid": 1.292159,
|
||||
"timestamp_close": 1767541575,
|
||||
"time_close": "04.01.26 16:46:15"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5193699,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2985.65,
|
||||
"entry_price": 3124.0962,
|
||||
"amount0_initial": 0.4461,
|
||||
"amount1_initial": 1592.1238,
|
||||
"liquidity": "8231768660184301",
|
||||
"range_upper": 3143.2105,
|
||||
"range_lower": 3102.6152,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767541587,
|
||||
"time_open": "04.01.26 16:46:27",
|
||||
"clp_fees": 5.59,
|
||||
"clp_TotPnL": 9.87,
|
||||
"hedge_TotPnL": -2.74009,
|
||||
"hedge_fees_paid": 3.283603,
|
||||
"timestamp_close": 1767561023,
|
||||
"time_close": "04.01.26 22:10:23"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194064,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2995.07,
|
||||
"entry_price": 3149.8178,
|
||||
"amount0_initial": 0.4308,
|
||||
"amount1_initial": 1638.2123,
|
||||
"liquidity": "8224032596348534",
|
||||
"range_upper": 3168.4557,
|
||||
"range_lower": 3127.5344,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767561335,
|
||||
"time_open": "04.01.26 22:15:35",
|
||||
"hedge_TotPnL": -10.8216,
|
||||
"hedge_fees_paid": 0.744327,
|
||||
"clp_fees": 10.96,
|
||||
"clp_TotPnL": 14.98,
|
||||
"timestamp_close": 1767574703,
|
||||
"time_close": "05.01.26 01:58:23"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194353,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2986.84,
|
||||
"entry_price": 3167.1887,
|
||||
"amount0_initial": 0.4622,
|
||||
"amount1_initial": 1523.0178,
|
||||
"liquidity": "8178792616583388",
|
||||
"range_upper": 3187.5227,
|
||||
"range_lower": 3146.3551,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767575103,
|
||||
"time_open": "05.01.26 02:05:03",
|
||||
"hedge_TotPnL": -11.72105,
|
||||
"hedge_fees_paid": 0.851074,
|
||||
"clp_fees": 1.32,
|
||||
"clp_TotPnL": 6.03,
|
||||
"timestamp_close": 1767575748,
|
||||
"time_close": "05.01.26 02:15:48"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194438,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2994.26,
|
||||
"entry_price": 3182.7452,
|
||||
"amount0_initial": 0.4695,
|
||||
"amount1_initial": 1499.9963,
|
||||
"liquidity": "8179052347416944",
|
||||
"range_upper": 3203.4994,
|
||||
"range_lower": 3162.1255,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767576062,
|
||||
"time_open": "05.01.26 02:21:02",
|
||||
"hedge_TotPnL": -13.130776,
|
||||
"hedge_fees_paid": 0.866544,
|
||||
"clp_fees": 2.0,
|
||||
"clp_TotPnL": 6.88,
|
||||
"timestamp_close": 1767577312,
|
||||
"time_close": "05.01.26 02:41:52"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194549,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2993.07,
|
||||
"entry_price": 3209.2706,
|
||||
"amount0_initial": 0.4437,
|
||||
"amount1_initial": 1569.0894,
|
||||
"liquidity": "8141974814554655",
|
||||
"range_upper": 3229.2289,
|
||||
"range_lower": 3187.5227,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767577667,
|
||||
"time_open": "05.01.26 02:47:47",
|
||||
"hedge_TotPnL": 9.143032,
|
||||
"hedge_fees_paid": 0.817659,
|
||||
"clp_fees": 1.68,
|
||||
"clp_TotPnL": -14.2,
|
||||
"timestamp_close": 1767578919,
|
||||
"time_close": "05.01.26 03:08:39"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194636,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2999.61,
|
||||
"entry_price": 3191.6689,
|
||||
"amount0_initial": 0.4122,
|
||||
"amount1_initial": 1683.9338,
|
||||
"liquidity": "8182395676682951",
|
||||
"range_upper": 3209.9125,
|
||||
"range_lower": 3168.4557,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767579235,
|
||||
"time_open": "05.01.26 03:13:55",
|
||||
"hedge_TotPnL": 12.194039,
|
||||
"hedge_fees_paid": 0.752756,
|
||||
"clp_fees": 4.31,
|
||||
"clp_TotPnL": -14.67,
|
||||
"timestamp_close": 1767587437,
|
||||
"time_close": "05.01.26 05:30:37"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5194825,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2968.38,
|
||||
"entry_price": 3162.4417,
|
||||
"amount0_initial": 0.4206,
|
||||
"amount1_initial": 1638.1774,
|
||||
"liquidity": "8134444867821814",
|
||||
"range_upper": 3181.1543,
|
||||
"range_lower": 3140.069,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"timestamp_open": 1767587750,
|
||||
"time_open": "05.01.26 05:35:50",
|
||||
"hedge_TotPnL": -0.1704,
|
||||
"hedge_fees_paid": 1.553601,
|
||||
"clp_fees": 15.16,
|
||||
"clp_TotPnL": 10.5,
|
||||
"combined_TotPnL": 13.22,
|
||||
"hedge_HL_cost_est": 0.77,
|
||||
"hedge_pnl_unrealized": 4.3,
|
||||
"last_sync_hl": 1767622049,
|
||||
"timestamp_close": 1767622037,
|
||||
"time_close": "05.01.26 15:07:17"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5195733,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2984.65,
|
||||
"entry_price": 3150.7629,
|
||||
"amount0_initial": 0.4341,
|
||||
"amount1_initial": 1617.0073,
|
||||
"liquidity": "4637302533941787",
|
||||
"range_upper": 3184.3369,
|
||||
"range_lower": 3111.9366,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "AUTO",
|
||||
"range_width_initial": 0.01160980429312654,
|
||||
"timestamp_open": 1767622560,
|
||||
"time_open": "05.01.26 15:16:00",
|
||||
"hedge_TotPnL": -3.139154,
|
||||
"hedge_fees_paid": 0.793472,
|
||||
"combined_TotPnL": 1.6,
|
||||
"hedge_HL_cost_est": 0.79,
|
||||
"hedge_pnl_unrealized": -2.92,
|
||||
"last_sync_hl": 1767624126,
|
||||
"clp_fees": 2.68,
|
||||
"clp_TotPnL": 5.4,
|
||||
"timestamp_close": 1767624113,
|
||||
"time_close": "05.01.26 15:41:53"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5195860,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2976.38,
|
||||
"entry_price": 3157.3861,
|
||||
"amount0_initial": 0.438,
|
||||
"amount1_initial": 1593.5552,
|
||||
"liquidity": "6634808665424129",
|
||||
"range_upper": 3181.1543,
|
||||
"range_lower": 3130.6634,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "AUTO",
|
||||
"range_width_initial": 0.008451255668317245,
|
||||
"timestamp_open": 1767624512,
|
||||
"time_open": "05.01.26 15:48:32",
|
||||
"hedge_TotPnL": -4.258952,
|
||||
"hedge_fees_paid": 0.804724,
|
||||
"combined_TotPnL": 0.01,
|
||||
"hedge_HL_cost_est": 0.81,
|
||||
"hedge_pnl_unrealized": -3.03,
|
||||
"last_sync_hl": 1767624869,
|
||||
"clp_fees": 1.25,
|
||||
"clp_TotPnL": 4.26,
|
||||
"timestamp_close": 1767624855,
|
||||
"time_close": "05.01.26 15:54:15"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5195910,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2957.51,
|
||||
"entry_price": 3166.872,
|
||||
"amount0_initial": 0.4342,
|
||||
"amount1_initial": 1582.386,
|
||||
"liquidity": "8772781500746006",
|
||||
"range_upper": 3184.3369,
|
||||
"range_lower": 3146.3551,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "AUTO",
|
||||
"range_width_initial": 0.00610577067463115,
|
||||
"timestamp_open": 1767625173,
|
||||
"time_open": "05.01.26 15:59:33",
|
||||
"hedge_TotPnL": -6.49276,
|
||||
"hedge_fees_paid": 0.782891,
|
||||
"combined_TotPnL": 2.73,
|
||||
"hedge_HL_cost_est": 0.78,
|
||||
"hedge_pnl_unrealized": -5.77,
|
||||
"last_sync_hl": 1767627649,
|
||||
"clp_fees": 5.74,
|
||||
"clp_TotPnL": 9.23,
|
||||
"timestamp_close": 1767627635,
|
||||
"time_close": "05.01.26 16:40:35"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5196059,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2984.89,
|
||||
"entry_price": 3194.862,
|
||||
"amount0_initial": 0.4148,
|
||||
"amount1_initial": 1659.6726,
|
||||
"liquidity": "7054813881893230",
|
||||
"range_upper": 3216.3384,
|
||||
"range_lower": 3168.4557,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "FIXED",
|
||||
"range_width_initial": 0.0075,
|
||||
"timestamp_open": 1767627994,
|
||||
"time_open": "05.01.26 16:46:34",
|
||||
"hedge_TotPnL": 9.768993,
|
||||
"hedge_fees_paid": 0.761925,
|
||||
"combined_TotPnL": -3.68,
|
||||
"hedge_HL_cost_est": 0.76,
|
||||
"hedge_pnl_unrealized": 13.07,
|
||||
"last_sync_hl": 1767633796,
|
||||
"clp_fees": 6.47,
|
||||
"clp_TotPnL": -13.17,
|
||||
"timestamp_close": 1767633781,
|
||||
"time_close": "05.01.26 18:23:01"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5196356,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2970.03,
|
||||
"entry_price": 3162.7579,
|
||||
"amount0_initial": 0.4513,
|
||||
"amount1_initial": 1542.7246,
|
||||
"liquidity": "7558082601107499",
|
||||
"range_upper": 3184.3369,
|
||||
"range_lower": 3140.069,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "AUTO",
|
||||
"range_width_initial": 0.007188283945973874,
|
||||
"timestamp_open": 1767634095,
|
||||
"time_open": "05.01.26 18:28:15",
|
||||
"hedge_TotPnL": -5.78208,
|
||||
"hedge_fees_paid": 0.833176,
|
||||
"combined_TotPnL": -1.93,
|
||||
"hedge_HL_cost_est": 0.83,
|
||||
"hedge_pnl_unrealized": -5.65,
|
||||
"last_sync_hl": 1767634756,
|
||||
"clp_fees": 0.53,
|
||||
"clp_TotPnL": 4.88,
|
||||
"timestamp_close": 1767634742,
|
||||
"time_close": "05.01.26 18:39:02"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5196394,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2997.55,
|
||||
"entry_price": 3187.5227,
|
||||
"amount0_initial": 0.4385,
|
||||
"amount1_initial": 1599.8109,
|
||||
"liquidity": "7092801036999878",
|
||||
"range_upper": 3209.9125,
|
||||
"range_lower": 3162.1255,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "FIXED",
|
||||
"range_width_initial": 0.0075,
|
||||
"timestamp_open": 1767635055,
|
||||
"time_open": "05.01.26 18:44:15",
|
||||
"hedge_TotPnL": -10.899792,
|
||||
"hedge_fees_paid": 0.809798,
|
||||
"combined_TotPnL": 5.4,
|
||||
"hedge_HL_cost_est": 0.61,
|
||||
"hedge_pnl_unrealized": 0.0,
|
||||
"last_sync_hl": 1767635701,
|
||||
"clp_fees": 1.13,
|
||||
"clp_TotPnL": 6.03,
|
||||
"timestamp_close": 1767635701,
|
||||
"time_close": "05.01.26 18:55:01"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5196471,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2980.42,
|
||||
"entry_price": 3209.5915,
|
||||
"amount0_initial": 0.4364,
|
||||
"amount1_initial": 1579.8119,
|
||||
"liquidity": "7027957507483544",
|
||||
"range_upper": 3232.4596,
|
||||
"range_lower": 3184.3369,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "FIXED",
|
||||
"range_width_initial": 0.0075,
|
||||
"timestamp_open": 1767636146,
|
||||
"time_open": "05.01.26 19:02:26",
|
||||
"hedge_TotPnL": -13.87062,
|
||||
"hedge_fees_paid": 0.817984,
|
||||
"combined_TotPnL": -2.61,
|
||||
"hedge_HL_cost_est": 1.43,
|
||||
"hedge_pnl_unrealized": 0.0,
|
||||
"last_sync_hl": 1767643534,
|
||||
"clp_fees": 7.68,
|
||||
"clp_TotPnL": 12.7,
|
||||
"timestamp_close": 1767643533,
|
||||
"time_close": "05.01.26 21:05:33"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5196956,
|
||||
"status": "CLOSED",
|
||||
"target_value": 2977.3,
|
||||
"entry_price": 3232.7828,
|
||||
"amount0_initial": 0.4269,
|
||||
"amount1_initial": 1597.0858,
|
||||
"liquidity": "6995425133879491",
|
||||
"range_upper": 3255.165,
|
||||
"range_lower": 3206.7043,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "FIXED",
|
||||
"range_width_initial": 0.0075,
|
||||
"timestamp_open": 1767643849,
|
||||
"time_open": "05.01.26 21:10:49",
|
||||
"hedge_TotPnL": -1.356196,
|
||||
"hedge_fees_paid": 1.243909,
|
||||
"combined_TotPnL": -3.62,
|
||||
"hedge_HL_cost_est": 1.58,
|
||||
"hedge_pnl_unrealized": 9.64,
|
||||
"last_sync_hl": 1767660041,
|
||||
"clp_fees": 10.88,
|
||||
"clp_TotPnL": -7.56,
|
||||
"timestamp_close": 1767660028,
|
||||
"time_close": "06.01.26 01:40:28"
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5198324,
|
||||
"status": "OPEN",
|
||||
"target_value": 2981.26,
|
||||
"entry_price": 3217.6251,
|
||||
"amount0_initial": 0.4045,
|
||||
"amount1_initial": 1679.6194,
|
||||
"liquidity": "7021319704792719",
|
||||
"range_upper": 3238.9306,
|
||||
"range_lower": 3190.7116,
|
||||
"token0_decimals": 18,
|
||||
"token1_decimals": 6,
|
||||
"range_mode": "AUTO",
|
||||
"range_width_initial": 0.007592620856214823,
|
||||
"timestamp_open": 1767688223,
|
||||
"time_open": "06.01.26 09:30:23",
|
||||
"hedge_TotPnL": 0.0,
|
||||
"hedge_fees_paid": 0.0,
|
||||
"combined_TotPnL": -0.47,
|
||||
"hedge_HL_cost_est": 0.57,
|
||||
"hedge_pnl_unrealized": 0.72,
|
||||
"last_sync_hl": 1767689255,
|
||||
"clp_fees": 0.12,
|
||||
"clp_TotPnL": 0.64
|
||||
}
|
||||
]
|
||||
82
florida/clp_abis.py
Normal file
82
florida/clp_abis.py
Normal 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"}
|
||||
]
|
||||
''')
|
||||
@ -8,7 +8,9 @@ STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
|
||||
|
||||
# --- DEFAULT STRATEGY ---
|
||||
DEFAULT_STRATEGY = {
|
||||
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
|
||||
"MONITOR_INTERVAL_SECONDS": 300, # Manager loop & sync interval
|
||||
"LOG_INTERVAL_SECONDS": 300, # Hedger console logging interval
|
||||
"RANGE_MODE": "AUTO", # Options: "AUTO" (BB-based), "FIXED" (RANGE_WIDTH_PCT)
|
||||
"CLOSE_POSITION_ENABLED": True, # Allow the bot to automatically close out-of-range positions
|
||||
"OPEN_POSITION_ENABLED": True, # Allow the bot to automatically open new positions
|
||||
"REBALANCE_ON_CLOSE_BELOW_RANGE": True, # Strategy flag for specific closing behavior
|
||||
@ -19,15 +21,16 @@ DEFAULT_STRATEGY = {
|
||||
"VALUE_REFERENCE": "USD", # Base currency for all calculations
|
||||
|
||||
# Range Settings
|
||||
"RANGE_WIDTH_PCT": Decimal("0.01"), # LP width (e.g. 0.05 = +/- 5% from current price)
|
||||
"SLIPPAGE_TOLERANCE": Decimal("0.02"), # Max allowed slippage for swaps and minting
|
||||
"RANGE_WIDTH_PCT": Decimal("0.03"), # LP width (e.g. 0.05 = +/- 5% from current price)
|
||||
"SLIPPAGE_TOLERANCE": Decimal("0.03"), # Max allowed slippage for swaps and minting
|
||||
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
|
||||
|
||||
# Hedging Settings
|
||||
"HEDGE_STRATEGY": "ASYMMETRIC", # Options: "STANDARD" (Full Range Hedge), "ASYMMETRIC" (Edge-Only Reduction), "FIXED" (Initial Delta)
|
||||
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
|
||||
|
||||
# Unified Hedger Settings
|
||||
"CHECK_INTERVAL": 1, # Loop speed for the hedger (seconds)
|
||||
|
||||
"LEVERAGE": 5, # Leverage to use on Hyperliquid
|
||||
"ZONE_BOTTOM_HEDGE_LIMIT": Decimal("1.0"), # Multiplier limit at the bottom of the range
|
||||
"ZONE_CLOSE_START": Decimal("10.0"), # Distance (pct) from edge to start closing logic
|
||||
@ -45,7 +48,7 @@ DEFAULT_STRATEGY = {
|
||||
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
|
||||
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
|
||||
"ENABLE_EDGE_CLEANUP": True, # Force rebalances when price is at range boundaries
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.02"), # % of range width used for edge detection
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.03"), # % of range width used for edge detection
|
||||
"MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
|
||||
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
|
||||
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
|
||||
@ -72,6 +75,9 @@ CLP_PROFILES = {
|
||||
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
||||
"POOL_FEE": 500,
|
||||
"TARGET_INVESTMENT_AMOUNT": 3000,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
"RANGE_WIDTH_PCT": Decimal("0.0075"),
|
||||
},
|
||||
"UNISWAP_wide": {
|
||||
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
|
||||
@ -98,6 +104,7 @@ CLP_PROFILES = {
|
||||
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
|
||||
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
"POOL_FEE": 100,
|
||||
"EDGE_CLEANUP_MARGIN_PCT": Decimal("0.1875"), # 0.1875 only for asymmetric shedge % of range width used for edge detection
|
||||
"RANGE_WIDTH_PCT": Decimal("0.004"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 1000,
|
||||
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
|
||||
@ -123,6 +130,34 @@ CLP_PROFILES = {
|
||||
"TARGET_INVESTMENT_AMOUNT": 200,
|
||||
"VALUE_REFERENCE": "USD",
|
||||
"RANGE_WIDTH_PCT": Decimal("0.10")
|
||||
},
|
||||
"AERODROME_BASE_CL": {
|
||||
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC",
|
||||
"COIN_SYMBOL": "ETH",
|
||||
"RPC_ENV_VAR": "BASE_RPC_URL",
|
||||
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
|
||||
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
|
||||
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
|
||||
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
|
||||
"POOL_FEE": 100, # TickSpacing 100 pool (0xb2cc...)
|
||||
"RANGE_WIDTH_PCT": Decimal("0.075"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 200,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
},
|
||||
"AERODROME_WETH-USDC_008": {
|
||||
"NAME": "Aerodrome SlipStream (Base) - WETH/USDC Stable",
|
||||
"COIN_SYMBOL": "ETH",
|
||||
"RPC_ENV_VAR": "BASE_RPC_URL",
|
||||
"NPM_ADDRESS": "0x827922686190790b37229fd06084350E74485b72",
|
||||
"ROUTER_ADDRESS": "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
|
||||
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH
|
||||
"TOKEN_B_ADDRESS": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
|
||||
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
|
||||
"POOL_FEE": 1, # TickSpacing 1 pool (0xdbc6...)
|
||||
"RANGE_WIDTH_PCT": Decimal("0.05"),
|
||||
"TARGET_INVESTMENT_AMOUNT": 200,
|
||||
"HEDGE_STRATEGY": "FIXED",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,20 +14,15 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(current_dir)
|
||||
sys.path.append(project_root)
|
||||
|
||||
# Import local modules
|
||||
try:
|
||||
from logging_utils import setup_logging
|
||||
except ImportError:
|
||||
setup_logging = None
|
||||
# Ensure root logger is clean if we can't use setup_logging
|
||||
logging.getLogger().handlers.clear()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# Ensure root logger is clean
|
||||
logging.getLogger().handlers.clear()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
from eth_account import Account
|
||||
from hyperliquid.exchange import Exchange
|
||||
from hyperliquid.info import Info
|
||||
from hyperliquid.utils import constants
|
||||
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY
|
||||
from clp_config import CLP_PROFILES, DEFAULT_STRATEGY, TARGET_DEX
|
||||
|
||||
# Load environment variables
|
||||
dotenv_path = os.path.join(current_dir, '.env')
|
||||
@ -213,7 +208,7 @@ class HyperliquidStrategy:
|
||||
else: # >=5% range
|
||||
return Decimal("0.075") # Standard for wide ranges
|
||||
|
||||
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict:
|
||||
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal, strategy_type: str = "ASYMMETRIC") -> Dict:
|
||||
# Note: current_short_size here is virtual (just for this specific strategy),
|
||||
# but the unified hedger will use the 'target_short' output primarily.
|
||||
|
||||
@ -221,6 +216,8 @@ class HyperliquidStrategy:
|
||||
|
||||
# --- ASYMMETRIC COMPENSATION ---
|
||||
adj_pct = Decimal("0.0")
|
||||
|
||||
if strategy_type == "ASYMMETRIC":
|
||||
range_width = self.high_range - self.low_range
|
||||
|
||||
if range_width > 0:
|
||||
@ -231,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)
|
||||
@ -273,12 +284,23 @@ class UnifiedHedger:
|
||||
|
||||
# Market Data Cache
|
||||
self.last_prices = {}
|
||||
self.price_history = {} # Symbol -> List[Decimal]
|
||||
self.price_history = {} # Symbol -> List[Decimal] (Fast: 1s samples)
|
||||
self.last_trade_times = {} # Symbol -> timestamp
|
||||
self.last_idle_log_times = {} # Symbol -> timestamp
|
||||
|
||||
# Shadow Orders (Global List)
|
||||
self.shadow_orders = []
|
||||
|
||||
# State: Emergency Close Hysteresis
|
||||
# Map: (file_path, token_id) -> bool
|
||||
self.emergency_close_active = {}
|
||||
|
||||
# Map: (file_path, token_id) -> Decimal (Locked hedge size)
|
||||
self.custom_fixed_targets = {}
|
||||
|
||||
# Map: (file_path, token_id) -> Decimal (Price when hedge leg opened)
|
||||
self.hedge_entry_prices = {}
|
||||
|
||||
self.startup_time = time.time()
|
||||
|
||||
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
|
||||
@ -286,6 +308,7 @@ class UnifiedHedger:
|
||||
|
||||
def _init_coin_configs(self):
|
||||
"""Pre-load configuration for known coins from CLP_PROFILES."""
|
||||
# 1. Load all profiles (order depends on dict iteration)
|
||||
for profile_key, profile_data in CLP_PROFILES.items():
|
||||
symbol = profile_data.get("COIN_SYMBOL")
|
||||
if symbol:
|
||||
@ -297,6 +320,18 @@ class UnifiedHedger:
|
||||
# Update with Profile Specifics
|
||||
self.coin_configs[symbol].update(profile_data)
|
||||
|
||||
# 2. Force overwrite with TARGET_DEX profile to ensure precedence
|
||||
target_profile = CLP_PROFILES.get(TARGET_DEX)
|
||||
if target_profile:
|
||||
symbol = target_profile.get("COIN_SYMBOL")
|
||||
if symbol:
|
||||
if symbol not in self.coin_configs:
|
||||
self.coin_configs[symbol] = DEFAULT_STRATEGY.copy()
|
||||
self.coin_configs[symbol]["sz_decimals"] = 4
|
||||
|
||||
logger.info(f"Overwriting config for {symbol} using TARGET_DEX: {TARGET_DEX}")
|
||||
self.coin_configs[symbol].update(target_profile)
|
||||
|
||||
def _get_sz_decimals(self, coin: str) -> int:
|
||||
try:
|
||||
meta = self.info.meta()
|
||||
@ -427,6 +462,7 @@ class UnifiedHedger:
|
||||
self.strategy_states[key]['pnl'] = to_decimal(entry.get('hedge_pnl_realized', 0))
|
||||
self.strategy_states[key]['fees'] = to_decimal(entry.get('hedge_fees_paid', 0))
|
||||
self.strategy_states[key]['status'] = entry.get('status', 'OPEN')
|
||||
self.strategy_states[key]['clp_fees'] = to_decimal(entry.get('clp_fees', 0))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading {filename}: {e}. Skipping updates.")
|
||||
@ -478,12 +514,23 @@ class UnifiedHedger:
|
||||
"start_time": start_time_ms,
|
||||
"pnl": to_decimal(position_data.get('hedge_pnl_realized', 0)),
|
||||
"fees": to_decimal(position_data.get('hedge_fees_paid', 0)),
|
||||
"clp_fees": to_decimal(position_data.get('clp_fees', 0)),
|
||||
"hedge_TotPnL": to_decimal(position_data.get('hedge_TotPnL', 0)), # NEW: Total Closed PnL
|
||||
"entry_price": entry_price, # Store for fishing logic
|
||||
"status": position_data.get('status', 'OPEN')
|
||||
}
|
||||
|
||||
# Initial hedge entry price is the CLP entry price
|
||||
self.hedge_entry_prices[key] = entry_price
|
||||
|
||||
logger.info(f"[STRAT] Init {key[1]} ({coin_symbol}) | Range: {lower}-{upper}")
|
||||
|
||||
# Ensure JSON has these fields initialized
|
||||
update_position_stats(key[0], key[1], {
|
||||
"hedge_TotPnL": float(self.strategy_states[key]['hedge_TotPnL']),
|
||||
"hedge_fees_paid": float(self.strategy_states[key]['fees'])
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to init strategy {key[1]}: {e}")
|
||||
|
||||
@ -652,7 +699,7 @@ class UnifiedHedger:
|
||||
price = to_decimal(mids[coin])
|
||||
self.last_prices[coin] = price
|
||||
|
||||
# Update Price History
|
||||
# Update Price History (Fast)
|
||||
if coin not in self.price_history: self.price_history[coin] = []
|
||||
self.price_history[coin].append(price)
|
||||
if len(self.price_history[coin]) > 300: self.price_history[coin].pop(0)
|
||||
@ -663,17 +710,58 @@ class UnifiedHedger:
|
||||
if coin not in self.last_prices: continue
|
||||
price = self.last_prices[coin]
|
||||
|
||||
# Get Config & Strategy Type
|
||||
config = self.coin_configs.get(coin, {})
|
||||
strategy_type = config.get("HEDGE_STRATEGY", "ASYMMETRIC")
|
||||
|
||||
# Calc Logic
|
||||
calc = strat.calculate_rebalance(price, Decimal("0"))
|
||||
calc = strat.calculate_rebalance(price, Decimal("0"), strategy_type)
|
||||
|
||||
if coin not in aggregates:
|
||||
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
|
||||
aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}
|
||||
|
||||
if status == 'CLOSING':
|
||||
# If Closing, we want target to be 0 for this strategy
|
||||
logger.info(f"[STRAT] {key[1]} is CLOSING -> Force Target 0")
|
||||
# --- EMERGENCY UPPER EDGE CLOSING (HYSTERESIS) ---
|
||||
# Logic: If price hits Top, close hedge. Do NOT re-open until price drops back to 75% of Range (FIXED) or Buffer (Others).
|
||||
|
||||
is_active_hysteresis = self.emergency_close_active.get(key, False)
|
||||
|
||||
if is_active_hysteresis:
|
||||
# CHECK RESET CONDITION
|
||||
if strategy_type == "FIXED":
|
||||
# Reset at 75% of range (from Bottom)
|
||||
range_width = strat.high_range - strat.low_range
|
||||
reset_threshold = strat.low_range + (range_width * Decimal("0.75"))
|
||||
else:
|
||||
reset_threshold = strat.high_range * Decimal("0.999")
|
||||
|
||||
if price < reset_threshold:
|
||||
logger.info(f"[STRAT] {key[1]} Price reset ({price:.2f} < {reset_threshold:.2f}). Resuming hedge.")
|
||||
self.emergency_close_active[key] = False
|
||||
is_active_hysteresis = False
|
||||
|
||||
# Capture NEW Dynamic Fixed Target and Entry Price
|
||||
if strategy_type == "FIXED":
|
||||
dynamic_delta = strat.get_pool_delta(price)
|
||||
self.custom_fixed_targets[key] = dynamic_delta
|
||||
self.hedge_entry_prices[key] = price
|
||||
logger.info(f"[STRAT] {key[1]} FIXED target reset to Dynamic Delta: {dynamic_delta:.4f} @ {price:.2f}")
|
||||
|
||||
if not is_active_hysteresis:
|
||||
# CHECK TRIGGER CONDITION
|
||||
if price >= strat.high_range:
|
||||
logger.warning(f"[STRAT] {key[1]} above High Range ({price:.2f} >= {strat.high_range:.2f}). Emergency closing hedge.")
|
||||
self.emergency_close_active[key] = True
|
||||
is_active_hysteresis = True
|
||||
# Reset entry price when closed
|
||||
self.hedge_entry_prices[key] = Decimal("0")
|
||||
|
||||
if status == 'CLOSING' or is_active_hysteresis:
|
||||
# If Closing OR Hysteresis Active, target is 0
|
||||
aggregates[coin]['is_closing'] = True
|
||||
# Do not add to target_short
|
||||
else:
|
||||
# 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']
|
||||
|
||||
@ -693,6 +781,8 @@ class UnifiedHedger:
|
||||
|
||||
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
|
||||
aggregates[coin]['is_at_edge'] = True
|
||||
if dist_bottom_pct < safety_margin_pct:
|
||||
aggregates[coin]['is_at_bottom_edge'] = True
|
||||
|
||||
# Check Shadow Orders (Pre-Execution)
|
||||
self.check_shadow_orders(l2_snapshots)
|
||||
@ -706,255 +796,256 @@ class UnifiedHedger:
|
||||
for coin in coins_to_process:
|
||||
data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False})
|
||||
|
||||
price = self.last_prices.get(coin, Decimal("0")) # FIX: Explicitly get price for this coin
|
||||
price = self.last_prices.get(coin, Decimal("0"))
|
||||
if price == 0: continue
|
||||
|
||||
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short)
|
||||
target_position = -target_short_abs # We want to be Short, so negative size
|
||||
|
||||
target_short_abs = data['target_short']
|
||||
target_position = -target_short_abs
|
||||
current_pos = current_positions.get(coin, Decimal("0"))
|
||||
|
||||
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
|
||||
diff = target_position - current_pos
|
||||
diff_abs = abs(diff)
|
||||
|
||||
# Thresholds
|
||||
config = self.coin_configs.get(coin, {})
|
||||
min_thresh = config.get("min_threshold", Decimal("0.008"))
|
||||
|
||||
# Volatility Multiplier
|
||||
min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
|
||||
vol_pct = self.calculate_volatility(coin)
|
||||
base_vol = Decimal("0.0005")
|
||||
vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0")
|
||||
|
||||
base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
|
||||
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
|
||||
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
|
||||
|
||||
# FORCE EDGE CLEANUP
|
||||
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True)
|
||||
if data['is_at_edge'] and enable_edge_cleanup:
|
||||
if dynamic_thresh > min_thresh:
|
||||
# logger.info(f"[EDGE] {coin} forced to min threshold.")
|
||||
dynamic_thresh = min_thresh
|
||||
if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
|
||||
if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
|
||||
|
||||
# Check Trigger
|
||||
action_needed = diff_abs > dynamic_thresh
|
||||
|
||||
# Determine Intent (Moved UP for Order Logic)
|
||||
is_buy_bool = diff > 0
|
||||
side_str = "BUY" if is_buy_bool else "SELL"
|
||||
|
||||
# Manage Existing Orders
|
||||
existing_orders = orders_map.get(coin, [])
|
||||
force_taker_retry = False
|
||||
|
||||
# Fishing Config
|
||||
enable_fishing = config.get("ENABLE_FISHING", False)
|
||||
fishing_timeout = config.get("FISHING_TIMEOUT_FALLBACK", 30)
|
||||
|
||||
# Check Existing Orders for compatibility
|
||||
order_matched = False
|
||||
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
|
||||
|
||||
for o in existing_orders:
|
||||
o_oid = o['oid']
|
||||
o_price = to_decimal(o['limitPx'])
|
||||
o_side = o['side'] # 'B' or 'A'
|
||||
o_side = o['side']
|
||||
o_timestamp = o.get('timestamp', int(time.time()*1000))
|
||||
|
||||
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
|
||||
|
||||
# Price Check (within buffer)
|
||||
dist_pct = abs(price - o_price) / price
|
||||
|
||||
# Maker Timeout Check (General)
|
||||
maker_timeout = config.get("MAKER_ORDER_TIMEOUT", 300)
|
||||
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
|
||||
|
||||
if is_same_side and order_age_sec > maker_timeout:
|
||||
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.")
|
||||
if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
|
||||
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
|
||||
self.cancel_order(coin, o_oid)
|
||||
continue
|
||||
|
||||
# Fishing Timeout Check
|
||||
if enable_fishing and is_same_side and order_age_sec > fishing_timeout:
|
||||
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
|
||||
if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
|
||||
logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
|
||||
self.cancel_order(coin, o_oid)
|
||||
force_taker_retry = True
|
||||
continue # Do not mark matched, let it flow to execution
|
||||
continue
|
||||
|
||||
if is_same_side and dist_pct < price_buffer_pct:
|
||||
if is_same_side and (abs(price - o_price) / price) < price_buffer_pct:
|
||||
order_matched = True
|
||||
if int(time.time()) % 10 == 0:
|
||||
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%) | Age: {order_age_sec:.1f}s")
|
||||
if int(time.time()) % 15 == 0:
|
||||
logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
|
||||
break
|
||||
else:
|
||||
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
|
||||
self.cancel_order(coin, o_oid)
|
||||
|
||||
# --- EXECUTION LOGIC ---
|
||||
if not order_matched:
|
||||
if action_needed or force_taker_retry:
|
||||
# Determine Urgency / Bypass Cooldown
|
||||
bypass_cooldown = False
|
||||
force_maker = False
|
||||
|
||||
# 0. Forced Taker Retry (Fishing Timeout)
|
||||
if force_taker_retry:
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker")
|
||||
|
||||
# 1. Urgent Closing -> Taker
|
||||
elif data.get('is_closing', False):
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit")
|
||||
|
||||
# 2. Ghost/Cleanup -> Maker
|
||||
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
|
||||
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce")
|
||||
else:
|
||||
logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)")
|
||||
continue # Skip execution for this coin
|
||||
if time.time() - self.startup_time > 5: force_maker = True
|
||||
else: continue # Skip startup ghost positions
|
||||
|
||||
# Large Hedge Check (Only Force Taker if AT EDGE)
|
||||
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
|
||||
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
|
||||
# Prevent IOC for BUYs at bottom edge
|
||||
if not (is_buy_bool and data.get('is_at_bottom_edge', False)):
|
||||
bypass_cooldown = True
|
||||
logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
|
||||
elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
|
||||
# Large hedge but safe zone -> Maker is fine, but maybe log it
|
||||
logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.")
|
||||
|
||||
# --- 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)
|
||||
|
||||
min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60)
|
||||
can_trade = False
|
||||
if bypass_cooldown:
|
||||
can_trade = True
|
||||
elif time.time() - last_trade > min_time_trade:
|
||||
can_trade = True
|
||||
|
||||
if can_trade:
|
||||
# Get Orderbook for Price
|
||||
if coin not in l2_snapshots:
|
||||
l2_snapshots[coin] = self.info.l2_snapshot(coin)
|
||||
|
||||
if bypass_cooldown or (time.time() - last_trade > min_time):
|
||||
if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
|
||||
levels = l2_snapshots[coin]['levels']
|
||||
if not levels[0] or not levels[1]: continue
|
||||
|
||||
bid = to_decimal(levels[0][0]['px'])
|
||||
ask = to_decimal(levels[1][0]['px'])
|
||||
|
||||
# Price logic
|
||||
create_shadow = False
|
||||
|
||||
# Decide Order Type: Taker (Ioc) or Maker (Alo)
|
||||
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo)
|
||||
|
||||
# Logic:
|
||||
# If Force Maker -> Alo
|
||||
# Else if Urgent -> Ioc
|
||||
# Else if Enable Fishing -> Alo
|
||||
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
|
||||
|
||||
# Let's clarify:
|
||||
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo.
|
||||
# New logic:
|
||||
# If bypass_cooldown -> Ioc
|
||||
# Else -> Alo (Fishing)
|
||||
|
||||
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"
|
||||
create_shadow = True
|
||||
else:
|
||||
# Fishing / Standard Maker
|
||||
exec_price = bid if is_buy_bool else ask
|
||||
order_type = "Alo"
|
||||
|
||||
logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f} | Type: {order_type}")
|
||||
|
||||
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()
|
||||
|
||||
# Shadow Order
|
||||
if create_shadow:
|
||||
if order_type == "Ioc":
|
||||
shadow_price = bid if is_buy_bool else ask
|
||||
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
|
||||
self.shadow_orders.append({
|
||||
'coin': coin,
|
||||
'side': side_str,
|
||||
'price': shadow_price,
|
||||
'expires_at': time.time() + shadow_timeout
|
||||
})
|
||||
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
|
||||
self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
|
||||
|
||||
# UPDATED: Sleep for API Lag (Phase 5.1)
|
||||
logger.info("Sleeping 10s to allow position update...")
|
||||
logger.info("Sleeping 10s for position update...")
|
||||
time.sleep(10)
|
||||
|
||||
# --- UPDATE CLOSED PnL FROM API ---
|
||||
self._update_closed_pnl(coin)
|
||||
else:
|
||||
# Cooldown log
|
||||
pass
|
||||
# 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:
|
||||
# Action NOT needed
|
||||
# Cleanup any dangling orders
|
||||
if existing_orders:
|
||||
for o in existing_orders:
|
||||
logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})")
|
||||
self.cancel_order(coin, o['oid'])
|
||||
hedge_pnl_curr = Decimal("0")
|
||||
|
||||
# --- IDLE LOGGING (Restored Format) ---
|
||||
# Calculate aggregate Gamma to estimate triggers
|
||||
# Gamma = 0.5 * Sum(L) * P^-1.5
|
||||
# We need Sum(L) for this coin.
|
||||
total_L = Decimal("0")
|
||||
# We need to re-iterate or cache L.
|
||||
# Simpler: Just re-sum L from active strats for this coin.
|
||||
for key, strat in self.strategies.items():
|
||||
if self.strategy_states[key]['coin'] == coin:
|
||||
total_L += strat.L
|
||||
fee_close_curr = (target_size * price) * Decimal("0.000432")
|
||||
uni_fees = to_decimal(self.strategy_states[k_strat].get('clp_fees', 0))
|
||||
|
||||
if total_L > 0 and price > 0:
|
||||
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5")))
|
||||
if gamma > 0:
|
||||
# Equilibrium Price (Diff = 0)
|
||||
p_mid = price + (diff / gamma)
|
||||
# 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))
|
||||
|
||||
# Triggers
|
||||
p_buy = price + (dynamic_thresh + diff) / gamma
|
||||
p_sell = price - (dynamic_thresh - diff) / gamma
|
||||
# Combined TotPnL = CLP_Unrealized + Hedge_Unrealized + Hedge_Realized - Hedge_Fees + CLP_Fees - Est_Close_Fee
|
||||
tot_curr = (clp_curr_val - strat_inst.target_value) + hedge_pnl_curr + realized_pnl - realized_fees - fee_close_curr + uni_fees
|
||||
|
||||
if int(time.time()) % 30 == 0:
|
||||
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 ""
|
||||
adj_val = data.get('adj_pct', Decimal("0")) * 100
|
||||
|
||||
# PnL Calc
|
||||
unrealized = current_pnls.get(coin, Decimal("0"))
|
||||
closed_pnl_total = Decimal("0")
|
||||
fees_total = Decimal("0")
|
||||
for k, s_state in self.strategy_states.items():
|
||||
if s_state['coin'] == coin:
|
||||
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
|
||||
fees_total += s_state.get('fees', Decimal("0"))
|
||||
closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
|
||||
fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
|
||||
total_pnl = (closed_pnl - fees) + unrealized
|
||||
|
||||
total_pnl = (closed_pnl_total - fees_total) + unrealized
|
||||
# Log individual strategy PnL
|
||||
if strategy_type == "FIXED":
|
||||
for k_strat, strat_inst in self.strategies.items():
|
||||
if self.strategy_states[k_strat]['coin'] != coin: continue
|
||||
|
||||
pnl_pad = " " if unrealized >= 0 else ""
|
||||
tot_pnl_pad = " " if total_pnl >= 0 else ""
|
||||
# Recalculate for logging (including bounds)
|
||||
clp_curr_val = get_clp_value(price, strat_inst)
|
||||
clp_low_val = get_clp_value(strat_inst.low_range, strat_inst)
|
||||
clp_high_val = get_clp_value(strat_inst.high_range, strat_inst)
|
||||
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {adj_val:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f}{pnl_pad} | TotPnL: {total_pnl:.2f}{tot_pnl_pad}")
|
||||
# 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:
|
||||
if int(time.time()) % 30 == 0:
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
|
||||
else:
|
||||
if int(time.time()) % 30 == 0:
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
|
||||
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
|
||||
|
||||
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))
|
||||
|
||||
|
||||
@ -63,65 +63,34 @@ formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# --- ABIs ---
|
||||
# (Kept minimal for brevity, normally would load from files)
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||
[
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
|
||||
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_POOL_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
ERC20_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_FACTORY_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
SWAP_ROUTER_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
WETH9_ABI = json.loads('''
|
||||
[
|
||||
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
|
||||
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
from clp_abis import (
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI,
|
||||
UNISWAP_V3_POOL_ABI,
|
||||
ERC20_ABI,
|
||||
UNISWAP_V3_FACTORY_ABI,
|
||||
AERODROME_FACTORY_ABI,
|
||||
AERODROME_POOL_ABI,
|
||||
AERODROME_NPM_ABI,
|
||||
SWAP_ROUTER_ABI,
|
||||
WETH9_ABI
|
||||
)
|
||||
|
||||
from clp_config import get_current_config, STATUS_FILE
|
||||
from tools.universal_swapper import execute_swap
|
||||
|
||||
# --- GET ACTIVE DEX CONFIG ---
|
||||
CONFIG = get_current_config()
|
||||
|
||||
DEX_TO_CHAIN = {
|
||||
"UNISWAP_V3": "ARBITRUM",
|
||||
"UNISWAP_wide": "ARBITRUM",
|
||||
"PANCAKESWAP_BNB": "BSC",
|
||||
"WETH_CBBTC_BASE": "BASE",
|
||||
"UNISWAP_BASE_CL": "BASE",
|
||||
"AERODROME_BASE_CL": "BASE",
|
||||
"AERODROME_WETH-USDC_008": "BASE"
|
||||
}
|
||||
|
||||
# --- CONFIGURATION FROM STRATEGY ---
|
||||
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
|
||||
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
|
||||
@ -130,11 +99,77 @@ REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", Tr
|
||||
TARGET_INVESTMENT_VALUE_USDC = CONFIG.get("TARGET_INVESTMENT_AMOUNT", 2000)
|
||||
INITIAL_HEDGE_CAPITAL_USDC = CONFIG.get("INITIAL_HEDGE_CAPITAL", 1000)
|
||||
RANGE_WIDTH_PCT = CONFIG.get("RANGE_WIDTH_PCT", Decimal("0.01"))
|
||||
RANGE_MODE = CONFIG.get("RANGE_MODE", "FIXED")
|
||||
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
|
||||
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
|
||||
|
||||
# --- AUTO RANGE HELPERS ---
|
||||
|
||||
def get_market_indicators() -> Optional[Dict]:
|
||||
file_path = os.path.join("market_data", "indicators.json")
|
||||
if not os.path.exists(file_path):
|
||||
return None
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check Freshness (5m)
|
||||
last_updated_str = data.get("last_updated")
|
||||
if not last_updated_str: return None
|
||||
|
||||
last_updated = datetime.fromisoformat(last_updated_str)
|
||||
if (datetime.now() - last_updated).total_seconds() > 300:
|
||||
logger.warning("⚠️ Market indicators file is stale (>5m).")
|
||||
return None
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading indicators: {e}")
|
||||
return None
|
||||
|
||||
def calculate_dynamic_range_pct(coin: str) -> Optional[Decimal]:
|
||||
indicators = get_market_indicators()
|
||||
if not indicators: return None
|
||||
|
||||
# Normalize symbols (Hyperliquid uses ETH, BNB while DEX uses WETH, WBNB)
|
||||
symbol_map = {"WETH": "ETH", "WBNB": "BNB"}
|
||||
lookup_coin = symbol_map.get(coin.upper(), coin.upper())
|
||||
|
||||
coin_data = indicators.get("data", {}).get(lookup_coin)
|
||||
if not coin_data: return None
|
||||
|
||||
try:
|
||||
price = Decimal(str(coin_data["current_price"]))
|
||||
bb12 = coin_data["bb"]["12h"]
|
||||
bb_low = Decimal(str(bb12["lower"]))
|
||||
bb_high = Decimal(str(bb12["upper"]))
|
||||
ma88 = Decimal(str(coin_data["ma"]["88"]))
|
||||
|
||||
# Condition 2: Price inside BB 12h
|
||||
if not (bb_low <= price <= bb_high):
|
||||
logger.warning(f"⚖️ AUTO: Price {price:.2f} is outside BB 12h ({bb_low:.2f} - {bb_high:.2f}). Skipping AUTO.")
|
||||
return None
|
||||
|
||||
# Condition 3: MA 88 inside BB 12h
|
||||
if not (bb_low <= ma88 <= bb_high):
|
||||
logger.warning(f"⚖️ AUTO: MA 88 {ma88:.2f} is outside BB 12h. Skipping AUTO.")
|
||||
return None
|
||||
|
||||
# Calculation: Max distance to BB edge
|
||||
dist_low = abs(price - bb_low)
|
||||
dist_high = abs(price - bb_high)
|
||||
max_dist = max(dist_low, dist_high)
|
||||
|
||||
range_pct = max_dist / price
|
||||
return range_pct
|
||||
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
logger.error(f"Error in dynamic range calc: {e}")
|
||||
return None
|
||||
|
||||
# --- CONFIGURATION CONSTANTS ---
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
|
||||
# Router address not strictly needed for Manager if using universal_swapper, but kept for ref
|
||||
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
|
||||
# Arbitrum WETH/USDC (or generic T0/T1)
|
||||
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
|
||||
@ -311,7 +346,8 @@ def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int
|
||||
if pool_address == '0x0000000000000000000000000000000000000000':
|
||||
return None, None
|
||||
|
||||
pool_contract = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
|
||||
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
|
||||
pool_contract = w3.eth.contract(address=pool_address, abi=pool_abi)
|
||||
|
||||
return {
|
||||
"token0_address": token0_address, "token1_address": token1_address,
|
||||
@ -397,6 +433,7 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
|
||||
def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount, token0: str, token1: str, amount0_needed: int, amount1_needed: int, sqrt_price_x96: int, d0: int, d1: int) -> bool:
|
||||
"""
|
||||
Checks balances, wraps ETH if needed, and swaps ONLY the required surplus to meet deposit requirements.
|
||||
Uses universal_swapper for the swap execution.
|
||||
"""
|
||||
token0 = clean_address(token0)
|
||||
token1 = clean_address(token1)
|
||||
@ -444,12 +481,12 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
# Price of Token0 in terms of Token1
|
||||
price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1)
|
||||
|
||||
swap_call = None
|
||||
token_in, token_out = None, None
|
||||
amount_in = 0
|
||||
chain_name = DEX_TO_CHAIN.get(os.environ.get("TARGET_DEX", "UNISWAP_V3"), "ARBITRUM")
|
||||
|
||||
buffer_multiplier = Decimal("1.02") # 2% buffer for slippage/price moves
|
||||
token_in_sym, token_out_sym = None, None
|
||||
amount_in_float = 0.0
|
||||
|
||||
buffer_multiplier = Decimal("1.03")
|
||||
if deficit0 > 0 and bal1 > amount1_needed:
|
||||
# Need T0 (ETH), Have extra T1 (USDC)
|
||||
# Swap T1 -> T0
|
||||
@ -462,8 +499,11 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
surplus1 = bal1 - amount1_needed
|
||||
|
||||
if surplus1 >= amount_in_needed:
|
||||
token_in, token_out = token1, token0
|
||||
amount_in = amount_in_needed
|
||||
# Get Symbols
|
||||
token_in_sym = token1_c.functions.symbol().call().upper()
|
||||
token_out_sym = token0_c.functions.symbol().call().upper()
|
||||
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d1))
|
||||
|
||||
logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}")
|
||||
else:
|
||||
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}")
|
||||
@ -479,38 +519,46 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
|
||||
surplus0 = bal0 - amount0_needed
|
||||
|
||||
if surplus0 >= amount_in_needed:
|
||||
token_in, token_out = token0, token1
|
||||
amount_in = amount_in_needed
|
||||
token_in_sym = token0_c.functions.symbol().call().upper()
|
||||
token_out_sym = token1_c.functions.symbol().call().upper()
|
||||
amount_in_float = float(Decimal(amount_in_needed) / Decimal(10**d0))
|
||||
|
||||
logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}")
|
||||
else:
|
||||
logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}")
|
||||
|
||||
if token_in and amount_in > 0:
|
||||
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...")
|
||||
if token_in_sym and amount_in_float > 0:
|
||||
logger.info(f"🔄 Delegating Swap to Universal Swapper: {amount_in_float} {token_in_sym} -> {token_out_sym} on {chain_name}...")
|
||||
try:
|
||||
# Use Standard Fee (500) if configured fee is weird (like 1 for Aerodrome tickSpacing)
|
||||
# This ensures the standard router finds a valid pool (WETH/USDC 0.05%)
|
||||
swap_fee = POOL_FEE if POOL_FEE >= 100 else 500
|
||||
|
||||
if not ensure_allowance(w3, account, token_in, UNISWAP_V3_SWAP_ROUTER_ADDRESS, amount_in):
|
||||
return False
|
||||
# Call Universal Swapper
|
||||
execute_swap(chain_name, token_in_sym, token_out_sym, amount_in_float, fee_tier=swap_fee)
|
||||
|
||||
params = (
|
||||
token_in, token_out, POOL_FEE, account.address,
|
||||
int(time.time()) + 120,
|
||||
amount_in,
|
||||
0, # amountOutMin (Market swap for rebalance)
|
||||
0
|
||||
)
|
||||
# Wait for node indexing
|
||||
logger.info("⏳ Waiting for balance update...")
|
||||
time.sleep(2)
|
||||
|
||||
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
|
||||
# Retry check loop
|
||||
for i in range(3):
|
||||
bal0 = token0_c.functions.balanceOf(account.address).call()
|
||||
bal1 = token1_c.functions.balanceOf(account.address).call()
|
||||
# If we are strictly >= needed, great.
|
||||
|
||||
if bal0 >= amount0_needed and bal1 >= amount1_needed:
|
||||
logger.info("✅ Balances sufficient.")
|
||||
return True
|
||||
else:
|
||||
|
||||
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
|
||||
@ -531,14 +579,20 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
|
||||
amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
|
||||
|
||||
# 3. Mint
|
||||
params = (
|
||||
base_params = [
|
||||
token0, token1, POOL_FEE,
|
||||
tick_lower, tick_upper,
|
||||
amount0, amount1,
|
||||
amount0_min, amount1_min,
|
||||
account.address,
|
||||
int(time.time()) + 180
|
||||
)
|
||||
]
|
||||
|
||||
# Aerodrome Slipstream expects sqrtPriceX96 as the last parameter
|
||||
if "AERODROME" in os.environ.get("TARGET_DEX", "").upper():
|
||||
base_params.append(0) # sqrtPriceX96
|
||||
|
||||
params = tuple(base_params)
|
||||
|
||||
receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position")
|
||||
|
||||
@ -696,9 +750,43 @@ def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
|
||||
save_status_data(data)
|
||||
logger.info(f"💾 Updated Position {token_id} status to {status}")
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
|
||||
# --- REAL-TIME ORACLE HELPER ---
|
||||
def get_realtime_price(coin: str) -> Optional[Decimal]:
|
||||
"""Fetches current mid-price directly from Hyperliquid API (low latency)."""
|
||||
try:
|
||||
url = "https://api.hyperliquid.xyz/info"
|
||||
response = requests.post(url, json={"type": "allMids"}, timeout=2)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Hyperliquid symbols are usually clean (ETH, BNB)
|
||||
# Map common variations just in case
|
||||
target = coin.upper().replace("WETH", "ETH").replace("WBNB", "BNB")
|
||||
|
||||
if target in data:
|
||||
return Decimal(data[target])
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Failed to fetch realtime Oracle price: {e}")
|
||||
return None
|
||||
|
||||
# --- MAIN LOOP ---
|
||||
|
||||
def main():
|
||||
# --- ARGUMENT PARSING ---
|
||||
parser = argparse.ArgumentParser(description="Uniswap CLP Manager")
|
||||
parser.add_argument("--force", type=float, help="Force open a position with specific range width (e.g., 0.75), ignoring AUTO safe checks.")
|
||||
args = parser.parse_args()
|
||||
|
||||
force_mode_active = False
|
||||
force_width_pct = Decimal("0")
|
||||
|
||||
if args.force:
|
||||
force_mode_active = True
|
||||
force_width_pct = Decimal(str(args.force)) / 100 # Convert 0.75 -> 0.0075
|
||||
logger.warning(f"🚨 FORCE MODE ACTIVE: Will bypass safe checks for FIRST position with width {args.force}%")
|
||||
|
||||
logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
|
||||
load_dotenv(override=True)
|
||||
|
||||
@ -722,9 +810,23 @@ def main():
|
||||
logger.info(f"👤 Wallet: {account.address}")
|
||||
|
||||
# Contracts
|
||||
target_dex_name = os.environ.get("TARGET_DEX", "").upper()
|
||||
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
|
||||
logger.info("✈️ Using Aerodrome NPM ABI")
|
||||
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=AERODROME_NPM_ABI)
|
||||
else:
|
||||
npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
|
||||
|
||||
factory_addr = npm.functions.factory().call()
|
||||
factory = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
|
||||
|
||||
# Select Factory ABI based on DEX type
|
||||
if "AERODROME" in target_dex_name or "AERODROME" in CONFIG.get("NAME", "").upper():
|
||||
logger.info("✈️ Using Aerodrome Factory ABI (tickSpacing instead of fee)")
|
||||
factory_abi = AERODROME_FACTORY_ABI
|
||||
else:
|
||||
factory_abi = UNISWAP_V3_FACTORY_ABI
|
||||
|
||||
factory = w3.eth.contract(address=factory_addr, abi=factory_abi)
|
||||
router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
|
||||
|
||||
while True:
|
||||
@ -817,7 +919,21 @@ def main():
|
||||
pnl_unrealized = current_pos_value_usd - initial_value
|
||||
total_pnl_usd = pnl_unrealized + total_fees_usd
|
||||
|
||||
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})"
|
||||
# --- PERSIST PERFORMANCE TO JSON ---
|
||||
update_position_status(token_id, "OPEN", {
|
||||
"clp_fees": round(float(total_fees_usd), 2),
|
||||
"clp_TotPnL": round(float(total_pnl_usd), 2)
|
||||
})
|
||||
|
||||
# Calculate Fees/h
|
||||
fees_per_h_str = "0.00"
|
||||
ts_open = active_auto_pos.get('timestamp_open')
|
||||
if ts_open:
|
||||
hours_open = (time.time() - ts_open) / 3600
|
||||
if hours_open > 0.01:
|
||||
fees_per_h_str = f"{float(total_fees_usd) / hours_open:.2f}"
|
||||
|
||||
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f} | ${fees_per_h_str}/h)"
|
||||
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
|
||||
|
||||
# --- KPI LOGGING ---
|
||||
@ -845,6 +961,32 @@ def main():
|
||||
|
||||
log_kpi_snapshot(snapshot)
|
||||
|
||||
# --- REPOSITION LOGIC ---
|
||||
pos_range_mode = active_auto_pos.get("range_mode", RANGE_MODE)
|
||||
|
||||
if pos_range_mode == "AUTO" and CLOSE_POSITION_ENABLED:
|
||||
coin_for_dynamic = pos_details['token0_symbol'] if not is_t0_stable else pos_details['token1_symbol']
|
||||
new_range_width = calculate_dynamic_range_pct(coin_for_dynamic)
|
||||
|
||||
if new_range_width:
|
||||
# Use initial width from JSON, or current config width as fallback
|
||||
old_range_width = Decimal(str(active_auto_pos.get("range_width_initial", RANGE_WIDTH_PCT)))
|
||||
|
||||
# Condition A: Difference > 20%
|
||||
width_diff_pct = abs(new_range_width - old_range_width) / old_range_width
|
||||
|
||||
# Condition B: Profit > 0.1%
|
||||
profit_pct = total_pnl_usd / initial_value
|
||||
|
||||
logger.info(f"📊 AUTO Check: CurRange {old_range_width*100:.2f}%, NewRange {new_range_width*100:.2f}% | Diff {width_diff_pct*100:.1f}% | Profit {profit_pct*100:.2f}%")
|
||||
|
||||
if width_diff_pct > 0.20 and profit_pct > 0.001:
|
||||
logger.warning(f"🔄 REPOSITION TRIGGERED: Width Diff {width_diff_pct*100:.1f}%, Profit {profit_pct*100:.2f}%")
|
||||
# Set in_range to False to force the closing logic below
|
||||
in_range = False
|
||||
else:
|
||||
logger.warning(f"⚖️ AUTO Check Skipped: Market indicators for {coin_for_dynamic} are stale or conditions not met.")
|
||||
|
||||
if not in_range and CLOSE_POSITION_ENABLED:
|
||||
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
|
||||
update_position_status(token_id, "CLOSING")
|
||||
@ -875,14 +1017,73 @@ def main():
|
||||
fee = POOL_FEE
|
||||
|
||||
pool_addr = factory.functions.getPool(token0, token1, fee).call()
|
||||
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
|
||||
pool_abi = AERODROME_POOL_ABI if "AERODROME" in CONFIG.get("NAME", "").upper() else UNISWAP_V3_POOL_ABI
|
||||
pool_c = w3.eth.contract(address=pool_addr, abi=pool_abi)
|
||||
pool_data = get_pool_dynamic_data(pool_c)
|
||||
|
||||
if pool_data:
|
||||
tick = pool_data['tick']
|
||||
# Define Range (+/- 2.5%)
|
||||
# log(1.025) / log(1.0001) approx 247 tick delta
|
||||
tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001))
|
||||
|
||||
# --- PRE-CALCULATE ESSENTIALS ---
|
||||
# Fetch Decimals & Symbols immediately (Required for Oracle Check)
|
||||
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
d0 = t0_c.functions.decimals().call()
|
||||
d1 = t1_c.functions.decimals().call()
|
||||
|
||||
t0_sym = t0_c.functions.symbol().call().upper()
|
||||
t1_sym = t1_c.functions.symbol().call().upper()
|
||||
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
|
||||
|
||||
is_t1_stable = any(s in t1_sym for s in stable_symbols)
|
||||
is_t0_stable = any(s in t0_sym for s in stable_symbols)
|
||||
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
|
||||
# Define coin_sym early for Guard Rails
|
||||
coin_sym = CONFIG.get("COIN_SYMBOL", "ETH")
|
||||
|
||||
# --- ORACLE GUARD RAIL ---
|
||||
# Protect against Pool/Oracle divergence (Manipulation/Depeg/Lag)
|
||||
if not force_mode_active:
|
||||
oracle_price = get_realtime_price(coin_sym)
|
||||
|
||||
if oracle_price:
|
||||
pool_price_dec = price_0_in_1 if is_t1_stable else (Decimal("1") / price_0_in_1)
|
||||
divergence = abs(pool_price_dec - oracle_price) / oracle_price
|
||||
|
||||
if divergence > Decimal("0.0025"): # 0.25% Tolerance
|
||||
logger.warning(f"⚠️ Price Divergence! Pool: {pool_price_dec:.2f} vs Oracle: {oracle_price:.2f} (Diff: {divergence*100:.2f}%). Aborting.")
|
||||
time.sleep(10)
|
||||
continue
|
||||
else:
|
||||
logger.warning("⚠️ Could not fetch Oracle price. Proceeding with caution (or consider aborting).")
|
||||
|
||||
# --- DYNAMIC RANGE CALCULATION ---
|
||||
active_range_width = RANGE_WIDTH_PCT
|
||||
current_range_mode = RANGE_MODE
|
||||
|
||||
# 1. PRIORITY: Force Mode
|
||||
if force_mode_active:
|
||||
logger.warning(f"🚨 FORCE OVERRIDE: Using forced width {force_width_pct*100:.2f}% (Ignoring safe checks)")
|
||||
active_range_width = force_width_pct
|
||||
current_range_mode = "FIXED"
|
||||
|
||||
# 2. AUTO Mode (Only if not forced)
|
||||
elif RANGE_MODE == "AUTO":
|
||||
dynamic_width = calculate_dynamic_range_pct(coin_sym)
|
||||
if dynamic_width:
|
||||
active_range_width = dynamic_width
|
||||
logger.info(f"⚖️ AUTO Range Activated: {active_range_width*100:.4f}%")
|
||||
else:
|
||||
logger.info(f"⛔ AUTO conditions not met. Waiting for safe entry...")
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
continue # Skip logic
|
||||
|
||||
# 3. FIXED Mode (Default Fallback) is already set by initial active_range_width
|
||||
|
||||
# Define Range
|
||||
tick_delta = int(math.log(1 + float(active_range_width)) / math.log(1.0001))
|
||||
|
||||
# Fetch actual tick spacing from pool
|
||||
tick_spacing = pool_c.functions.tickSpacing().call()
|
||||
@ -893,28 +1094,10 @@ def main():
|
||||
|
||||
# Calculate Amounts
|
||||
# Target Value logic
|
||||
d0 = 18 # Default WETH (Corrected below if needed, but we rely on raw logic)
|
||||
# Actually, we should fetch decimals from contract to be safe, but config assumes standard.
|
||||
|
||||
# Fetch Decimals for precision
|
||||
t0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
t1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
d0 = t0_c.functions.decimals().call()
|
||||
d1 = t1_c.functions.decimals().call()
|
||||
|
||||
# Determine Investment Value in Token1 terms
|
||||
target_usd = Decimal(str(TARGET_INVESTMENT_VALUE_USDC))
|
||||
|
||||
# Check which is stable
|
||||
t0_sym = t0_c.functions.symbol().call().upper()
|
||||
t1_sym = t1_c.functions.symbol().call().upper()
|
||||
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
|
||||
|
||||
is_t1_stable = any(s in t1_sym for s in stable_symbols)
|
||||
is_t0_stable = any(s in t0_sym for s in stable_symbols)
|
||||
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
|
||||
investment_val_token1 = Decimal("0")
|
||||
|
||||
if str(TARGET_INVESTMENT_VALUE_USDC).upper() == "MAX":
|
||||
@ -939,10 +1122,33 @@ def main():
|
||||
amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_token1, d0, d1, pool_data['sqrtPriceX96'])
|
||||
|
||||
if check_and_swap_for_deposit(w3, router, account, token0, token1, amt0, amt1, pool_data['sqrtPriceX96'], d0, d1):
|
||||
# --- STALE DATA PROTECTION (Pre-Mint) ---
|
||||
# Check if price moved significantly during calculation/swap
|
||||
pre_mint_data = get_pool_dynamic_data(pool_c)
|
||||
if pre_mint_data:
|
||||
tick_diff = abs(pre_mint_data['tick'] - pool_data['tick'])
|
||||
# 13 ticks ~ 0.13% price move. Abort if volatile.
|
||||
if tick_diff > 13:
|
||||
logger.warning(f"⚠️ Price moved too much ({tick_diff} ticks) during setup/swap. Aborting mint to prevent bad entry.")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
|
||||
if minted:
|
||||
# Calculate entry price and amounts for JSON compatibility
|
||||
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
# --- DISABLE FORCE MODE AFTER FIRST MINT ---
|
||||
if force_mode_active:
|
||||
logger.info("🛑 FORCE MODE CONSUMED: Returning to standard AUTO checks for future positions.")
|
||||
force_mode_active = False
|
||||
|
||||
# --- RE-FETCH PRICE FOR ACCURATE ENTRY DATA (Post-Mint) ---
|
||||
fresh_pool_data = get_pool_dynamic_data(pool_c)
|
||||
if fresh_pool_data:
|
||||
fresh_tick = fresh_pool_data['tick']
|
||||
price_0_in_1 = price_from_tick(fresh_tick, d0, d1)
|
||||
logger.info(f"🔄 Refreshed Entry Tick: {fresh_tick} (Was: {pool_data['tick']})")
|
||||
else:
|
||||
price_0_in_1 = price_from_tick(pool_data['tick'], d0, d1)
|
||||
|
||||
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
|
||||
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
|
||||
|
||||
@ -970,6 +1176,8 @@ def main():
|
||||
"range_lower": round(r_lower, 4),
|
||||
"token0_decimals": d0,
|
||||
"token1_decimals": d1,
|
||||
"range_mode": current_range_mode,
|
||||
"range_width_initial": float(active_range_width),
|
||||
"timestamp_open": int(time.time()),
|
||||
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
|
||||
}
|
||||
|
||||
103
florida/doc/AERODROME_CL_INTEGRATION.md
Normal file
103
florida/doc/AERODROME_CL_INTEGRATION.md
Normal 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` |
|
||||
66
florida/doc/CLP_DATA_INTERPRETATION.md
Normal file
66
florida/doc/CLP_DATA_INTERPRETATION.md
Normal 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.*
|
||||
93
florida/doc/SECURITY_AND_OPTIMIZATION.md
Normal file
93
florida/doc/SECURITY_AND_OPTIMIZATION.md
Normal 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.
|
||||
49
florida/tests/backtest/analyze_results.py
Normal file
49
florida/tests/backtest/analyze_results.py
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
|
||||
def analyze():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
results_file = os.path.join(current_dir, "optimization_results.csv")
|
||||
|
||||
if not os.path.exists(results_file):
|
||||
print(f"File not found: {results_file}")
|
||||
return
|
||||
|
||||
print(f"Analyzing {results_file}...")
|
||||
|
||||
best_pnl = -float('inf')
|
||||
best_config = None
|
||||
|
||||
rows = []
|
||||
|
||||
with open(results_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(row)
|
||||
|
||||
pnl = float(row['TOTAL_PNL'])
|
||||
uni_fees = float(row['UNI_FEES'])
|
||||
hl_pnl = float(row['HL_PNL'])
|
||||
|
||||
print(f"Config: Range={row['RANGE_WIDTH_PCT']}, Thresh={row['BASE_REBALANCE_THRESHOLD_PCT']} | PnL: ${pnl:.2f} (Fees: ${uni_fees:.2f}, Hedge: ${hl_pnl:.2f})")
|
||||
|
||||
if pnl > best_pnl:
|
||||
best_pnl = pnl
|
||||
best_config = row
|
||||
|
||||
print("\n" + "="*40)
|
||||
print(f"🏆 BEST CONFIGURATION")
|
||||
print("="*40)
|
||||
if best_config:
|
||||
print(f"Range Width: {float(best_config['RANGE_WIDTH_PCT'])*100:.2f}%")
|
||||
print(f"Rebalance Thresh: {float(best_config['BASE_REBALANCE_THRESHOLD_PCT'])*100:.0f}%")
|
||||
print(f"Total PnL: ${float(best_config['TOTAL_PNL']):.2f}")
|
||||
print(f" > Uni Fees: ${float(best_config['UNI_FEES']):.2f}")
|
||||
print(f" > Hedge PnL: ${float(best_config['HL_PNL']):.2f}")
|
||||
else:
|
||||
print("No valid results found.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze()
|
||||
312
florida/tests/backtest/backtester.py
Normal file
312
florida/tests/backtest/backtester.py
Normal file
@ -0,0 +1,312 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.mocks import MockExchangeState, MockWeb3, MockExchangeAPI, MockInfo, MockContract
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(name)s] %(message)s')
|
||||
logger = logging.getLogger("BACKTESTER")
|
||||
|
||||
class Backtester:
|
||||
def __init__(self, book_file, trades_file, config_overrides=None):
|
||||
self.book_file = book_file
|
||||
self.trades_file = trades_file
|
||||
self.config_overrides = config_overrides or {}
|
||||
self.events = []
|
||||
self.state = MockExchangeState()
|
||||
|
||||
# Mocks
|
||||
self.mock_web3 = MockWeb3(self.state)
|
||||
self.mock_hl_api = MockExchangeAPI(self.state)
|
||||
self.mock_hl_info = MockInfo(self.state)
|
||||
|
||||
# Components (Lazy loaded)
|
||||
self.manager = None
|
||||
self.hedger = None
|
||||
|
||||
def load_data(self):
|
||||
logger.info("Loading Market Data...")
|
||||
# Load Book
|
||||
with open(self.book_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
self.events.append({
|
||||
"type": "BOOK",
|
||||
"ts": int(row['timestamp_ms']),
|
||||
"data": row
|
||||
})
|
||||
|
||||
# Load Trades
|
||||
# (Optional: Trades are useful for market impact, but for basic PnL tracking
|
||||
# based on mid-price, Book is sufficient. Loading trades just to advance time)
|
||||
with open(self.trades_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
self.events.append({
|
||||
"type": "TRADE",
|
||||
"ts": int(row['timestamp_ms']),
|
||||
"data": row
|
||||
})
|
||||
|
||||
# Sort by Timestamp
|
||||
self.events.sort(key=lambda x: x['ts'])
|
||||
logger.info(f"Loaded {len(self.events)} events.")
|
||||
|
||||
def patch_and_init(self):
|
||||
logger.info("Initializing Logic...")
|
||||
|
||||
# --- PATCH MANAGER ---
|
||||
# We need to patch clp_manager.Web3 to return our MockWeb3
|
||||
# And os.environ for config
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"TARGET_DEX": "PANCAKESWAP_BNB", # Example
|
||||
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"BNB_RPC_URL": "http://mock",
|
||||
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"MAIN_WALLET_ADDRESS": "0xMyWallet"
|
||||
}):
|
||||
import clp_manager
|
||||
import clp_hedger
|
||||
|
||||
# Apply Config Overrides
|
||||
if self.config_overrides:
|
||||
logger.info(f"Applying Config Overrides: {self.config_overrides}")
|
||||
for k, v in self.config_overrides.items():
|
||||
# Patch Manager
|
||||
if hasattr(clp_manager, k):
|
||||
setattr(clp_manager, k, v)
|
||||
# Patch Hedger
|
||||
if hasattr(clp_hedger, k):
|
||||
setattr(clp_hedger, k, v)
|
||||
|
||||
# 1. Init Manager
|
||||
# clp_manager.main() connects to Web3. We need to inject our mock.
|
||||
# Since clp_manager creates w3 inside main(), we can't inject easily without patching Web3 class.
|
||||
|
||||
self.manager_module = clp_manager
|
||||
self.hedger_module = clp_hedger
|
||||
|
||||
def run(self):
|
||||
self.load_data()
|
||||
self.patch_and_init()
|
||||
|
||||
# MOCK TIME
|
||||
start_time = self.events[0]['ts'] / 1000.0
|
||||
|
||||
# STATUS FILE MOCK
|
||||
self.status_memory = [] # List[Dict]
|
||||
|
||||
def mock_load_status():
|
||||
logger.info(f"MOCK LOAD STATUS: Found {len(self.status_memory)} items")
|
||||
return self.status_memory
|
||||
|
||||
def mock_save_status(data):
|
||||
logger.info(f"MOCK SAVE STATUS: Saving {len(data)} items")
|
||||
self.status_memory = data
|
||||
|
||||
def mock_hedger_scan():
|
||||
return []
|
||||
|
||||
# We need to globally patch time.time and the Libraries
|
||||
web3_class_mock = MagicMock(return_value=self.mock_web3)
|
||||
web3_class_mock.to_wei = self.mock_web3.to_wei
|
||||
web3_class_mock.from_wei = self.mock_web3.from_wei
|
||||
web3_class_mock.is_address = self.mock_web3.is_address
|
||||
web3_class_mock.to_checksum_address = lambda x: x
|
||||
|
||||
# Mock Web3.keccak to return correct topics
|
||||
def mock_keccak(text=None, hexstr=None):
|
||||
# Known Topics
|
||||
if text == "Transfer(address,address,uint256)":
|
||||
return bytes.fromhex("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||
if text == "IncreaseLiquidity(uint256,uint128,uint256,uint256)":
|
||||
return bytes.fromhex("7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde")
|
||||
if text == "DecreaseLiquidity(uint256,uint128,uint256,uint256)":
|
||||
# 0x26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4
|
||||
return bytes.fromhex("26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4")
|
||||
if text == "Collect(uint256,address,uint256,uint256)":
|
||||
# 0x70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d
|
||||
return bytes.fromhex("70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d")
|
||||
|
||||
return b'\x00'*32
|
||||
|
||||
web3_class_mock.keccak = MagicMock(side_effect=mock_keccak)
|
||||
|
||||
# Ensure environment is patched during the whole run
|
||||
env_patch = {
|
||||
"TARGET_DEX": "PANCAKESWAP_BNB",
|
||||
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"BNB_RPC_URL": "http://mock",
|
||||
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"MAIN_WALLET_ADDRESS": "0xMyWallet"
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_patch), \
|
||||
patch('time.time', side_effect=lambda: self.state.current_time_ms / 1000.0), \
|
||||
patch('clp_manager.Web3', web3_class_mock), \
|
||||
patch('clp_hedger.Account.from_key', return_value=MagicMock(address="0xMyWallet")), \
|
||||
patch('clp_hedger.Exchange', return_value=self.mock_hl_api), \
|
||||
patch('clp_hedger.Info', return_value=self.mock_hl_info), \
|
||||
patch('clp_manager.load_status_data', side_effect=mock_load_status), \
|
||||
patch('clp_manager.save_status_data', side_effect=mock_save_status), \
|
||||
patch('clp_manager.clean_address', side_effect=lambda x: x), \
|
||||
patch('clp_hedger.glob.glob', return_value=[]):
|
||||
|
||||
# Initialize Hedger (It creates the classes in __init__)
|
||||
self.hedger = self.hedger_module.UnifiedHedger()
|
||||
|
||||
# Initialize Manager Components manually (simulate main setup)
|
||||
w3 = self.mock_web3
|
||||
account = MagicMock(address="0xMyWallet")
|
||||
npm = w3.eth.contract("0xNPM", [])
|
||||
factory = w3.eth.contract("0xFactory", [])
|
||||
router = w3.eth.contract("0xRouter", [])
|
||||
|
||||
# --- SIMULATION LOOP ---
|
||||
last_manager_tick = 0
|
||||
manager_interval = 60 * 1000
|
||||
|
||||
trade_count = len([e for e in self.events if e['type'] == "TRADE"])
|
||||
book_count = len([e for e in self.events if e['type'] == "BOOK"])
|
||||
logger.info(f"SIMULATION START: {len(self.events)} total events ({book_count} BOOK, {trade_count} TRADE)")
|
||||
|
||||
for event in self.events:
|
||||
self.state.current_time_ms = event['ts']
|
||||
|
||||
# Update Market
|
||||
if event['type'] == "BOOK":
|
||||
row = event['data']
|
||||
mid = Decimal(row['mid_price'])
|
||||
self.state.update_price("BNB", mid)
|
||||
|
||||
if event['type'] == "TRADE":
|
||||
self.state.process_trade(event['data'])
|
||||
|
||||
# Run Logic
|
||||
|
||||
# 1. Manager (Every X seconds)
|
||||
if self.state.current_time_ms - last_manager_tick > manager_interval:
|
||||
self.manager_module.run_tick(w3, account, npm, factory, router)
|
||||
last_manager_tick = self.state.current_time_ms
|
||||
|
||||
# SYNC MANAGER STATUS TO HEDGER
|
||||
for pos in self.status_memory:
|
||||
if pos.get('status') == 'OPEN' and pos.get('type') == 'AUTOMATIC':
|
||||
key = ("MOCK", pos['token_id'])
|
||||
if key not in self.hedger.strategies:
|
||||
self.hedger._init_single_strategy(key, pos, "BNB")
|
||||
else:
|
||||
self.hedger.strategy_states[key]['status'] = pos.get('status', 'OPEN')
|
||||
elif pos.get('status') == 'CLOSED':
|
||||
key = ("MOCK", pos['token_id'])
|
||||
if key in self.hedger.strategies:
|
||||
self.hedger.strategy_states[key]['status'] = 'CLOSED'
|
||||
|
||||
|
||||
# 2. Hedger (Every Tick/Event)
|
||||
self.hedger.run_tick()
|
||||
|
||||
# Finalize: Collect accrued fees from open positions
|
||||
logger.info(f"Finalizing... Checking {len(self.state.uni_positions)} open positions.")
|
||||
for token_id, pos in self.state.uni_positions.items():
|
||||
raw_owed0 = pos.get('tokensOwed0', 0)
|
||||
logger.info(f"DEBUG: Position {token_id} Raw TokensOwed0: {raw_owed0}")
|
||||
|
||||
owed0 = Decimal(raw_owed0) / Decimal(10**18)
|
||||
owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18)
|
||||
|
||||
# Convert to USD
|
||||
price = self.state.prices.get("BNB", Decimal("0"))
|
||||
# Fee0 is USDT (USD), Fee1 is WBNB
|
||||
usd_val = owed0 + (owed1 * price)
|
||||
|
||||
if usd_val > 0:
|
||||
self.state.uni_fees_collected += usd_val
|
||||
logger.info(f"Finalizing Open Position {token_id}: Accrued Fees ${usd_val:.2f}")
|
||||
|
||||
logger.info("Backtest Complete.")
|
||||
logger.info(f"Final Uni Fees: {self.state.uni_fees_collected}")
|
||||
logger.info(f"Final HL PnL: {self.state.hl_realized_pnl - self.state.hl_fees_paid}")
|
||||
|
||||
def calculate_final_nav(self):
|
||||
"""Calculates total Net Asset Value (USD) at the end of simulation."""
|
||||
total_usd = Decimal("0")
|
||||
|
||||
# 1. Wallet Balances
|
||||
# We assume T0=USDT, T1=WBNB for this profile
|
||||
price = self.state.prices.get("BNB", Decimal("0"))
|
||||
|
||||
for sym, bal in self.state.wallet_balances.items():
|
||||
if sym in ["USDC", "USDT"]:
|
||||
total_usd += bal
|
||||
elif sym in ["BNB", "WBNB", "NATIVE"]:
|
||||
total_usd += bal * price
|
||||
elif sym in ["ETH", "WETH"]:
|
||||
# If ETH price available? We mocked update_price("BNB") only.
|
||||
# Assuming ETH price static or 0 if not tracked
|
||||
eth_price = self.state.prices.get("ETH", Decimal("0"))
|
||||
total_usd += bal * eth_price
|
||||
|
||||
# 2. Uniswap Positions (Liquidity Value)
|
||||
# Value = Amount0 * Price0 + Amount1 * Price1
|
||||
# We need to calculate amounts from liquidity & current price
|
||||
import math
|
||||
# Helper to get amounts from liquidity
|
||||
def get_amounts(liquidity, sqrt_price_x96, tick_lower, tick_upper):
|
||||
# Simplified: Use the amounts we stored at mint time?
|
||||
# No, that's initial. We need current value.
|
||||
# But calculating precise amounts from liquidity/sqrtPrice requires complex math.
|
||||
# For approximation, we can look at what the manager logged as "Deposited"
|
||||
# if price hasn't moved much, or implement full liquidity math.
|
||||
|
||||
# Since implementing full math here is complex, let's use a simplified approach:
|
||||
# If we are in range, we have a mix.
|
||||
# If out of range, we have 100% of one token.
|
||||
|
||||
# Better: The Mock 'mint' stored initial amounts.
|
||||
# We can adjust by price ratio? No, IL is non-linear.
|
||||
|
||||
# Let's use the 'decrease_liquidity' logic mock if available?
|
||||
# Or just assume Liquidity Value = Initial Value + PnL (Fees) - IL.
|
||||
|
||||
# For this MVP, let's just count the Fees collected (Realized) + Initial Capital (Wallet).
|
||||
# BUT we spent wallet funds to open LP.
|
||||
# So Wallet is LOW. LP has Value.
|
||||
|
||||
# We MUST value the LP.
|
||||
# Let's approximate:
|
||||
# Value = Liquidity / (something) ...
|
||||
# Actually, `clp_manager.py` calculates `actual_value` on entry.
|
||||
# We can track `entry_value` in the position state.
|
||||
return Decimal("0") # Placeholder if we can't calc easily
|
||||
|
||||
# 3. Hyperliquid Positions (Unrealized PnL + Margin)
|
||||
hl_equity = self.state.hl_balances.get("USDC", 0) # Margin
|
||||
for sym, pos in self.state.hl_positions.items():
|
||||
hl_equity += pos['unrealized_pnl']
|
||||
|
||||
total_usd += hl_equity
|
||||
|
||||
return total_usd
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage:
|
||||
# python tests/backtest/backtester.py market_data/BNB_raw_20251230_book.csv market_data/BNB_raw_20251230_trades.csv
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python backtester.py <book_csv> <trades_csv>")
|
||||
else:
|
||||
bt = Backtester(sys.argv[1], sys.argv[2])
|
||||
bt.run()
|
||||
82
florida/tests/backtest/grid_search.py
Normal file
82
florida/tests/backtest/grid_search.py
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
import itertools
|
||||
from decimal import Decimal
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.backtester import Backtester
|
||||
|
||||
def main():
|
||||
# Grid Parameters
|
||||
# We want to optimize:
|
||||
# 1. RANGE_WIDTH_PCT: How wide is the LP position? (e.g. 0.01 = +/-1%, 0.05 = +/-5%)
|
||||
# 2. BASE_REBALANCE_THRESHOLD_PCT: When do we hedge? (e.g. 0.10 = 10% delta drift, 0.20 = 20%)
|
||||
|
||||
param_grid = {
|
||||
"RANGE_WIDTH_PCT": [0.005, 0.01, 0.025, 0.05],
|
||||
"BASE_REBALANCE_THRESHOLD_PCT": [0.01, 0.05]
|
||||
}
|
||||
|
||||
keys, values = zip(*param_grid.items())
|
||||
combinations = [dict(zip(keys, v)) for v in itertools.product(*values)]
|
||||
|
||||
results = []
|
||||
|
||||
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
|
||||
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
|
||||
|
||||
print(f"Starting Grid Search with {len(combinations)} combinations...")
|
||||
|
||||
for idx, config in enumerate(combinations):
|
||||
print(f"\n--- Run {idx+1}/{len(combinations)}: {config} ---")
|
||||
|
||||
# Initialize Backtester with overrides
|
||||
bt = Backtester(book_file, trades_file, config_overrides=config)
|
||||
|
||||
try:
|
||||
bt.run()
|
||||
|
||||
# Collect Metrics
|
||||
uni_fees = bt.state.uni_fees_collected
|
||||
hl_realized = bt.state.hl_realized_pnl - bt.state.hl_fees_paid
|
||||
|
||||
# HL Unrealized
|
||||
hl_unrealized = sum(p['unrealized_pnl'] for p in bt.state.hl_positions.values())
|
||||
|
||||
# Total PnL (Yield + Hedge Result) - Ignoring IL for now (Mock limitation)
|
||||
total_pnl = uni_fees + hl_realized + hl_unrealized
|
||||
|
||||
result = {
|
||||
**config,
|
||||
"UNI_FEES": float(uni_fees),
|
||||
"HL_REALIZED": float(hl_realized),
|
||||
"HL_UNREALIZED": float(hl_unrealized),
|
||||
"TOTAL_PNL": float(total_pnl)
|
||||
}
|
||||
results.append(result)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Run failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Save Results
|
||||
out_file = os.path.join(current_dir, "optimization_results.csv")
|
||||
keys = list(combinations[0].keys()) + ["UNI_FEES", "HL_REALIZED", "HL_UNREALIZED", "TOTAL_PNL"]
|
||||
|
||||
with open(out_file, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=keys)
|
||||
writer.writeheader()
|
||||
writer.writerows(results)
|
||||
|
||||
print(f"\nGrid Search Complete. Results saved to {out_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
607
florida/tests/backtest/mocks.py
Normal file
607
florida/tests/backtest/mocks.py
Normal file
@ -0,0 +1,607 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, List, Any, Optional
|
||||
from hexbytes import HexBytes
|
||||
|
||||
logger = logging.getLogger("BACKTEST_MOCK")
|
||||
|
||||
class MockExchangeState:
|
||||
"""
|
||||
Central source of truth for the simulation.
|
||||
Acts as the "Blockchain" and the "CEX Engine".
|
||||
"""
|
||||
def __init__(self):
|
||||
self.current_time_ms = 0
|
||||
self.prices = {} # symbol -> price (Decimal)
|
||||
self.ticks = {} # symbol -> current tick (int)
|
||||
|
||||
# Balances
|
||||
self.wallet_balances = {
|
||||
"NATIVE": Decimal("100.0"), # ETH or BNB
|
||||
"ETH": Decimal("100.0"),
|
||||
"USDC": Decimal("100000.0"),
|
||||
"WETH": Decimal("100.0"),
|
||||
"BNB": Decimal("100.0"),
|
||||
"WBNB": Decimal("100.0"),
|
||||
"USDT": Decimal("100000.0")
|
||||
}
|
||||
self.hl_balances = {"USDC": Decimal("10000.0"), "USDT": Decimal("10000.0")}
|
||||
self.hl_positions = {} # symbol -> {size, entry_px, unrealized_pnl}
|
||||
|
||||
# Uniswap Positions
|
||||
self.next_token_id = 1000
|
||||
self.uni_positions = {} # token_id -> {liquidity, tickLower, tickUpper, ...}
|
||||
|
||||
# Hyperliquid Orders
|
||||
self.hl_orders = [] # List of {oid, coin, side, limitPx, sz, timestamp}
|
||||
self.next_oid = 1
|
||||
|
||||
# Fees/PnL Tracking
|
||||
self.uni_fees_collected = Decimal("0.0")
|
||||
self.hl_fees_paid = Decimal("0.0")
|
||||
self.hl_realized_pnl = Decimal("0.0")
|
||||
|
||||
# Pending TXs (Simulating Mempool/Execution)
|
||||
self.pending_txs = []
|
||||
|
||||
def update_price(self, symbol: str, price: Decimal, tick: int = 0):
|
||||
self.prices[symbol] = price
|
||||
if tick:
|
||||
self.ticks[symbol] = tick
|
||||
|
||||
# Update PnL for open positions
|
||||
if symbol in self.hl_positions:
|
||||
pos = self.hl_positions[symbol]
|
||||
size = pos['size']
|
||||
if size != 0:
|
||||
# Long: (Price - Entry) * Size
|
||||
# Short: (Entry - Price) * abs(Size)
|
||||
if size > 0:
|
||||
pos['unrealized_pnl'] = (price - pos['entry_px']) * size
|
||||
else:
|
||||
pos['unrealized_pnl'] = (pos['entry_px'] - price) * abs(size)
|
||||
|
||||
def process_trade(self, trade_data):
|
||||
"""Simulate fee accumulation from market trades."""
|
||||
# trade_data: {price, size, ...} from CSV
|
||||
try:
|
||||
# DEBUG: Confirm entry
|
||||
if getattr(self, '_debug_trade_entry', False) is False:
|
||||
logger.info(f"DEBUG: Processing Trades... Positions: {len(self.uni_positions)}")
|
||||
self._debug_trade_entry = True
|
||||
|
||||
price = Decimal(trade_data['price'])
|
||||
size = Decimal(trade_data['size']) # Amount in Base Token (BNB)
|
||||
|
||||
# Simple Fee Logic:
|
||||
# If trade price is within a position's range, it earns fees.
|
||||
# Fee = Volume * 0.05% (Fee Tier) * MarketShare (Assume 10%)
|
||||
|
||||
fee_tier = Decimal("0.0005") # 0.05%
|
||||
|
||||
# Realistic Market Share Simulation
|
||||
# Assume Pool Depth in active ticks is $5,000,000
|
||||
# Our Position is approx $1,000
|
||||
# Share = 1,000 / 5,000,000 = 0.0002 (0.02%)
|
||||
market_share = Decimal("0.0002")
|
||||
|
||||
import math
|
||||
# Current Tick of the trade
|
||||
try:
|
||||
# price = 1.0001^tick -> tick = log(price) / log(1.0001)
|
||||
# Note: If T0=USDT, Price T0/T1 = 1/Price_USD.
|
||||
# But our TickLower/Upper in Mock are generated based on T0=USDT logic?
|
||||
# clp_manager calculates ticks based on price_from_tick logic.
|
||||
|
||||
# If T0=USDT, T1=WBNB. Price (T0/T1) ~ 0.00116.
|
||||
# Ticks will be negative.
|
||||
# Trade Price is 860.
|
||||
# We need to invert price to get tick if the pool is inverted.
|
||||
# For BNB tests, we know T0=USDT.
|
||||
|
||||
# Invert price for tick calc
|
||||
inv_price = Decimal("1") / price
|
||||
tick = int(math.log(float(inv_price)) / math.log(1.0001))
|
||||
except:
|
||||
tick = 0
|
||||
|
||||
# Iterate all OPEN Uniswap positions
|
||||
for token_id, pos in self.uni_positions.items():
|
||||
# Check Range
|
||||
if pos['tickLower'] <= tick <= pos['tickUpper']:
|
||||
vol_usd = price * size
|
||||
fee_earned = vol_usd * fee_tier * market_share
|
||||
pos['tokensOwed0'] = pos.get('tokensOwed0', 0) + int(fee_earned * 10**18)
|
||||
|
||||
# Debug logging (Disabled for production runs)
|
||||
# if getattr(self, '_debug_fee_log_count', 0) < 10:
|
||||
# logger.info(f"DEBUG: Fee Earned! Tick {tick} inside {pos['tickLower']} <-> {pos['tickUpper']}. Fee: {fee_earned}")
|
||||
# self._debug_fee_log_count = getattr(self, '_debug_fee_log_count', 0) + 1
|
||||
|
||||
else:
|
||||
# Debug logging (Disabled)
|
||||
# if getattr(self, '_debug_tick_log_count', 0) < 10:
|
||||
# logger.info(f"DEBUG: Trade Tick {tick} OUTSIDE {pos['tickLower']} <-> {pos['tickUpper']} (Price: {price})")
|
||||
# self._debug_tick_log_count = getattr(self, '_debug_tick_log_count', 0) + 1
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing trade: {e}")
|
||||
|
||||
def process_transaction(self, tx_data):
|
||||
"""Executes a pending transaction and updates state."""
|
||||
func = tx_data['func']
|
||||
args = tx_data['args']
|
||||
value = tx_data.get('value', 0)
|
||||
contract_addr = tx_data['contract']
|
||||
|
||||
logger.info(f"PROCESSING TX: {func} on {contract_addr} Val: {value}")
|
||||
|
||||
# 1. DEPOSIT (Wrap)
|
||||
if func == "deposit":
|
||||
# Wrap Native -> Wrapped
|
||||
# Assume contract_addr is the wrapped token
|
||||
# In mocks, we map address to symbol
|
||||
# But we don't have the instance here easily, so we guess.
|
||||
# If value > 0, it's a wrap.
|
||||
amount = Decimal(value) / Decimal(10**18)
|
||||
if self.wallet_balances.get("NATIVE", 0) >= amount:
|
||||
self.wallet_balances["NATIVE"] -= amount
|
||||
# Find which token this is.
|
||||
# If it's the WETH/WBNB address
|
||||
target_token = "WBNB" # Default assumption for this test profile
|
||||
if contract_addr == "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": target_token = "WETH"
|
||||
|
||||
self.wallet_balances[target_token] = self.wallet_balances.get(target_token, 0) + amount
|
||||
logger.info(f"Wrapped {amount} NATIVE to {target_token}")
|
||||
else:
|
||||
logger.error("Insufficient NATIVE balance for wrap")
|
||||
|
||||
# 2. MINT (New Position)
|
||||
elif func == "mint":
|
||||
# Params: (token0, token1, fee, tickLower, tickUpper, amount0Desired, amount1Desired, ...)
|
||||
params = args[0]
|
||||
token0 = params[0]
|
||||
token1 = params[1]
|
||||
amount0 = Decimal(params[5]) / Decimal(10**18) # Approx decimals
|
||||
amount1 = Decimal(params[6]) / Decimal(10**18)
|
||||
|
||||
logger.info(f"Minting Position: {amount0} T0, {amount1} T1")
|
||||
|
||||
# Deduct Balances (Simplified: assuming we have enough)
|
||||
# In real mock we should check symbols
|
||||
self.wallet_balances["WBNB"] = max(0, self.wallet_balances.get("WBNB", 0) - amount0)
|
||||
self.wallet_balances["USDT"] = max(0, self.wallet_balances.get("USDT", 0) - amount1)
|
||||
|
||||
# Create Position
|
||||
token_id = self.next_token_id
|
||||
self.next_token_id += 1
|
||||
|
||||
self.uni_positions[token_id] = {
|
||||
"token0": token0,
|
||||
"token1": token1,
|
||||
"tickLower": params[3],
|
||||
"tickUpper": params[4],
|
||||
"liquidity": 1000000 # Dummy liquidity
|
||||
}
|
||||
logger.info(f"Minted TokenID: {token_id}")
|
||||
self.last_minted_token_id = token_id
|
||||
return token_id # Helper return
|
||||
|
||||
# 3. COLLECT (Fees)
|
||||
elif func == "collect":
|
||||
# Params: (params) -> (tokenId, recipient, amount0Max, amount1Max)
|
||||
params = args[0]
|
||||
token_id = params[0]
|
||||
|
||||
# Retrieve accumulated fees
|
||||
if token_id in self.uni_positions:
|
||||
pos = self.uni_positions[token_id]
|
||||
owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18)
|
||||
owed0 = Decimal(pos.get('tokensOwed0', 0)) / Decimal(10**18)
|
||||
|
||||
# Reset
|
||||
pos['tokensOwed1'] = 0
|
||||
pos['tokensOwed0'] = 0
|
||||
|
||||
fee0 = owed0
|
||||
fee1 = owed1
|
||||
else:
|
||||
fee0 = Decimal("0")
|
||||
fee1 = Decimal("0")
|
||||
|
||||
self.wallet_balances["WBNB"] = self.wallet_balances.get("WBNB", 0) + fee0
|
||||
self.wallet_balances["USDT"] = self.wallet_balances.get("USDT", 0) + fee1
|
||||
|
||||
# Calculate USD Value of fees
|
||||
# T0 = USDT, T1 = WBNB
|
||||
# fee0 is USDT, fee1 is WBNB
|
||||
price = self.state.prices.get("BNB", Decimal("0"))
|
||||
usd_val = fee0 + (fee1 * price)
|
||||
|
||||
self.uni_fees_collected += usd_val
|
||||
logger.info(f"Collected Fees for {token_id}: {fee0:.4f} T0 + {fee1:.4f} T1 = ${usd_val:.2f}")
|
||||
|
||||
# 4. SWAP (ExactInputSingle)
|
||||
elif func == "exactInputSingle":
|
||||
# Params: (params) -> struct
|
||||
# struct ExactInputSingleParams {
|
||||
# address tokenIn; address tokenOut; fee; recipient; deadline; amountIn; amountOutMinimum; sqrtPriceLimitX96;
|
||||
# }
|
||||
# Since args[0] is the struct (tuple/list)
|
||||
# We need to guess indices or check ABI.
|
||||
# Standard: tokenIn(0), tokenOut(1), fee(2), recipient(3), deadline(4), amountIn(5), minOut(6)
|
||||
params = args[0]
|
||||
token_in_addr = params[0]
|
||||
token_out_addr = params[1]
|
||||
amount_in_wei = params[5]
|
||||
|
||||
# Map address to symbol
|
||||
# We can't access contract instance here easily, so use known map or iterate
|
||||
sym_map = {
|
||||
"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH",
|
||||
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC",
|
||||
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB",
|
||||
"0x55d398326f99059fF775485246999027B3197955": "USDT"
|
||||
}
|
||||
|
||||
sym_in = sym_map.get(token_in_addr, "UNKNOWN")
|
||||
sym_out = sym_map.get(token_out_addr, "UNKNOWN")
|
||||
|
||||
amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Approx
|
||||
if sym_in in ["USDC", "USDT"]: amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Mock usually 18 dec for simplicity unless specified
|
||||
|
||||
# Price calculation
|
||||
# If swapping Base (WBNB) -> Quote (USDT), Price is ~300
|
||||
# If Quote -> Base, Price is 1/300
|
||||
price = self.prices.get("BNB", Decimal("300"))
|
||||
|
||||
amount_out = 0
|
||||
if sym_in == "WBNB" and sym_out == "USDT":
|
||||
amount_out = amount_in * price
|
||||
elif sym_in == "USDT" and sym_out == "WBNB":
|
||||
amount_out = amount_in / price
|
||||
else:
|
||||
amount_out = amount_in # 1:1 fallback
|
||||
|
||||
self.wallet_balances[sym_in] = max(0, self.wallet_balances.get(sym_in, 0) - amount_in)
|
||||
self.wallet_balances[sym_out] = self.wallet_balances.get(sym_out, 0) + amount_out
|
||||
|
||||
logger.info(f"SWAP: {amount_in:.4f} {sym_in} -> {amount_out:.4f} {sym_out}")
|
||||
|
||||
def match_orders(self):
|
||||
"""Simple order matching against current price."""
|
||||
# In a real backtest, we'd check High/Low of the candle or Orderbook depth.
|
||||
# Here we assume perfect liquidity at current price for simplicity,
|
||||
# or implement simple slippage.
|
||||
pass
|
||||
|
||||
# --- WEB3 MOCKS ---
|
||||
|
||||
class MockContractFunction:
|
||||
def __init__(self, name, parent_contract, state: MockExchangeState):
|
||||
self.name = name
|
||||
self.contract = parent_contract
|
||||
self.state = state
|
||||
self.args = []
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
return self
|
||||
|
||||
def call(self, transaction=None):
|
||||
# SIMULATE READS
|
||||
if self.name == "slot0":
|
||||
# Determine Pair
|
||||
symbol = "BNB" if "BNB" in self.state.prices else "ETH"
|
||||
price = self.state.prices.get(symbol, Decimal("300"))
|
||||
|
||||
# For BNB Chain, T0 is USDT (0x55d), T1 is WBNB (0xbb4)
|
||||
# Price of T0 (USDT) in T1 (WBNB) is 1 / Price
|
||||
if symbol == "BNB":
|
||||
if price > 0:
|
||||
price = Decimal("1") / price
|
||||
else:
|
||||
price = Decimal("0")
|
||||
|
||||
sqrt_px = price.sqrt() * (2**96)
|
||||
|
||||
# Tick
|
||||
import math
|
||||
try:
|
||||
# price = 1.0001^tick
|
||||
tick = int(math.log(float(price)) / math.log(1.0001))
|
||||
except:
|
||||
tick = 0
|
||||
|
||||
return (int(sqrt_px), tick, 0, 0, 0, 0, True)
|
||||
|
||||
if self.name == "positions":
|
||||
token_id = self.args[0]
|
||||
if token_id in self.state.uni_positions:
|
||||
p = self.state.uni_positions[token_id]
|
||||
return (0, "", p['token0'], p['token1'], 500, p['tickLower'], p['tickUpper'], p['liquidity'],
|
||||
0, 0, p.get('tokensOwed0', 0), p.get('tokensOwed1', 0))
|
||||
else:
|
||||
raise Exception("Position not found")
|
||||
|
||||
if self.name == "balanceOf":
|
||||
addr = self.args[0]
|
||||
# Hacky: detect token by contract address
|
||||
symbol = self.contract.symbol_map.get(self.contract.address, "UNKNOWN")
|
||||
return int(self.state.wallet_balances.get(symbol, 0) * (10**self.contract.decimals_val))
|
||||
|
||||
if self.name == "decimals":
|
||||
return self.contract.decimals_val
|
||||
|
||||
if self.name == "symbol":
|
||||
return self.contract.symbol_val
|
||||
|
||||
if self.name == "allowance":
|
||||
return 10**50 # Infinite allowance
|
||||
|
||||
# Pool Methods
|
||||
if self.name == "tickSpacing":
|
||||
return 10
|
||||
if self.name == "token0":
|
||||
# Return USDT for BNB profile (0x55d < 0xbb4)
|
||||
if "BNB" in self.state.prices:
|
||||
return "0x55d398326f99059fF775485246999027B3197955"
|
||||
return "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH
|
||||
|
||||
if self.name == "token1":
|
||||
# Return WBNB for BNB profile
|
||||
if "BNB" in self.state.prices:
|
||||
return "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
|
||||
return "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC
|
||||
|
||||
if self.name == "fee":
|
||||
return 500
|
||||
if self.name == "liquidity":
|
||||
return 1000000000000000000
|
||||
|
||||
return None
|
||||
|
||||
def build_transaction(self, tx_params):
|
||||
# Queue Transaction for Execution
|
||||
self.state.pending_txs.append({
|
||||
"func": self.name,
|
||||
"args": self.args,
|
||||
"contract": self.contract.address,
|
||||
"value": tx_params.get("value", 0)
|
||||
})
|
||||
|
||||
return {"data": "0xMOCK", "to": self.contract.address, "value": tx_params.get("value", 0)}
|
||||
|
||||
def estimate_gas(self, tx_params):
|
||||
return 100000
|
||||
|
||||
class MockContract:
|
||||
def __init__(self, address, abi, state: MockExchangeState):
|
||||
self.address = address
|
||||
self.abi = abi
|
||||
self.state = state
|
||||
self.functions = self
|
||||
|
||||
# Meta for simulation
|
||||
self.symbol_map = {
|
||||
"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH",
|
||||
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC",
|
||||
# BNB Chain
|
||||
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB", # FIXED: Was BNB, but address is WBNB
|
||||
"0x55d398326f99059fF775485246999027B3197955": "USDT"
|
||||
}
|
||||
symbol = self.symbol_map.get(address, "MOCK")
|
||||
is_stable = symbol in ["USDC", "USDT"]
|
||||
self.decimals_val = 18 if not is_stable else 6
|
||||
if symbol == "USDT": self.decimals_val = 18 # BNB USDT is 18
|
||||
self.symbol_val = symbol
|
||||
|
||||
def __getattr__(self, name):
|
||||
return MockContractFunction(name, self, self.state)
|
||||
|
||||
class MockEth:
|
||||
def __init__(self, state: MockExchangeState):
|
||||
self.state = state
|
||||
self.chain_id = 42161
|
||||
self.max_priority_fee = 100000000
|
||||
|
||||
def contract(self, address, abi):
|
||||
return MockContract(address, abi, self.state)
|
||||
|
||||
def get_block(self, block_identifier):
|
||||
return {'baseFeePerGas': 100000000, 'timestamp': self.state.current_time_ms // 1000}
|
||||
|
||||
def get_balance(self, address):
|
||||
# Native balance
|
||||
return int(self.state.wallet_balances.get("NATIVE", 0) * 10**18)
|
||||
|
||||
def get_transaction_count(self, account, block_identifier=None):
|
||||
return 1
|
||||
|
||||
def send_raw_transaction(self, raw_tx):
|
||||
# EXECUTE PENDING TX
|
||||
if self.state.pending_txs:
|
||||
tx_data = self.state.pending_txs.pop(0)
|
||||
res = self.state.process_transaction(tx_data)
|
||||
|
||||
return b'\x00' * 32
|
||||
|
||||
def wait_for_transaction_receipt(self, tx_hash, timeout=120):
|
||||
# MOCK LOGS GENERATION
|
||||
# We assume every tx is a successful Mint for now to test the flow
|
||||
# In a real engine we'd inspect the tx data to determine the event
|
||||
|
||||
# Transfer Topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
|
||||
transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||
# IncreaseLiquidity Topic: 0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde
|
||||
increase_topic = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
|
||||
|
||||
# Use the actual minted ID if available, else dummy
|
||||
real_token_id = getattr(self.state, 'last_minted_token_id', 123456)
|
||||
token_id_hex = hex(real_token_id)[2:].zfill(64)
|
||||
|
||||
# Liquidity + Amount0 + Amount1
|
||||
# 1000 Liquidity, 1 ETH (18 dec), 3000 USDC (6 dec)
|
||||
# 1 ETH = 1e18, 3000 USDC = 3e9
|
||||
data_liq = "00000000000000000000000000000000000000000000000000000000000003e8" # 1000
|
||||
data_amt0 = "0000000000000000000000000000000000000000000000000de0b6b3a7640000" # 1e18
|
||||
data_amt1 = "00000000000000000000000000000000000000000000000000000000b2d05e00" # 3e9
|
||||
|
||||
class Receipt:
|
||||
status = 1
|
||||
blockNumber = 12345
|
||||
logs = [
|
||||
{
|
||||
'topics': [
|
||||
HexBytes(transfer_topic),
|
||||
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # From
|
||||
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # To
|
||||
HexBytes("0x" + token_id_hex) # TokenID
|
||||
],
|
||||
'data': b''
|
||||
},
|
||||
{
|
||||
'topics': [
|
||||
HexBytes(increase_topic)
|
||||
],
|
||||
'data': bytes.fromhex(data_liq + data_amt0 + data_amt1)
|
||||
}
|
||||
]
|
||||
|
||||
return Receipt()
|
||||
|
||||
class MockWeb3:
|
||||
def __init__(self, state: MockExchangeState):
|
||||
self.eth = MockEth(state)
|
||||
self.middleware_onion = type('obj', (object,), {'inject': lambda *args, **kwargs: None})
|
||||
|
||||
def is_connected(self):
|
||||
return True
|
||||
def to_checksum_address(self, addr):
|
||||
return addr
|
||||
def is_address(self, addr):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def to_wei(val, unit):
|
||||
if unit == 'gwei': return int(val * 10**9)
|
||||
if unit == 'ether': return int(val * 10**18)
|
||||
return int(val)
|
||||
|
||||
@staticmethod
|
||||
def from_wei(val, unit):
|
||||
if unit == 'gwei': return Decimal(val) / Decimal(10**9)
|
||||
if unit == 'ether': return Decimal(val) / Decimal(10**18)
|
||||
return Decimal(val)
|
||||
|
||||
@staticmethod
|
||||
def keccak(text=None):
|
||||
return b'\x00'*32 # Dummy
|
||||
|
||||
# --- HYPERLIQUID MOCKS ---
|
||||
|
||||
class MockInfo:
|
||||
def __init__(self, state: MockExchangeState):
|
||||
self.state = state
|
||||
|
||||
def all_mids(self):
|
||||
# Return string prices as per API
|
||||
return {k: str(v) for k, v in self.state.prices.items()}
|
||||
|
||||
def user_state(self, address):
|
||||
positions = []
|
||||
for sym, pos in self.state.hl_positions.items():
|
||||
positions.append({
|
||||
"position": {
|
||||
"coin": sym,
|
||||
"szi": str(pos['size']),
|
||||
"entryPx": str(pos['entry_px']),
|
||||
"unrealizedPnl": str(pos['unrealized_pnl'])
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
"marginSummary": {
|
||||
"accountValue": str(self.state.hl_balances.get("USDC", 0)),
|
||||
"totalMarginUsed": "0",
|
||||
"totalNtlPos": "0",
|
||||
"totalRawUsd": "0"
|
||||
},
|
||||
"assetPositions": positions
|
||||
}
|
||||
|
||||
def open_orders(self, address):
|
||||
return self.state.hl_orders
|
||||
|
||||
def user_fills(self, address):
|
||||
return [] # TODO: Store fills in state
|
||||
|
||||
def l2_snapshot(self, coin):
|
||||
# Generate artificial orderbook around mid price
|
||||
price = self.state.prices.get(coin, Decimal("0"))
|
||||
if price == 0: return {'levels': [[], []]}
|
||||
|
||||
# Spread 0.05%
|
||||
bid = price * Decimal("0.99975")
|
||||
ask = price * Decimal("1.00025")
|
||||
|
||||
return {
|
||||
"levels": [
|
||||
[{"px": str(bid), "sz": "100.0", "n": 1}], # Bids
|
||||
[{"px": str(ask), "sz": "100.0", "n": 1}] # Asks
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class MockExchangeAPI:
|
||||
def __init__(self, state: MockExchangeState):
|
||||
self.state = state
|
||||
|
||||
def order(self, coin, is_buy, sz, limit_px, order_type, reduce_only=False):
|
||||
# Execute immediately for IO/Market, or add to book
|
||||
# Simulating Fill
|
||||
price = Decimal(str(limit_px))
|
||||
size = Decimal(str(sz))
|
||||
cost = price * size
|
||||
|
||||
# Fee (Taker 0.035%)
|
||||
fee = cost * Decimal("0.00035")
|
||||
self.state.hl_fees_paid += fee
|
||||
|
||||
# Update Position
|
||||
if coin not in self.state.hl_positions:
|
||||
self.state.hl_positions[coin] = {'size': Decimal(0), 'entry_px': Decimal(0), 'unrealized_pnl': Decimal(0)}
|
||||
|
||||
pos = self.state.hl_positions[coin]
|
||||
current_size = pos['size']
|
||||
|
||||
# Update Entry Price (Weighted Average)
|
||||
# New Entry = (OldSize * OldEntry + NewSize * NewPrice) / (OldSize + NewSize)
|
||||
signed_size = size if is_buy else -size
|
||||
new_size = current_size + signed_size
|
||||
|
||||
if new_size == 0:
|
||||
pos['entry_px'] = 0
|
||||
elif (current_size > 0 and signed_size > 0) or (current_size < 0 and signed_size < 0):
|
||||
# Increasing position
|
||||
val_old = abs(current_size) * pos['entry_px']
|
||||
val_new = size * price
|
||||
pos['entry_px'] = (val_old + val_new) / abs(new_size)
|
||||
else:
|
||||
# Closing/Reducing - Entry Price doesn't change, PnL is realized
|
||||
# Fraction closed
|
||||
closed_ratio = min(abs(signed_size), abs(current_size)) / abs(current_size)
|
||||
# This logic is simplified, real PnL logic is complex
|
||||
|
||||
pos['size'] = new_size
|
||||
|
||||
logger.info(f"MOCK HL EXEC: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price}. New Size: {new_size}")
|
||||
|
||||
return {"status": "ok", "response": {"data": {"statuses": [{"filled": {"oid": 123}}]}}}
|
||||
|
||||
def cancel(self, coin, oid):
|
||||
self.state.hl_orders = [o for o in self.state.hl_orders if o['oid'] != oid]
|
||||
return {"status": "ok"}
|
||||
25
florida/tests/backtest/run_backtest.py
Normal file
25
florida/tests/backtest/run_backtest.py
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.backtester import Backtester
|
||||
|
||||
def main():
|
||||
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
|
||||
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
|
||||
|
||||
if not os.path.exists(book_file):
|
||||
print(f"Error: Data file not found: {book_file}")
|
||||
return
|
||||
|
||||
print(f"Starting Backtest on {book_file}...")
|
||||
bt = Backtester(book_file, trades_file)
|
||||
bt.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
107
florida/tools/analyze_pool_data.py
Normal file
107
florida/tools/analyze_pool_data.py
Normal 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()
|
||||
171
florida/tools/calculate_market_data.py
Normal file
171
florida/tools/calculate_market_data.py
Normal 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()
|
||||
92
florida/tools/check_aerodrome_pool.py
Normal file
92
florida/tools/check_aerodrome_pool.py
Normal 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()
|
||||
103
florida/tools/check_arbitrum_pool.py
Normal file
103
florida/tools/check_arbitrum_pool.py
Normal file
@ -0,0 +1,103 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from web3 import Web3
|
||||
from dotenv import load_dotenv
|
||||
from decimal import Decimal
|
||||
|
||||
# Add project root
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Load Env
|
||||
load_dotenv()
|
||||
|
||||
RPC_URL = os.environ.get("MAINNET_RPC_URL") # Arbitrum RPC
|
||||
POOL_ADDRESS = "0xC6962004f452bE9203591991D15f6b388e09E8D0" # ARB/WETH 500
|
||||
|
||||
ERC20_ABI = [
|
||||
{"constant": True, "inputs": [], "name": "symbol", "outputs": [{"name": "", "type": "string"}], "payable": False, "stateMutability": "view", "type": "function"},
|
||||
{"constant": True, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": False, "stateMutability": "view", "type": "function"},
|
||||
{"constant": True, "inputs": [{"name": "_owner", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "balance", "type": "uint256"}], "payable": False, "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
|
||||
POOL_ABI = [
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
|
||||
def main():
|
||||
if not RPC_URL:
|
||||
print("Error: MAINNET_RPC_URL not found in .env")
|
||||
return
|
||||
|
||||
w3 = Web3(Web3.HTTPProvider(RPC_URL))
|
||||
if not w3.is_connected():
|
||||
print("Error: Could not connect to RPC")
|
||||
return
|
||||
|
||||
print(f"Connected to Arbitrum: {w3.eth.block_number}")
|
||||
|
||||
pool_contract = w3.eth.contract(address=POOL_ADDRESS, abi=POOL_ABI)
|
||||
|
||||
# 1. Metadata
|
||||
t0_addr = pool_contract.functions.token0().call()
|
||||
t1_addr = pool_contract.functions.token1().call()
|
||||
fee = pool_contract.functions.fee().call()
|
||||
|
||||
t0_contract = w3.eth.contract(address=t0_addr, abi=ERC20_ABI)
|
||||
t1_contract = w3.eth.contract(address=t1_addr, abi=ERC20_ABI)
|
||||
|
||||
t0_sym = t0_contract.functions.symbol().call()
|
||||
t1_sym = t1_contract.functions.symbol().call()
|
||||
t0_dec = t0_contract.functions.decimals().call()
|
||||
t1_dec = t1_contract.functions.decimals().call()
|
||||
|
||||
print(f"\nPool: {t0_sym} / {t1_sym} ({fee/10000}%)")
|
||||
print(f"Token0: {t0_sym} ({t0_addr}) - {t0_dec} dec")
|
||||
print(f"Token1: {t1_sym} ({t1_addr}) - {t1_dec} dec")
|
||||
|
||||
# 2. State
|
||||
slot0 = pool_contract.functions.slot0().call()
|
||||
sqrt_price = slot0[0]
|
||||
tick = slot0[1]
|
||||
liquidity = pool_contract.functions.liquidity().call()
|
||||
|
||||
print(f"\nState:")
|
||||
print(f"Liquidity: {liquidity}")
|
||||
print(f"Tick: {tick}")
|
||||
print(f"SqrtPriceX96: {sqrt_price}")
|
||||
|
||||
# 3. Price Calc
|
||||
# price = (sqrt / 2^96)^2
|
||||
p = (Decimal(sqrt_price) / Decimal(2**96)) ** 2
|
||||
|
||||
# Adjust for decimals: Price = raw_price * 10^(d0 - d1)
|
||||
adj_price = p * (Decimal(10) ** (t0_dec - t1_dec))
|
||||
inv_price = Decimal(1) / adj_price if adj_price > 0 else 0
|
||||
|
||||
print(f"\nPrices:")
|
||||
print(f"1 {t0_sym} = {adj_price:.6f} {t1_sym}")
|
||||
print(f"1 {t1_sym} = {inv_price:.6f} {t0_sym}")
|
||||
|
||||
# 4. TVL Estimation (Balances)
|
||||
t0_bal = t0_contract.functions.balanceOf(POOL_ADDRESS).call()
|
||||
t1_bal = t1_contract.functions.balanceOf(POOL_ADDRESS).call()
|
||||
|
||||
t0_human = Decimal(t0_bal) / Decimal(10**t0_dec)
|
||||
t1_human = Decimal(t1_bal) / Decimal(10**t1_dec)
|
||||
|
||||
print(f"\nTVL (Locked in Contract):")
|
||||
print(f"{t0_human:,.2f} {t0_sym}")
|
||||
print(f"{t1_human:,.2f} {t1_sym}")
|
||||
|
||||
# Assume WETH is approx 3350 (or whatever current market is, we can use slot0 price if one is stable)
|
||||
# If one is USD stable, we can calc total.
|
||||
# ARB / WETH. WETH is ~$3300.
|
||||
# Let's just output raw.
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
florida/tools/debug_aerodrome.py
Normal file
93
florida/tools/debug_aerodrome.py
Normal 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()
|
||||
33
florida/tools/debug_factory.py
Normal file
33
florida/tools/debug_factory.py
Normal 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
104
florida/tools/debug_mint.py
Normal 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)
|
||||
39
florida/tools/debug_ts_mapping.py
Normal file
39
florida/tools/debug_ts_mapping.py
Normal 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
43
florida/tools/debug_tx.py
Normal 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()
|
||||
3
florida/tools/get_checksum.py
Normal file
3
florida/tools/get_checksum.py
Normal file
@ -0,0 +1,3 @@
|
||||
from web3 import Web3
|
||||
addr = "0xbe6d8f0d397708d99755b7857067757F97174d7d"
|
||||
print(Web3.to_checksum_address(addr))
|
||||
66
florida/tools/mint_aerodrome.py
Normal file
66
florida/tools/mint_aerodrome.py
Normal 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()
|
||||
192
florida/tools/pool_scanner.py
Normal file
192
florida/tools/pool_scanner.py
Normal 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
|
||||
54
florida/tools/pool_scanner_config.json
Normal file
54
florida/tools/pool_scanner_config.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
349
florida/tools/universal_swapper.py
Normal file
349
florida/tools/universal_swapper.py
Normal 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
56
gen_sim.py
Normal 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')
|
||||
@ -42,7 +42,9 @@ console_handler.setFormatter(console_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler
|
||||
file_handler = logging.FileHandler(os.path.join(current_dir, 'logs', 'telegram_monitor.log'), encoding='utf-8')
|
||||
from clp_config import TARGET_DEX
|
||||
|
||||
file_handler = logging.FileHandler(os.path.join(current_dir, 'logs', f'{TARGET_DEX}_telegram_monitor.log'), encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.addFilter(UnixMsLogFilter())
|
||||
file_formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
@ -56,9 +58,10 @@ TELEGRAM_ENABLED = os.getenv('TELEGRAM_MONITOR_ENABLED', 'False').lower() == 'tr
|
||||
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
|
||||
TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID', '')
|
||||
TELEGRAM_CHECK_INTERVAL = int(os.getenv('TELEGRAM_CHECK_INTERVAL_SECONDS', '60'))
|
||||
from clp_config import STATUS_FILE
|
||||
TELEGRAM_STATE_FILE = os.getenv('TELEGRAM_STATE_FILE', 'telegram_monitor_state.json')
|
||||
TELEGRAM_TIMEOUT = int(os.getenv('TELEGRAM_TIMEOUT_SECONDS', '10'))
|
||||
HEDGE_STATUS_FILE = os.getenv('HEDGE_STATUS_FILE', 'hedge_status.json')
|
||||
HEDGE_STATUS_FILE = os.getenv('HEDGE_STATUS_FILE', STATUS_FILE)
|
||||
|
||||
class TelegramNotifier:
|
||||
"""Handles Telegram API communication"""
|
||||
|
||||
107
tools/analyze_pool_data.py
Normal file
107
tools/analyze_pool_data.py
Normal 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()
|
||||
171
tools/calculate_market_data.py
Normal file
171
tools/calculate_market_data.py
Normal 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
192
tools/pool_scanner.py
Normal 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
146
tools/record_pool_depth.py
Normal 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
349
tools/universal_swapper.py
Normal 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}")
|
||||
Reference in New Issue
Block a user