refactor: Standardize CLP Manager and Hedger modules & cleanup

- **clp_manager.py**: Renamed from 'uniswap_manager.py'. Standardized logic for Uniswap V3 liquidity provision.
- **clp_hedger.py**: Renamed from 'unified_hedger.py'. Consolidated hedging logic including Delta Calculation fixes, EAC (Edge Avoidance), and Fishing order implementation.
- **Cleanup**: Removed legacy 'aerodrome' folder and tools.
- **Monitoring**: Added Telegram monitoring scripts.
- **Config**: Updated gitignore to exclude market data CSVs.
This commit is contained in:
2025-12-31 11:09:33 +01:00
parent 69fbf389c8
commit b22fdcf741
31 changed files with 3499 additions and 6869 deletions

4
.gitignore vendored
View File

@ -28,3 +28,7 @@ hedge_status.json
# Temporary files
*.tmp
*.bak
# Data
*.csv
florida/market_data/

54
AGENTS.md Normal file
View File

@ -0,0 +1,54 @@
# AGENTS.md - Repository Guidelines for Agentic Coding
## Project Overview
This is a Python blockchain trading system for Uniswap CLP (Concentrated Liquidity Pool) management and hedging on Hyperliquid. The system consists of CLP hedgers, Uniswap managers, and KPI tracking tools.
## Build/Lint/Test Commands
```bash
# Install dependencies
pip install -r requirements.txt
# Run Python with type checking (recommended workflow)
python -m mypy clp_hedger.py
python -m mypy uniswap_manager.py
# Lint with flake8 (optional)
flake8 --max-line-length=100 *.py
# Run specific modules
python clp_hedger.py
python uniswap_manager.py
```
## Code Style Guidelines
### Imports
- Standard library imports first, then third-party, then local imports
- Use `from typing import Optional, Dict, Any, List, Union`
- Import logging_utils with try/except fallback pattern
### Types & Precision
- Use `Decimal` for all financial calculations with `getcontext().prec = 60`
- Function signatures: `def func_name(param: Type) -> ReturnType:`
- Convert to float only for SDK compatibility at the last moment
### Naming Conventions
- Constants: `UPPER_SNAKE_CASE`
- Functions/variables: `lower_snake_case`
- Classes: `PascalCase` (rarely used)
- Loggers: `logger = logging.getLogger("MODULE_NAME")`
### Error Handling
- Use try/except blocks for external dependencies (web3, hyperliquid, logging_utils)
- Log warnings for missing optional dependencies
- Return Decimal("0") for failed conversions, not None
### Logging
- Use structured logging with UnixMsLogFilter for timestamp consistency
- Log to files in `logs/` directory (auto-created)
- Logger names should be uppercase: "HEDGER", "UNISWAP_MANAGER", "KPI_TRACKER"
### Environment
- Load `.env` files from current directory with fallback
- Use absolute paths for cross-directory imports
- Append project root to `sys.path` for local modules

View File

@ -1,70 +0,0 @@
import os
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from dotenv import load_dotenv
from datetime import datetime, timedelta
import json
# Load environment variables from a .env file if it exists
load_dotenv()
def create_and_authorize_agent():
"""
Creates and authorizes a new agent key pair using your main wallet,
following the correct SDK pattern.
"""
# --- STEP 1: Load your main wallet ---
# This is the wallet that holds the funds and has been activated on Hyperliquid.
main_wallet_private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
if not main_wallet_private_key:
main_wallet_private_key = input("Please enter the private key of your MAIN trading wallet: ")
try:
main_account = Account.from_key(main_wallet_private_key)
print(f"\n✅ Loaded main wallet: {main_account.address}")
except Exception as e:
print(f"❌ Error: Invalid main wallet private key provided. Details: {e}")
return
# --- STEP 2: Initialize the Exchange with your MAIN account ---
# This object is used to send the authorization transaction.
exchange = Exchange(main_account, constants.MAINNET_API_URL, account_address=main_account.address)
# --- STEP 3: Create and approve the agent with a specific name ---
# agent name must be between 1 and 16 characters long
agent_name = "my_new_agent"
print(f"\n🔗 Authorizing a new agent named '{agent_name}'...")
try:
# --- FIX: Pass only the agent name string to the function ---
approve_result, agent_private_key = exchange.approve_agent(agent_name)
if approve_result.get("status") == "ok":
# Derive the agent's public address from the key we received
agent_account = Account.from_key(agent_private_key)
print("\n🎉 SUCCESS! Agent has been authorized on-chain.")
print("="*50)
print("SAVE THESE SECURELY. This is what your bot will use.")
print(f" Name: {agent_name}")
print(f" (Agent has a default long-term validity)")
print(f"🔑 Agent Private Key: {agent_private_key}")
print(f"🏠 Agent Address: {agent_account.address}")
print("="*50)
print("\nYou can now set this private key as the AGENT_PRIVATE_KEY environment variable.")
else:
print("\n❌ ERROR: Agent authorization failed.")
print(" Response:", approve_result)
if "Vault may not perform this action" in str(approve_result):
print("\n ACTION REQUIRED: This error means your main wallet (vault) has not been activated. "
"Please go to the Hyperliquid website, connect this wallet, and make a deposit to activate it.")
except Exception as e:
print(f"\nAn unexpected error occurred during authorization: {e}")
if __name__ == "__main__":
create_and_authorize_agent()

View File

@ -1,134 +0,0 @@
import os
import csv
import time
import logging
from decimal import Decimal
from typing import Dict, Optional
# Setup Logger
logger = logging.getLogger("KPI_TRACKER")
logger.setLevel(logging.INFO)
# Basic handler if not already handled by parent
if not logger.handlers:
ch = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - KPI - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
KPI_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', 'kpi_history.csv')
def initialize_kpi_csv():
"""Creates the CSV with headers if it doesn't exist."""
if not os.path.exists(os.path.dirname(KPI_FILE)):
os.makedirs(os.path.dirname(KPI_FILE))
if not os.path.exists(KPI_FILE):
with open(KPI_FILE, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"Timestamp",
"Date",
"NAV_Total_USD",
"Benchmark_HODL_USD",
"Alpha_USD",
"Uniswap_Val_USD",
"Uniswap_Fees_Claimed_USD",
"Uniswap_Fees_Unclaimed_USD",
"Hedge_Equity_USD",
"Hedge_PnL_Realized_USD",
"Hedge_Fees_Paid_USD",
"ETH_Price",
"Fee_Coverage_Ratio"
])
def calculate_hodl_benchmark(initial_eth: Decimal, initial_usdc: Decimal, initial_hedge_usdc: Decimal, current_eth_price: Decimal) -> Decimal:
"""Calculates value if assets were just held (Wallet Assets + Hedge Account Cash)."""
return (initial_eth * current_eth_price) + initial_usdc + initial_hedge_usdc
def log_kpi_snapshot(
snapshot_data: Dict[str, float]
):
"""
Logs a KPI snapshot to CSV.
Expected keys in snapshot_data:
- initial_eth, initial_usdc, initial_hedge_usdc
- current_eth_price
- uniswap_pos_value_usd
- uniswap_fees_claimed_usd
- uniswap_fees_unclaimed_usd
- hedge_equity_usd
- hedge_pnl_realized_usd
- hedge_fees_paid_usd
- wallet_eth_bal, wallet_usdc_bal (Optional, for full NAV)
"""
try:
initialize_kpi_csv()
# Convert all inputs to Decimal for precision
price = Decimal(str(snapshot_data.get('current_eth_price', 0)))
# 1. Benchmark (HODL)
init_eth = Decimal(str(snapshot_data.get('initial_eth', 0)))
init_usdc = Decimal(str(snapshot_data.get('initial_usdc', 0)))
init_hedge = Decimal(str(snapshot_data.get('initial_hedge_usdc', 0)))
benchmark_val = calculate_hodl_benchmark(init_eth, init_usdc, init_hedge, price)
# 2. Strategy NAV (Net Asset Value)
# NAV = Uni Pos + Uni Fees (Claimed+Unclaimed) + Hedge Equity + (Wallet Surplus - Initial Wallet Surplus?)
# For simplicity, we focus on the Strategy PnL components:
# Strategy Val = (Current Uni Pos) + (Claimed Fees) + (Unclaimed Fees) + (Hedge PnL Realized) + (Hedge Unrealized?)
# Note: Hedge Equity usually includes margin. We strictly want "Value Generated".
uni_val = Decimal(str(snapshot_data.get('uniswap_pos_value_usd', 0)))
uni_fees_claimed = Decimal(str(snapshot_data.get('uniswap_fees_claimed_usd', 0)))
uni_fees_unclaimed = Decimal(str(snapshot_data.get('uniswap_fees_unclaimed_usd', 0)))
# Hedge PnL (Realized + Unrealized) is better than Equity for PnL tracking,
# but Equity represents actual redeemable cash. Let's use Equity if provided, or PnL components.
hedge_equity = Decimal(str(snapshot_data.get('hedge_equity_usd', 0)))
hedge_fees = Decimal(str(snapshot_data.get('hedge_fees_paid_usd', 0)))
# Simplified NAV for Strategy Comparison:
# We assume 'hedge_equity' is the Liquidation Value of the hedge account.
# But if we want strictly "Strategy Performance", we usually do:
# Current Value = Uni_Val + Unclaimed + Hedge_Equity
# (Assuming Hedge_Equity started at 0 or we track delta? No, usually Hedge Account has deposit).
# Let's define NAV as Total Current Liquidation Value of Strategy Components
current_nav = uni_val + uni_fees_unclaimed + uni_fees_claimed + hedge_equity
# Alpha
alpha = current_nav - benchmark_val
# Coverage Ratio
total_hedge_cost = abs(hedge_fees) # + funding if available
total_uni_earnings = uni_fees_claimed + uni_fees_unclaimed
if total_hedge_cost > 0:
coverage_ratio = total_uni_earnings / total_hedge_cost
else:
coverage_ratio = Decimal("999.0") # Infinite/Good
# Write
with open(KPI_FILE, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerow([
int(time.time()),
time.strftime('%Y-%m-%d %H:%M:%S'),
f"{current_nav:.2f}",
f"{benchmark_val:.2f}",
f"{alpha:.2f}",
f"{uni_val:.2f}",
f"{uni_fees_claimed:.2f}",
f"{uni_fees_unclaimed:.2f}",
f"{hedge_equity:.2f}",
f"{snapshot_data.get('hedge_pnl_realized_usd', 0):.2f}",
f"{hedge_fees:.2f}",
f"{price:.2f}",
f"{coverage_ratio:.2f}"
])
logger.info(f"📊 KPI Logged | NAV: ${current_nav:.2f} | Benchmark: ${benchmark_val:.2f} | Alpha: ${alpha:.2f}")
except Exception as e:
logger.error(f"Failed to log KPI: {e}")

View File

@ -3,74 +3,137 @@ from decimal import Decimal
# --- GLOBAL SETTINGS ---
# Use environment variables to switch profiles
# Example: TARGET_DEX="UNISWAP_V3"
TARGET_DEX = os.environ.get("TARGET_DEX", "UNISWAP_V3")
STATUS_FILE = os.environ.get("STATUS_FILE", "hedge_status.json")
STATUS_FILE = os.environ.get("STATUS_FILE", f"{TARGET_DEX}_status.json")
# --- DEX PROFILES ---
DEX_PROFILES = {
# --- DEFAULT STRATEGY ---
DEFAULT_STRATEGY = {
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
"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
# Investment Settings
"TARGET_INVESTMENT_AMOUNT": 2000, # Total USD value to deploy into the LP position
"INITIAL_HEDGE_CAPITAL": 1000, # Capital reserved on Hyperliquid for hedging
"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
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"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%)
"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
"MAX_HEDGE_MULTIPLIER": Decimal("1.25"), # Max allowed hedge size relative to calculated target
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.25"), # Base tolerance for delta drift (20%)
"EDGE_PROXIMITY_PCT": Decimal("0.04"), # Distance to range edge where protection activates
"VELOCITY_THRESHOLD_PCT": Decimal("0.0005"), # Minimum price velocity to trigger volatility logic
"POSITION_OPEN_EDGE_PROXIMITY_PCT": Decimal("0.06"), # Safety margin when opening new positions
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
"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
"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)
"FISHING_ORDER_SIZE_PCT": Decimal("0.10"), # Size of individual fishing orders
"FISHING_TIMEOUT_FALLBACK": 30, # Seconds before converting fishing order to taker
# EAC (Enhanced Asymmetric Compensation)
"EAC_NARROW_RANGE_THRESHOLD": Decimal("0.02"), # <2% = narrow
"EAC_MEDIUM_RANGE_THRESHOLD": Decimal("0.05"), # <5% = medium
"EAC_NARROW_BOOST": Decimal("0.15"), # 15% boost
"EAC_MEDIUM_BOOST": Decimal("0.10"), # 10% boost
"EAC_WIDE_BOOST": Decimal("0.075"), # 7.5% boost
}
# --- CLP PROFILES ---
CLP_PROFILES = {
"UNISWAP_V3": {
"NAME": "Uniswap V3 (Arbitrum)",
"COIN_SYMBOL": "ETH", # Asset to hedge on Hyperliquid
"RPC_ENV_VAR": "MAINNET_RPC_URL", # Env var to read RPC from
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "MAINNET_RPC_URL",
"NPM_ADDRESS": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
"ROUTER_ADDRESS": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
"WETH_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
"USDC_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"POOL_FEE": 500, # 0.05%
},
"PANCAKESWAP_V3": {
"NAME": "PancakeSwap V3 (Arbitrum)",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "MAINNET_RPC_URL",
"NPM_ADDRESS": "0x46A15B0b27311cedF172AB29E4f4766fbE7F4364",
"ROUTER_ADDRESS": "0x1b81D678ffb9C0263b24A97847620C99d213eB14",
"WETH_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"USDC_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"TOKEN_A_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500,
},
"UNISWAP_BNB": {
"NAME": "Uniswap V3 (BNB Chain)",
"COIN_SYMBOL": "BNB", # Hedge BNB
"RPC_ENV_VAR": "BNB_RPC_URL", # Needs a BSC RPC
# Uniswap V3 Official Addresses on BNB Chain
"NPM_ADDRESS": "0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613",
"ROUTER_ADDRESS": "0xB971eF87ede563556b2ED4b1C0b0019111Dd35d2",
# Pool: 0x47a90a2d92a8367a91efa1906bfc8c1e05bf10c4
# Tokens: WBNB / USDT
"WETH_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", # WBNB
"USDC_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT (BSC)
"POOL_FEE": 500, # 0.05%
"UNISWAP_wide": {
"NAME": "Uniswap V3 (Arbitrum) - ETH/USDC Wide",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "MAINNET_RPC_URL",
"NPM_ADDRESS": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
"ROUTER_ADDRESS": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
"TOKEN_A_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
"TOKEN_B_ADDRESS": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"POOL_FEE": 500,
"RANGE_WIDTH_PCT": Decimal("0.05"),
"TARGET_INVESTMENT_AMOUNT": 2000,
"MIN_HEDGE_THRESHOLD": Decimal("0.01"),
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.15"),
},
"PANCAKESWAP_BNB": {
"NAME": "PancakeSwap V3 (BNB Chain)",
"NAME": "PancakeSwap V3 (BNB Chain) - BNB/USDT",
"COIN_SYMBOL": "BNB",
"RPC_ENV_VAR": "BNB_RPC_URL",
"NPM_ADDRESS": "0x46A15B0b27311cedF172AB29E4f4766fbE7F4364",
"ROUTER_ADDRESS": "0x1b81D678ffb9C0263b24A97847620C99d213eB14", # Smart Router
# Pool: 0x172fcD41E0913e95784454622d1c3724f546f849 (USDT/WBNB)
"WETH_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", # WBNB
"USDC_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"ROUTER_ADDRESS": "0x1b81D678ffb9C0263b24A97847620C99d213eB14",
"TOKEN_A_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", # WBNB
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100,
"RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.10"),
"EDGE_PROXIMITY_PCT": Decimal("0.015"),
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.1"),
"MIN_TIME_BETWEEN_TRADES": 20,
"ENABLE_FISHING": False,
"FISHING_ORDER_SIZE_PCT": Decimal("0.05"),
"MAKER_ORDER_TIMEOUT": 180,
"FISHING_TIMEOUT_FALLBACK": 60,
},
"WETH_CBBTC_BASE": {
"NAME": "Aerodrome/Uni (Base) - WETH/cbBTC",
"COIN_SYMBOL": "ETH",
"RPC_ENV_VAR": "BASE_RPC_URL",
"NPM_ADDRESS": "0x0000000000000000000000000000000000000000", # Placeholder
"ROUTER_ADDRESS": "0x0000000000000000000000000000000000000000", # Placeholder
"TOKEN_A_ADDRESS": "0x4200000000000000000000000000000000000006", # WETH (Base)
"TOKEN_B_ADDRESS": "0xcbB7C915AB58735a1391B9fE18541b4d8926D412", # cbBTC (Base)
"WRAPPED_NATIVE_ADDRESS": "0x4200000000000000000000000000000000000006",
"POOL_FEE": 3000,
"TARGET_INVESTMENT_AMOUNT": 200,
"VALUE_REFERENCE": "USD",
"RANGE_WIDTH_PCT": Decimal("0.10")
}
}
# --- STRATEGY SETTINGS ---
MONITOR_INTERVAL_SECONDS = 60
CLOSE_POSITION_ENABLED = True
OPEN_POSITION_ENABLED = True
REBALANCE_ON_CLOSE_BELOW_RANGE = True
TARGET_INVESTMENT_VALUE_USDC = 2000
INITIAL_HEDGE_CAPITAL_USDC = 1000
RANGE_WIDTH_PCT = Decimal("0.05") # +/- 5%
SLIPPAGE_TOLERANCE = Decimal("0.02") # 2%
TRANSACTION_TIMEOUT_SECONDS = 30
# --- HELPER TO GET ACTIVE CONFIG ---
def get_current_config():
conf = DEX_PROFILES.get(TARGET_DEX)
if not conf:
raise ValueError(f"Unknown DEX profile: {TARGET_DEX}")
return conf
profile = CLP_PROFILES.get(TARGET_DEX)
if not profile:
raise ValueError(f"Unknown CLP profile: {TARGET_DEX}")
# Merge Default Strategy with Profile (Profile wins)
config = DEFAULT_STRATEGY.copy()
config.update(profile)
return config

File diff suppressed because it is too large Load Diff

View File

@ -117,23 +117,28 @@ WETH9_ABI = json.loads('''
]
''')
from clp_config import (
get_current_config, STATUS_FILE, MONITOR_INTERVAL_SECONDS,
CLOSE_POSITION_ENABLED, OPEN_POSITION_ENABLED,
REBALANCE_ON_CLOSE_BELOW_RANGE, TARGET_INVESTMENT_VALUE_USDC,
INITIAL_HEDGE_CAPITAL_USDC, RANGE_WIDTH_PCT,
SLIPPAGE_TOLERANCE, TRANSACTION_TIMEOUT_SECONDS
)
from clp_config import get_current_config, STATUS_FILE
# --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_config()
# --- CONFIGURATION ---
# --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
OPEN_POSITION_ENABLED = CONFIG.get("OPEN_POSITION_ENABLED", True)
REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", True)
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"))
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
# --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC
WETH_ADDRESS = CONFIG["WETH_ADDRESS"]
USDC_ADDRESS = CONFIG["USDC_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
USDC_ADDRESS = CONFIG["TOKEN_B_ADDRESS"]
POOL_FEE = CONFIG.get("POOL_FEE", 500)
# --- HELPER FUNCTIONS ---
@ -510,7 +515,7 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str, token1: str, amount0: int, amount1: int, tick_lower: int, tick_upper: int) -> Optional[Dict]:
def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str, token1: str, amount0: int, amount1: int, tick_lower: int, tick_upper: int, d0: int, d1: int) -> Optional[Dict]:
"""
Approves tokens and mints a new V3 position.
"""
@ -569,17 +574,25 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
minted_data['amount1'] = int(data[128:192], 16)
if minted_data['token_id']:
# Format for Log
# Assuming Token0=WETH (18), Token1=USDC (6) - should use fetched decimals ideally
# We fetched decimals in main(), but here we can assume or pass them.
# For simplicity, I'll pass them or use defaults since this is specific to this pair
d0, d1 = 18, 6
# Format for Log using actual decimals
fmt_amt0 = Decimal(minted_data['amount0']) / Decimal(10**d0)
fmt_amt1 = Decimal(minted_data['amount1']) / Decimal(10**d1)
logger.info(f"✅ POSITION OPENED | ID: {minted_data['token_id']} | Deposited: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC")
logger.info(f"✅ POSITION OPENED | ID: {minted_data['token_id']} | Deposited: {fmt_amt0:.6f} + {fmt_amt1:.6f}")
# --- VERIFY TICKS ON-CHAIN ---
try:
pos_data = npm_contract.functions.positions(minted_data['token_id']).call()
# pos_data structure: nonce, operator, t0, t1, fee, tickLower, tickUpper, ...
minted_data['tick_lower'] = pos_data[5]
minted_data['tick_upper'] = pos_data[6]
logger.info(f"🔗 Verified Ticks: {minted_data['tick_lower']} <-> {minted_data['tick_upper']}")
except Exception as e:
logger.warning(f"⚠️ Could not verify ticks immediately: {e}")
# Fallback to requested ticks if fetch fails
minted_data['tick_lower'] = tick_lower
minted_data['tick_upper'] = tick_upper
return minted_data
except Exception as e:
@ -587,7 +600,7 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
return None
def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id: int, liquidity: int) -> bool:
def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id: int, liquidity: int, d0: int, d1: int) -> bool:
if liquidity == 0: return True
logger.info(f"📉 Decreasing Liquidity for {token_id}...")
@ -619,11 +632,10 @@ def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id:
amt1 = int(data[128:192], 16)
break
d0, d1 = 18, 6 # Assuming WETH/USDC
fmt_amt0 = Decimal(amt0) / Decimal(10**d0)
fmt_amt1 = Decimal(amt1) / Decimal(10**d1)
logger.info(f"📉 POSITION CLOSED (Liquidity Removed) | ID: {token_id} | Withdrawn: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC")
logger.info(f"📉 POSITION CLOSED (Liquidity Removed) | ID: {token_id} | Withdrawn: {fmt_amt0:.6f} + {fmt_amt1:.6f}")
except Exception as e:
logger.warning(f"Closed but failed to parse details: {e}")
@ -677,7 +689,9 @@ def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
entry.update(extra_data)
if status == "CLOSED":
entry['timestamp_close'] = int(time.time())
now = datetime.now()
entry['timestamp_close'] = int(now.timestamp())
entry['time_close'] = now.strftime("%d.%m.%y %H:%M:%S")
save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}")
@ -760,6 +774,11 @@ def main():
lower_price = price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals'])
upper_price = price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals'])
# --- RANGE DISPLAY ---
# Calculate ranges from ticks for display purposes
real_range_lower = round(float(lower_price), 4)
real_range_upper = round(float(upper_price), 4)
status_msg = "✅ IN RANGE" if in_range else "⚠️ OUT OF RANGE"
# Calculate Unclaimed Fees (Simulation)
@ -831,7 +850,7 @@ def main():
update_position_status(token_id, "CLOSING")
# 1. Remove Liquidity
if decrease_liquidity(w3, npm, account, token_id, pos_details['liquidity']):
if decrease_liquidity(w3, npm, account, token_id, pos_details['liquidity'], pos_details['token0_decimals'], pos_details['token1_decimals']):
# 2. Collect Fees
collect_fees(w3, npm, account, token_id)
update_position_status(token_id, "CLOSED")
@ -920,27 +939,39 @@ 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):
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper)
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
entry_price = float(price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1))
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
# Calculate actual initial value
actual_value = (fmt_amt0 * entry_price) + fmt_amt1
if is_t1_stable:
entry_price = float(price_0_in_1)
actual_value = (fmt_amt0 * entry_price) + fmt_amt1
r_upper = float(price_from_tick(minted['tick_upper'], d0, d1))
r_lower = float(price_from_tick(minted['tick_lower'], d0, d1))
else:
# Inverted (T0 is stable)
entry_price = float(Decimal("1") / price_0_in_1)
actual_value = fmt_amt0 + (fmt_amt1 * entry_price)
r_upper = float(Decimal("1") / price_from_tick(minted['tick_lower'], d0, d1))
r_lower = float(Decimal("1") / price_from_tick(minted['tick_upper'], d0, d1))
# Prepare ordered data with specific rounding
new_position_data = {
"type": "AUTOMATIC", # Will be handled by update_position_status logic if new
"type": "AUTOMATIC",
"target_value": round(float(actual_value), 2),
"entry_price": round(entry_price, 2),
"entry_price": round(entry_price, 4),
"amount0_initial": round(fmt_amt0, 4),
"amount1_initial": round(fmt_amt1, 2),
"amount1_initial": round(fmt_amt1, 4),
"liquidity": str(minted['liquidity']),
"range_upper": round(float(price_from_tick(tick_upper, d0, d1)), 2),
"range_lower": round(float(price_from_tick(tick_lower, d0, d1)), 2),
"timestamp_open": int(time.time())
"range_upper": round(r_upper, 4),
"range_lower": round(r_lower, 4),
"token0_decimals": d0,
"token1_decimals": d1,
"timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
}
update_position_status(minted['token_id'], "OPEN", new_position_data)
@ -957,4 +988,4 @@ def main():
time.sleep(MONITOR_INTERVAL_SECONDS)
if __name__ == "__main__":
main()
main()

View File

@ -0,0 +1,34 @@
# Quick Start Guide
## 1. Test the Monitor
```bash
cd tools
python test_telegram_simple.py
```
## 2. Setup Telegram Bot (Optional)
```bash
python telegram_setup.py
```
## 3. Configure Environment
Copy `tools/.env.example` to `.env` and add:
```bash
TELEGRAM_MONITOR_ENABLED=True
TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
```
## 4. Run Monitor
```bash
python tools/telegram_monitor.py
```
## Files Created
- `tools/telegram_monitor.py` - Main monitoring script
- `tools/telegram_setup.py` - Setup helper
- `tools/test_telegram_simple.py` - Test script
- `tools/.env.example` - Environment template
- `tools/README_TELEGRAM.md` - Full documentation
Monitor checks `hedge_status.json` every 60 seconds for new positions and sends formatted notifications to Telegram.

View File

@ -1,6 +1,8 @@
{
"ui": {
"useAlternateBuffer": true
"useAlternateBuffer": true,
"incrementalRendering": true,
"multiline": true
},
"tools": {
"truncateToolOutputLines": 10000

View File

@ -108,7 +108,7 @@
{
"type": "AUTOMATIC",
"token_id": 6154897,
"status": "OPEN",
"status": "CLOSED",
"target_value": 998.49,
"entry_price": 849.3437,
"amount0_initial": 498.5177,
@ -118,6 +118,101 @@
"range_lower": 836.9493,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767001797
"timestamp_open": 1767001797,
"target_value_end": 1005.08,
"timestamp_close": 1767102435
},
{
"type": "AUTOMATIC",
"token_id": 6161247,
"status": "CLOSED",
"target_value": 995.73,
"entry_price": 862.6115,
"amount0_initial": 495.7523,
"amount1_initial": 0.5796,
"liquidity": "2299317483414760958984",
"range_upper": 875.5579,
"range_lower": 850.0224,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767102508,
"hedge_TotPnL": 0.523778
},
{
"type": "AUTOMATIC",
"token_id": 6162814,
"status": "CLOSED",
"target_value": 995.27,
"entry_price": 860.0,
"amount0_initial": 496.4754,
"amount1_initial": 0.58,
"liquidity": "8300226074094182294178",
"range_upper": 863.616,
"range_lower": 856.384,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767126772,
"hedge_TotPnL": -3.412645,
"hedge_fees_paid": 1.002278,
"timestamp_close": 1767144919
},
{
"type": "AUTOMATIC",
"token_id": 6163606,
"status": "CLOSED",
"target_value": 1000.01,
"entry_price": 862.0807,
"amount0_initial": 500.0068,
"amount1_initial": 0.58,
"liquidity": "8283150435973737393211",
"range_upper": 865.7014,
"range_lower": 858.46,
"timestamp_open": 1767145082,
"timestamp_close": 1767151372
},
{
"type": "AUTOMATIC",
"token_id": 6163987,
"status": "CLOSED",
"target_value": 995.26,
"entry_price": 857.9836,
"amount0_initial": 497.6305,
"amount1_initial": 0.58,
"liquidity": "8313185309628073121633",
"range_upper": 861.5872,
"range_lower": 854.3801,
"timestamp_open": 1767152045,
"timestamp_close": 1767158799
},
{
"type": "AUTOMATIC",
"token_id": 6164411,
"status": "CLOSED",
"target_value": 991.83,
"entry_price": 855.03,
"amount0_initial": 495.9174,
"amount1_initial": 0.58,
"liquidity": "8280770348281556176465",
"range_upper": 858.6211,
"range_lower": 851.4389,
"timestamp_open": 1767158967,
"timestamp_close": 1767163852
},
{
"type": "AUTOMATIC",
"token_id": 6164702,
"status": "OPEN",
"target_value": 981.88,
"entry_price": 846.4517,
"amount0_initial": 490.942,
"amount1_initial": 0.58,
"liquidity": "8220443727732589279738",
"range_upper": 869.8855,
"range_lower": 862.782,
"token0_decimals": 18,
"token1_decimals": 18,
"timestamp_open": 1767164052,
"hedge_TotPnL": -0.026171,
"hedge_fees_paid": 0.097756
}
]

View File

@ -298,6 +298,8 @@
"range_lower": 2827.096,
"token0_decimals": 18,
"token1_decimals": 6,
"timestamp_open": 1766968369
"timestamp_open": 1766968369,
"hedge_TotPnL": -5.078135,
"hedge_fees_paid": 2.029157
}
]

View File

@ -8,49 +8,56 @@ 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
"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
"MONITOR_INTERVAL_SECONDS": 60, # How often the Manager checks for range status
"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
# Investment Settings
"TARGET_INVESTMENT_AMOUNT": 2000, # Total USD value to deploy into the LP position
"INITIAL_HEDGE_CAPITAL": 1000, # Capital reserved on Hyperliquid for hedging
"VALUE_REFERENCE": "USD", # Base currency for all calculations
"WRAPPED_NATIVE_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH/WBNB address
"TARGET_INVESTMENT_AMOUNT": 2000, # Total USD value to deploy into the LP position
"INITIAL_HEDGE_CAPITAL": 1000, # Capital reserved on Hyperliquid for hedging
"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
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"MIN_HEDGE_THRESHOLD": Decimal("0.012"), # Minimum delta change (in coins) required to trigger a trade
"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
"TRANSACTION_TIMEOUT_SECONDS": 30, # Timeout for blockchain transactions
# Hedging Settings
"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%)
"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
"MAX_HEDGE_MULTIPLIER": Decimal("1.25"),# Max allowed hedge size relative to calculated target
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.25"), # Base tolerance for delta drift (20%)
"EDGE_PROXIMITY_PCT": Decimal("0.04"), # Distance to range edge where protection activates
"VELOCITY_THRESHOLD_PCT": Decimal("0.0005"), # Minimum price velocity to trigger volatility logic
"POSITION_OPEN_EDGE_PROXIMITY_PCT": Decimal("0.06"), # Safety margin when opening new positions
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
"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
"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)
"FISHING_ORDER_SIZE_PCT": Decimal("0.10"), # Size of individual fishing orders
"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%)
"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
"MAX_HEDGE_MULTIPLIER": Decimal("1.25"), # Max allowed hedge size relative to calculated target
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.25"), # Base tolerance for delta drift (20%)
"EDGE_PROXIMITY_PCT": Decimal("0.04"), # Distance to range edge where protection activates
"VELOCITY_THRESHOLD_PCT": Decimal("0.0005"), # Minimum price velocity to trigger volatility logic
"POSITION_OPEN_EDGE_PROXIMITY_PCT": Decimal("0.06"), # Safety margin when opening new positions
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions
"LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves
"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
"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)
"FISHING_ORDER_SIZE_PCT": Decimal("0.10"), # Size of individual fishing orders
"FISHING_TIMEOUT_FALLBACK": 30, # Seconds before converting fishing order to taker
# EAC (Enhanced Asymmetric Compensation)
"EAC_NARROW_RANGE_THRESHOLD": Decimal("0.02"), # <2% = narrow
"EAC_MEDIUM_RANGE_THRESHOLD": Decimal("0.05"), # <5% = medium
"EAC_NARROW_BOOST": Decimal("0.15"), # 15% boost
"EAC_MEDIUM_BOOST": Decimal("0.10"), # 10% boost
"EAC_WIDE_BOOST": Decimal("0.075"), # 7.5% boost
}
# --- CLP PROFILES ---
@ -91,9 +98,17 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100,
"RANGE_WIDTH_PCT": Decimal("0.015"),
"RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.05"), # ~$30 for BNB
"MIN_HEDGE_THRESHOLD": Decimal("0.015"),
"BASE_REBALANCE_THRESHOLD_PCT": Decimal("0.10"),
"EDGE_PROXIMITY_PCT": Decimal("0.015"),
"DYNAMIC_THRESHOLD_MULTIPLIER": Decimal("1.1"),
"MIN_TIME_BETWEEN_TRADES": 20,
"ENABLE_FISHING": False,
"FISHING_ORDER_SIZE_PCT": Decimal("0.05"),
"MAKER_ORDER_TIMEOUT": 180,
"FISHING_TIMEOUT_FALLBACK": 60,
},
"WETH_CBBTC_BASE": {
"NAME": "Aerodrome/Uni (Base) - WETH/cbBTC",

View File

@ -45,7 +45,7 @@ class UnixMsLogFilter(logging.Filter):
return True
# Configure Logging
logger = logging.getLogger("UNIFIED_HEDGER")
logger = logging.getLogger("CLP_HEDGER")
logger.setLevel(logging.INFO)
logger.propagate = False # Prevent double logging from root logger
logger.handlers.clear() # Clear existing handlers to prevent duplicates
@ -58,7 +58,7 @@ console_handler.setFormatter(console_fmt)
logger.addHandler(console_handler)
# File Handler
log_file = os.path.join(log_dir, 'unified_hedger.log')
log_file = os.path.join(log_dir, 'clp_hedger.log')
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter())
@ -281,7 +281,7 @@ class UnifiedHedger:
self.startup_time = time.time()
logger.info(f"[UNIFIED] Master Hedger initialized. Agent: {self.account.address}")
logger.info(f"[CLP_HEDGER] Master Hedger initialized. Agent: {self.account.address}")
self._init_coin_configs()
def _init_coin_configs(self):
@ -468,12 +468,17 @@ class UnifiedHedger:
liquidity_val, liquidity_scale
)
# Fix: Use persistent start time from JSON to track all fills
ts_open = position_data.get('timestamp_open')
start_time_ms = int(ts_open * 1000) if ts_open else int(time.time() * 1000)
self.strategies[key] = strat
self.strategy_states[key] = {
"coin": coin_symbol,
"start_time": int(time.time() * 1000),
"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)),
"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')
}
@ -536,6 +541,46 @@ class UnifiedHedger:
except Exception as e:
logger.error(f"Error cancelling order: {e}")
def _update_closed_pnl(self, coin: str):
"""Fetch fills from API and sum closedPnl and fees for active strategies."""
try:
# 1. Identify relevant strategies for this coin
active_strats = [k for k, v in self.strategy_states.items() if v['coin'] == coin]
if not active_strats: return
# 2. Fetch all fills (This is heavy, maybe cache or limit?)
# SDK user_fills returns recent fills.
fills = self.info.user_fills(self.vault_address or self.account.address)
for key in active_strats:
start_time = self.strategy_states[key]['start_time']
total_closed_pnl = Decimal("0")
total_fees = Decimal("0")
for fill in fills:
if fill['coin'] == coin:
# Check timestamp
if fill['time'] >= start_time:
# Sum closedPnl
total_closed_pnl += to_decimal(fill.get('closedPnl', 0))
# Sum fees
total_fees += to_decimal(fill.get('fee', 0))
# Update State
self.strategy_states[key]['hedge_TotPnL'] = total_closed_pnl
self.strategy_states[key]['fees'] = total_fees
# Write to JSON
file_path, token_id = key
update_position_stats(file_path, token_id, {
"hedge_TotPnL": float(total_closed_pnl),
"hedge_fees_paid": float(total_fees)
})
logger.info(f"[PnL] Updated {coin} | Closed PnL: ${total_closed_pnl:.2f} | Fees: ${total_fees:.2f}")
except Exception as e:
logger.error(f"Failed to update closed PnL/Fees for {coin}: {e}")
def run(self):
logger.info("Starting Unified Hedger Loop...")
self.update_coin_decimals()
@ -587,12 +632,15 @@ class UnifiedHedger:
# Map current positions
current_positions = {} # Coin -> Size
current_pnls = {} # Coin -> Unrealized PnL
current_entry_pxs = {} # Coin -> Entry Price (NEW)
for pos in user_state["assetPositions"]:
c = pos["position"]["coin"]
s = to_decimal(pos["position"]["szi"])
u = to_decimal(pos["position"]["unrealizedPnl"])
e = to_decimal(pos["position"]["entryPx"])
current_positions[c] = s
current_pnls[c] = u
current_entry_pxs[c] = e
# 4. Aggregate Targets
# Coin -> { 'target_short': Decimal, 'contributors': int, 'is_at_edge': bool }
@ -692,6 +740,10 @@ class UnifiedHedger:
# 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
@ -715,8 +767,16 @@ class UnifiedHedger:
# Price Check (within buffer)
dist_pct = abs(price - o_price) / price
# Fishing Timeout Check
# 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.")
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.")
self.cancel_order(coin, o_oid)
@ -732,122 +792,122 @@ class UnifiedHedger:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
if order_matched:
continue # Order exists, wait for it
# --- EXECUTION LOGIC ---
if action_needed or force_taker_retry:
bypass_cooldown = False
force_maker = False
if not order_matched:
if action_needed or force_taker_retry:
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")
# 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
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
# 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
elif data.get('contributors', 0) == 0:
if time.time() - self.startup_time > 5:
force_maker = True
logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce")
else:
logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)")
continue # Skip execution for this coin
# Large Hedge Check
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
bypass_cooldown = True
logger.info(f"[WARN] LARGE HEDGE: {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
# Large Hedge Check (Only Force Taker if AT EDGE)
large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0"))
if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False):
bypass_cooldown = True
logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})")
elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker:
# Large hedge but safe zone -> Maker is fine, but maybe log it
logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.")
# Determine Intent
is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL"
last_trade = self.last_trade_times.get(coin, 0)
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
last_trade = self.last_trade_times.get(coin, 0)
if can_trade:
# Get Orderbook for Price
if coin not in l2_snapshots:
l2_snapshots[coin] = self.info.l2_snapshot(coin)
levels = l2_snapshots[coin]['levels']
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 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"
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
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}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
if can_trade:
# Get Orderbook for Price
if coin not in l2_snapshots:
l2_snapshots[coin] = self.info.l2_snapshot(coin)
# Shadow Order
if create_shadow:
shadow_price = bid if is_buy_bool else ask
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
self.shadow_orders.append({
'coin': coin,
'side': side_str,
'price': shadow_price,
'expires_at': time.time() + shadow_timeout
})
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
levels = l2_snapshots[coin]['levels']
if not levels[0] or not levels[1]: continue
# UPDATED: Sleep for API Lag (Phase 5.1)
logger.info("Sleeping 10s to allow position update...")
time.sleep(10)
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 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}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
self.last_trade_times[coin] = time.time()
# Shadow Order
if create_shadow:
shadow_price = bid if is_buy_bool else ask
shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600)
self.shadow_orders.append({
'coin': coin,
'side': side_str,
'price': shadow_price,
'expires_at': time.time() + shadow_timeout
})
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
# UPDATED: Sleep for API Lag (Phase 5.1)
logger.info("Sleeping 10s to allow position update...")
time.sleep(10)
# --- UPDATE CLOSED PnL FROM API ---
self._update_closed_pnl(coin)
else:
# Cooldown log
pass
else:
# Cooldown log
pass
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'])
# 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'])
# --- IDLE LOGGING (Restored Format) ---
# Calculate aggregate Gamma to estimate triggers
@ -876,11 +936,14 @@ class UnifiedHedger:
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0"))
realized = 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:
realized += (s_state['pnl'] - s_state['fees'])
total_pnl = realized + unrealized
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
total_pnl = (closed_pnl_total - fees_total) + unrealized
pnl_pad = " " if unrealized >= 0 else ""
tot_pnl_pad = " " if total_pnl >= 0 else ""

View File

@ -1,11 +1,3 @@
# --- AERODROME MANAGER (BASE CHAIN) ---
# Adapted from uniswap_manager.py for Aerodrome Slipstream on Base.
# Key Differences:
# 1. Base Chain RPC & Tokens (WETH/USDC)
# 2. Aerodrome Slipstream Contract Addresses
# 3. Dynamic Tick Spacing (vs hardcoded 10)
# 4. Separate Status File (aerodrome_status.json)
import os
import sys
import time
@ -19,6 +11,7 @@ from typing import Optional, Dict, Tuple, Any, List
from web3 import Web3
from web3.exceptions import TimeExhausted, ContractLogicError
from web3.middleware import ExtraDataToPOAMiddleware # FIX for Web3.py v6+
from eth_account import Account
from eth_account.signers.local import LocalAccount
from dotenv import load_dotenv
@ -41,18 +34,19 @@ sys.path.append(current_dir)
log_dir = os.path.join(current_dir, 'logs')
os.makedirs(log_dir, exist_ok=True)
# Logger Name changed to avoid conflict
LOGGER_NAME = "AERODROME_MANAGER"
try:
from logging_utils import setup_logging
logger = setup_logging("normal", LOGGER_NAME)
# Assuming setup_logging might handle file logging if configured,
# but to be safe and explicit as requested, we'll add a FileHandler here
# or rely on setup_logging if it supports it.
# Since I don't see setup_logging code, I will manually add a file handler to the logger.
logger = setup_logging("normal", "UNISWAP_MANAGER")
except ImportError:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(LOGGER_NAME)
logger = logging.getLogger("UNISWAP_MANAGER")
# Custom Filter for Millisecond Unix Timestamp
class UnixMsLogFilter(logging.Filter):
@ -61,7 +55,7 @@ class UnixMsLogFilter(logging.Filter):
return True
# Add File Handler
log_file = os.path.join(log_dir, 'aerodrome_manager.log')
log_file = os.path.join(log_dir, 'uniswap_manager.log')
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter())
@ -69,7 +63,8 @@ formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# --- ABIs (Standard V3/Slipstream Compatible) ---
# --- 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"},
@ -84,12 +79,12 @@ NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
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": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
{"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": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "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"}
]
''')
@ -122,25 +117,29 @@ WETH9_ABI = json.loads('''
]
''')
# --- CONFIGURATION (BASE CHAIN / AERODROME) ---
# Aerodrome Slipstream Addresses (Base)
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = "0x827922686190790b37229fd06084350e74485b72"
AERODROME_SWAP_ROUTER_ADDRESS = "0xBE6D8f0d05027F14e266dCC1E844717068f6f296"
from clp_config import get_current_config, STATUS_FILE
# Base Chain Tokens
WETH_ADDRESS = "0x4200000000000000000000000000000000000006"
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
# --- GET ACTIVE DEX CONFIG ---
CONFIG = get_current_config()
STATUS_FILE = "aerodrome_status.json" # Separate status file for Base
MONITOR_INTERVAL_SECONDS = 666
CLOSE_POSITION_ENABLED = True
OPEN_POSITION_ENABLED = True
REBALANCE_ON_CLOSE_BELOW_RANGE = True
TARGET_INVESTMENT_VALUE_USDC = 2000
INITIAL_HEDGE_CAPITAL_USDC = 2000
RANGE_WIDTH_PCT = Decimal("0.02") # +/- 1%
SLIPPAGE_TOLERANCE = Decimal("0.02")
TRANSACTION_TIMEOUT_SECONDS = 30
# --- CONFIGURATION FROM STRATEGY ---
MONITOR_INTERVAL_SECONDS = CONFIG.get("MONITOR_INTERVAL_SECONDS", 60)
CLOSE_POSITION_ENABLED = CONFIG.get("CLOSE_POSITION_ENABLED", True)
OPEN_POSITION_ENABLED = CONFIG.get("OPEN_POSITION_ENABLED", True)
REBALANCE_ON_CLOSE_BELOW_RANGE = CONFIG.get("REBALANCE_ON_CLOSE_BELOW_RANGE", True)
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"))
SLIPPAGE_TOLERANCE = CONFIG.get("SLIPPAGE_TOLERANCE", Decimal("0.02"))
TRANSACTION_TIMEOUT_SECONDS = CONFIG.get("TRANSACTION_TIMEOUT_SECONDS", 30)
# --- CONFIGURATION CONSTANTS ---
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = CONFIG["NPM_ADDRESS"]
UNISWAP_V3_SWAP_ROUTER_ADDRESS = CONFIG["ROUTER_ADDRESS"]
# Arbitrum WETH/USDC (or generic T0/T1)
WETH_ADDRESS = CONFIG["WRAPPED_NATIVE_ADDRESS"]
USDC_ADDRESS = CONFIG["TOKEN_B_ADDRESS"]
POOL_FEE = CONFIG.get("POOL_FEE", 500)
# --- HELPER FUNCTIONS ---
@ -161,13 +160,13 @@ def to_wei_int(value: Decimal, decimals: int) -> int:
return int(value * (Decimal(10) ** decimals))
def get_gas_params(w3: Web3) -> Dict[str, int]:
"""Get dynamic gas parameters for EIP-1559 (Base Chain)."""
"""Get dynamic gas parameters for EIP-1559."""
latest_block = w3.eth.get_block("latest")
base_fee = latest_block['baseFeePerGas']
# Base chain fees are low, but we prioritize
max_priority_fee = w3.eth.max_priority_fee or Web3.to_wei(0.05, 'gwei')
# Priority fee: 0.1 gwei or dynamic
max_priority_fee = w3.eth.max_priority_fee or Web3.to_wei(0.1, 'gwei')
# Max Fee = Base Fee * 1.25 + Priority Fee
# Max Fee = Base Fee * 1.5 + Priority Fee
max_fee = int(base_fee * 1.25) + max_priority_fee
return {
@ -183,26 +182,36 @@ def send_transaction_robust(
gas_limit: Optional[int] = None,
extra_msg: str = ""
) -> Optional[Any]:
"""
Builds, signs, sends, and waits for a transaction with timeout and status check.
"""
try:
# 1. Prepare Params
# Use 'pending' to ensure we get the correct nonce if a tx was just sent/mined
tx_params = {
'from': account.address,
'nonce': w3.eth.get_transaction_count(account.address),
'nonce': w3.eth.get_transaction_count(account.address, 'pending'),
'value': value,
'chainId': w3.eth.chain_id,
}
# 2. Add Gas Params
gas_fees = get_gas_params(w3)
tx_params.update(gas_fees)
# 3. Simulate (Call) & Estimate Gas
try:
# If function call object provided
if hasattr(func_call, 'call'):
func_call.call({'from': account.address, 'value': value})
func_call.call({'from': account.address, 'value': value}) # Safety Dry-Run
estimated_gas = func_call.estimate_gas({'from': account.address, 'value': value})
else:
# Raw transaction construction if func_call is just params dict (rare here)
estimated_gas = 200000
tx_params['gas'] = gas_limit if gas_limit else int(estimated_gas * 1.2)
tx_params['gas'] = gas_limit if gas_limit else int(estimated_gas * 1.2) # 20% buffer
# Build
if hasattr(func_call, 'build_transaction'):
tx = func_call.build_transaction(tx_params)
else:
@ -212,12 +221,17 @@ def send_transaction_robust(
logger.error(f"❌ Simulation/Estimation failed for {extra_msg}: {e}")
return None
# 4. Sign
signed_tx = account.sign_transaction(tx)
# 5. Send
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
logger.info(f"📤 Sent {extra_msg} | Hash: {tx_hash.hex()}")
# 6. Wait for Receipt
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=TRANSACTION_TIMEOUT_SECONDS)
# 7. Verify Status
if receipt.status == 1:
logger.info(f"✅ Executed {extra_msg} | Block: {receipt.blockNumber}")
return receipt
@ -227,15 +241,22 @@ def send_transaction_robust(
except TimeExhausted:
logger.error(f"⌛ Transaction Timeout {extra_msg} - Check Mempool")
# In a full production bot, we would implement gas bumping here.
return None
except Exception as e:
logger.error(f"❌ Transaction Error {extra_msg}: {e}")
return None
def price_from_sqrt_price_x96(sqrt_price_x96: int, token0_decimals: int, token1_decimals: int) -> Decimal:
"""
Returns price of Token0 in terms of Token1.
"""
sqrt_price = Decimal(sqrt_price_x96)
q96 = Decimal(2) ** 96
price = (sqrt_price / q96) ** 2
# Adjust for decimals: Price = (T1 / 10^d1) / (T0 / 10^d0)
# = (T1/T0) * (10^d0 / 10^d1)
adjustment = Decimal(10) ** (token0_decimals - token1_decimals)
return price * adjustment
@ -255,6 +276,7 @@ def get_amounts_for_liquidity(sqrt_ratio_current: int, sqrt_ratio_a: int, sqrt_r
amount1 = 0
Q96 = 1 << 96
# Calculations performed in high-precision integer math (EVM style)
if sqrt_ratio_current <= sqrt_ratio_a:
amount0 = (liquidity * Q96 // sqrt_ratio_a) - (liquidity * Q96 // sqrt_ratio_b)
amount1 = 0
@ -271,6 +293,7 @@ def get_amounts_for_liquidity(sqrt_ratio_current: int, sqrt_ratio_a: int, sqrt_r
def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int):
try:
# Check ownership first to avoid errors? positions() works regardless of owner usually.
position_data = npm_contract.functions.positions(token_id).call()
(nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper, liquidity,
feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data
@ -278,6 +301,7 @@ def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int
token0_contract = w3.eth.contract(address=token0_address, abi=ERC20_ABI)
token1_contract = w3.eth.contract(address=token1_address, abi=ERC20_ABI)
# Multi-call optimization could be used here, but keeping simple for now
token0_symbol = token0_contract.functions.symbol().call()
token1_symbol = token1_contract.functions.symbol().call()
token0_decimals = token0_contract.functions.decimals().call()
@ -309,18 +333,25 @@ def get_pool_dynamic_data(pool_contract) -> Optional[Dict[str, Any]]:
return None
def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_value_token1: Decimal, decimals0, decimals1, sqrt_price_current_x96) -> Tuple[int, int]:
"""
Calculates required token amounts for a target investment value.
Uses precise Decimal math.
"""
sqrt_price_current = get_sqrt_ratio_at_tick(current_tick)
sqrt_price_lower = get_sqrt_ratio_at_tick(tick_lower)
sqrt_price_upper = get_sqrt_ratio_at_tick(tick_upper)
# Price of T0 in T1
price_t0_in_t1 = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1)
# Calculate amounts for a "Test" liquidity amount
L_test = 1 << 128
amt0_test_wei, amt1_test_wei = get_amounts_for_liquidity(sqrt_price_current, sqrt_price_lower, sqrt_price_upper, L_test)
amt0_test = Decimal(amt0_test_wei) / Decimal(10**decimals0)
amt1_test = Decimal(amt1_test_wei) / Decimal(10**decimals1)
# Value in Token1 terms
value_test = (amt0_test * price_t0_in_t1) + amt1_test
if value_test <= 0:
@ -334,6 +365,9 @@ def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_valu
return final_amt0_wei, final_amt1_wei
def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spender_address: str, amount_needed: int) -> bool:
"""
Checks if allowance is sufficient, approves if not.
"""
try:
token_c = w3.eth.contract(address=token_address, abi=ERC20_ABI)
allowance = token_c.functions.allowance(account.address, spender_address).call()
@ -343,9 +377,12 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
logger.info(f"🔓 Approving {token_address} for {spender_address}...")
# Some tokens (USDT) fail if approving from non-zero to non-zero.
# Safe practice: Approve 0 first if allowance > 0, then new amount.
if allowance > 0:
send_transaction_robust(w3, account, token_c.functions.approve(spender_address, 0), extra_msg="Reset Allowance")
# Approve
receipt = send_transaction_robust(
w3, account,
token_c.functions.approve(spender_address, amount_needed),
@ -358,6 +395,9 @@ def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spende
return False
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.
"""
token0 = clean_address(token0)
token1 = clean_address(token1)
token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
@ -366,15 +406,17 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
# Calculate Deficits
deficit0 = max(0, amount0_needed - bal0)
deficit1 = max(0, amount1_needed - bal1)
weth_lower = WETH_ADDRESS.lower()
# Auto-wrap ETH on Base
# --- AUTO WRAP ETH ---
if (deficit0 > 0 and token0.lower() == weth_lower) or (deficit1 > 0 and token1.lower() == weth_lower):
eth_bal = w3.eth.get_balance(account.address)
gas_reserve = Web3.to_wei(0.005, 'ether') # Lower gas reserve for Base
# Keep 0.01 ETH for gas
gas_reserve = Web3.to_wei(0.01, 'ether')
available_eth = max(0, eth_bal - gas_reserve)
wrap_needed = 0
@ -388,6 +430,7 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
weth_c = w3.eth.contract(address=WETH_ADDRESS, abi=WETH9_ABI)
receipt = send_transaction_robust(w3, account, weth_c.functions.deposit(), value=amount_to_wrap, extra_msg="Wrap ETH")
if receipt:
# Refresh Balances
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
deficit0 = max(0, amount0_needed - bal0)
@ -396,16 +439,28 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
if deficit0 == 0 and deficit1 == 0:
return True
# --- SWAP SURPLUS ---
# Smart Swap: Calculate exactly how much we need to swap
# 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
buffer_multiplier = Decimal("1.02")
buffer_multiplier = Decimal("1.02") # 2% buffer for slippage/price moves
if deficit0 > 0 and bal1 > amount1_needed:
# Need T0 (ETH), Have extra T1 (USDC)
# Swap T1 -> T0
# Cost in T1 = Deficit0 * Price(T0 in T1)
cost_in_t1 = Decimal(deficit0) / Decimal(10**d0) * price_0_in_1
# Convert back to T1 Wei and apply buffer
amount_in_needed = int(cost_in_t1 * Decimal(10**d1) * buffer_multiplier)
surplus1 = bal1 - amount1_needed
if surplus1 >= amount_in_needed:
token_in, token_out = token1, token0
amount_in = amount_in_needed
@ -414,10 +469,15 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}")
elif deficit1 > 0 and bal0 > amount0_needed:
# Need T1 (USDC), Have extra T0 (ETH)
# Swap T0 -> T1
# Cost in T0 = Deficit1 / Price(T0 in T1)
if price_0_in_1 > 0:
cost_in_t0 = (Decimal(deficit1) / Decimal(10**d1)) / price_0_in_1
amount_in_needed = int(cost_in_t0 * Decimal(10**d0) * buffer_multiplier)
surplus0 = bal0 - amount0_needed
if surplus0 >= amount_in_needed:
token_in, token_out = token0, token1
amount_in = amount_in_needed
@ -428,41 +488,51 @@ def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount,
if token_in and amount_in > 0:
logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...")
if not ensure_allowance(w3, account, token_in, AERODROME_SWAP_ROUTER_ADDRESS, amount_in):
if not ensure_allowance(w3, account, token_in, UNISWAP_V3_SWAP_ROUTER_ADDRESS, amount_in):
return False
params = (
token_in, token_out, 500, account.address,
token_in, token_out, POOL_FEE, account.address,
int(time.time()) + 120,
amount_in,
0,
0, # amountOutMin (Market swap for rebalance)
0
)
receipt = send_transaction_robust(w3, account, router_contract.functions.exactInputSingle(params), extra_msg="Swap Surplus")
if receipt:
# Final check - Recursive check to ensure we hit target or retry
# But return True/False based on immediate check
bal0 = token0_c.functions.balanceOf(account.address).call()
bal1 = token1_c.functions.balanceOf(account.address).call()
# If we are strictly >= needed, great.
if bal0 >= amount0_needed and bal1 >= amount1_needed:
return True
else:
logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
logger.warning(f"❌ Insufficient funds. T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
logger.warning(f"❌ Insufficient funds (No suitable swap found). T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}")
return False
def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str, token1: str, amount0: int, amount1: int, tick_lower: int, tick_upper: int) -> Optional[Dict]:
logger.info("🚀 Minting new position on Aerodrome...")
def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str, token1: str, amount0: int, amount1: int, tick_lower: int, tick_upper: int, d0: int, d1: int) -> Optional[Dict]:
"""
Approves tokens and mints a new V3 position.
"""
logger.info("🚀 Minting new position...")
# 1. Approve
if not ensure_allowance(w3, account, token0, NONFUNGIBLE_POSITION_MANAGER_ADDRESS, amount0): return None
if not ensure_allowance(w3, account, token1, NONFUNGIBLE_POSITION_MANAGER_ADDRESS, amount1): return None
# 2. Calculate Min Amounts (Slippage Protection)
# Using 0.5% slippage tolerance
amount0_min = int(Decimal(amount0) * (Decimal(1) - SLIPPAGE_TOLERANCE))
amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE))
# 3. Mint
params = (
token0, token1, 500,
token0, token1, POOL_FEE,
tick_lower, tick_upper,
amount0, amount1,
amount0_min, amount1_min,
@ -473,29 +543,56 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position")
if receipt and receipt.status == 1:
# Parse Logs
try:
# Transfer Event (Topic0)
transfer_topic = Web3.keccak(text="Transfer(address,address,uint256)").hex()
# IncreaseLiquidity Event (Topic0)
increase_liq_topic = Web3.keccak(text="IncreaseLiquidity(uint256,uint128,uint256,uint256)").hex()
minted_data = {'token_id': None, 'amount0': 0, 'amount1': 0}
minted_data = {'token_id': None, 'liquidity': 0, 'amount0': 0, 'amount1': 0}
for log in receipt.logs:
topics = [t.hex() for t in log['topics']]
# Capture Token ID
if topics[0] == transfer_topic:
if "0000000000000000000000000000000000000000" in topics[1]:
minted_data['token_id'] = int(topics[3], 16)
# Capture Amounts
if topics[0] == increase_liq_topic:
# decoding data: liquidity(uint128), amount0(uint256), amount1(uint256)
# data is a single hex string, we need to decode it
data = log['data'].hex()
if data.startswith('0x'): data = data[2:]
if data.startswith('0x'):
data = data[2:]
# liquidity is first 32 bytes (padded), amt0 next 32, amt1 next 32
minted_data['liquidity'] = int(data[0:64], 16)
minted_data['amount0'] = int(data[64:128], 16)
minted_data['amount1'] = int(data[128:192], 16)
if minted_data['token_id']:
d0, d1 = 18, 6
# Format for Log using actual decimals
fmt_amt0 = Decimal(minted_data['amount0']) / Decimal(10**d0)
fmt_amt1 = Decimal(minted_data['amount1']) / Decimal(10**d1)
logger.info(f"✅ POSITION OPENED | ID: {minted_data['token_id']} | Deposited: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC")
logger.info(f"✅ POSITION OPENED | ID: {minted_data['token_id']} | Deposited: {fmt_amt0:.6f} + {fmt_amt1:.6f}")
# --- VERIFY TICKS ON-CHAIN ---
try:
pos_data = npm_contract.functions.positions(minted_data['token_id']).call()
# pos_data structure: nonce, operator, t0, t1, fee, tickLower, tickUpper, ...
minted_data['tick_lower'] = pos_data[5]
minted_data['tick_upper'] = pos_data[6]
logger.info(f"🔗 Verified Ticks: {minted_data['tick_lower']} <-> {minted_data['tick_upper']}")
except Exception as e:
logger.warning(f"⚠️ Could not verify ticks immediately: {e}")
# Fallback to requested ticks if fetch fails
minted_data['tick_lower'] = tick_lower
minted_data['tick_upper'] = tick_upper
return minted_data
except Exception as e:
@ -503,7 +600,7 @@ def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str
return None
def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id: int, liquidity: int) -> bool:
def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id: int, liquidity: int, d0: int, d1: int) -> bool:
if liquidity == 0: return True
logger.info(f"📉 Decreasing Liquidity for {token_id}...")
@ -511,7 +608,7 @@ def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id:
params = (
token_id,
liquidity,
0, 0,
0, 0, # amountMin0, amountMin1
int(time.time()) + 180
)
@ -519,86 +616,122 @@ def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id:
if receipt and receipt.status == 1:
try:
# Parse DecreaseLiquidity Event
decrease_topic = Web3.keccak(text="DecreaseLiquidity(uint256,uint128,uint256,uint256)").hex()
amt0, amt1 = 0, 0
for log in receipt.logs:
topics = [t.hex() for t in log['topics']]
if topics[0] == decrease_topic and int(topics[1], 16) == token_id:
data = log['data'].hex()[2:]
amt0 = int(data[64:128], 16)
amt1 = int(data[128:192], 16)
break
if topics[0] == decrease_topic:
# Check tokenID (topic 1)
if int(topics[1], 16) == token_id:
data = log['data'].hex()[2:]
# liquidity (32), amt0 (32), amt1 (32)
amt0 = int(data[64:128], 16)
amt1 = int(data[128:192], 16)
break
d0, d1 = 18, 6
fmt_amt0 = Decimal(amt0) / Decimal(10**d0)
fmt_amt1 = Decimal(amt1) / Decimal(10**d1)
logger.info(f"📉 POSITION CLOSED | ID: {token_id} | Withdrawn: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC")
logger.info(f"📉 POSITION CLOSED (Liquidity Removed) | ID: {token_id} | Withdrawn: {fmt_amt0:.6f} + {fmt_amt1:.6f}")
except Exception as e:
logger.warning(f"Closed but failed to parse details: {e}")
return True
return False
def collect_fees(w3: Web3, npm_contract, account: LocalAccount, token_id: int) -> bool:
logger.info(f"💰 Collecting Fees for {token_id}...")
max_val = 2**128 - 1
params = (token_id, account.address, max_val, max_val)
params = (
token_id,
account.address,
max_val, max_val
)
receipt = send_transaction_robust(w3, account, npm_contract.functions.collect(params), extra_msg=f"Collect Fees {token_id}")
return receipt is not None
# --- STATE MANAGEMENT ---
def load_status_data() -> List[Dict]:
if not os.path.exists(STATUS_FILE): return []
if not os.path.exists(STATUS_FILE):
return []
try:
with open(STATUS_FILE, 'r') as f: return json.load(f)
except: return []
with open(STATUS_FILE, 'r') as f:
return json.load(f)
except:
return []
def save_status_data(data: List[Dict]):
with open(STATUS_FILE, 'w') as f: json.dump(data, f, indent=2)
with open(STATUS_FILE, 'w') as f:
json.dump(data, f, indent=2)
def update_position_status(token_id: int, status: str, extra_data: Dict = {}):
data = load_status_data()
# Find existing or create new
entry = next((item for item in data if item.get('token_id') == token_id), None)
if not entry:
if status in ["OPEN", "PENDING_HEDGE"]:
entry = {"type": "AUTOMATIC", "token_id": token_id}
data.append(entry)
else: return
else:
return # Can't update non-existent position unless opening
entry['status'] = status
entry.update(extra_data)
if status == "CLOSED":
entry['timestamp_close'] = int(time.time())
now = datetime.now()
entry['timestamp_close'] = int(now.timestamp())
entry['time_close'] = now.strftime("%d.%m.%y %H:%M:%S")
save_status_data(data)
logger.info(f"💾 Updated Position {token_id} status to {status}")
# --- MAIN LOOP ---
def main():
logger.info("🔷 Aerodrome Manager (Base Chain) Starting...")
logger.info(f"🔷 {CONFIG['NAME']} Manager V2 Starting...")
load_dotenv(override=True)
rpc_url = os.environ.get("BASE_RPC_URL") # UPDATED ENV VAR
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY")
# Dynamically load the RPC based on DEX Profile
rpc_url = os.environ.get(CONFIG["RPC_ENV_VAR"])
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
if not rpc_url or not private_key:
logger.error("❌ Missing BASE_RPC_URL or MAIN_WALLET_PRIVATE_KEY in .env")
logger.error("❌ Missing RPC or Private Key in .env")
return
w3 = Web3(Web3.HTTPProvider(rpc_url))
if not w3.is_connected():
logger.error("❌ Could not connect to Base RPC")
logger.error("❌ Could not connect to RPC")
return
# FIX: Inject POA middleware for BNB Chain/Polygon/etc. (Web3.py v6+)
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
account = Account.from_key(private_key)
logger.info(f"👤 Wallet: {account.address}")
# Contracts
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)
router = w3.eth.contract(address=clean_address(AERODROME_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
router = w3.eth.contract(address=clean_address(UNISWAP_V3_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI)
while True:
try:
status_data = load_status_data()
open_positions = [p for p in status_data if p.get('status') == 'OPEN']
active_auto_pos = next((p for p in open_positions if p.get('type') == 'AUTOMATIC'), None)
if active_auto_pos:
@ -608,27 +741,65 @@ def main():
if pos_details:
pool_data = get_pool_dynamic_data(pool_c)
current_tick = pool_data['tick']
# Check Range
tick_lower = pos_details['tickLower']
tick_upper = pos_details['tickUpper']
in_range = tick_lower <= current_tick < tick_upper
current_price = price_from_tick(current_tick, pos_details['token0_decimals'], pos_details['token1_decimals'])
lower_price = price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals'])
upper_price = price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals'])
# Calculate Prices for logging
price_0_in_1 = price_from_tick(current_tick, pos_details['token0_decimals'], pos_details['token1_decimals'])
# --- SMART STABLE DETECTION ---
# Determine which token is the "Stable" side to anchor USD value
stable_symbols = ["USDC", "USDT", "DAI", "FDUSD", "USDS"]
is_t1_stable = any(s in pos_details['token1_symbol'].upper() for s in stable_symbols)
is_t0_stable = any(s in pos_details['token0_symbol'].upper() for s in stable_symbols)
if is_t1_stable:
# Standard: T0=Volatile, T1=Stable. Price = T1 per T0
current_price = price_0_in_1
lower_price = price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals'])
upper_price = price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals'])
elif is_t0_stable:
# Inverted: T0=Stable, T1=Volatile. Price = T0 per T1
# We want Price of T1 in terms of T0
current_price = Decimal("1") / price_0_in_1
lower_price = Decimal("1") / price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals'])
upper_price = Decimal("1") / price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals'])
else:
# Fallback to T1
current_price = price_0_in_1
lower_price = price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals'])
upper_price = price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals'])
# --- RANGE DISPLAY ---
# Calculate ranges from ticks for display purposes
real_range_lower = round(float(lower_price), 4)
real_range_upper = round(float(upper_price), 4)
status_msg = "✅ IN RANGE" if in_range else "⚠️ OUT OF RANGE"
# Simulated Fees
# Calculate Unclaimed Fees (Simulation)
unclaimed0, unclaimed1, total_fees_usd = 0, 0, 0
try:
# Call collect with zero address to simulate fee estimation
fees_sim = npm.functions.collect((token_id, "0x0000000000000000000000000000000000000000", 2**128-1, 2**128-1)).call({'from': account.address})
unclaimed0 = to_decimal(fees_sim[0], pos_details['token0_decimals'])
unclaimed1 = to_decimal(fees_sim[1], pos_details['token1_decimals'])
total_fees_usd = (unclaimed0 * current_price) + unclaimed1
except: pass
u0 = to_decimal(fees_sim[0], pos_details['token0_decimals'])
u1 = to_decimal(fees_sim[1], pos_details['token1_decimals'])
if is_t1_stable:
total_fees_usd = (u0 * current_price) + u1
else:
total_fees_usd = u0 + (u1 * current_price)
except Exception as e:
logger.debug(f"Fee simulation failed for {token_id}: {e}")
# PnL Calc
# Calculate Total PnL (Fees + Price Appreciation/Depreciation)
# We need the initial investment value (target_value)
initial_value = Decimal(str(active_auto_pos.get('target_value', 0)))
curr_amt0_wei, curr_amt1_wei = get_amounts_for_liquidity(
pool_data['sqrtPriceX96'],
get_sqrt_ratio_at_tick(tick_lower),
@ -637,26 +808,71 @@ def main():
)
curr_amt0 = Decimal(curr_amt0_wei) / Decimal(10**pos_details['token0_decimals'])
curr_amt1 = Decimal(curr_amt1_wei) / Decimal(10**pos_details['token1_decimals'])
current_pos_value_usd = (curr_amt0 * current_price) + curr_amt1
if is_t1_stable:
current_pos_value_usd = (curr_amt0 * current_price) + curr_amt1
else:
current_pos_value_usd = curr_amt0 + (curr_amt1 * current_price)
pnl_unrealized = current_pos_value_usd - initial_value
total_pnl_usd = pnl_unrealized + total_fees_usd
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}] | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})")
pnl_text = f" | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})"
logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}]{pnl_text}")
# --- KPI LOGGING ---
if log_kpi_snapshot:
snapshot = {
'initial_eth': active_auto_pos.get('amount0_initial', 0),
'initial_usdc': active_auto_pos.get('amount1_initial', 0),
'initial_hedge_usdc': INITIAL_HEDGE_CAPITAL_USDC,
'current_eth_price': float(current_price),
'uniswap_pos_value_usd': float(current_pos_value_usd),
'uniswap_fees_claimed_usd': 0.0, # Not tracked accumulated yet in JSON, using Unclaimed mainly
'uniswap_fees_unclaimed_usd': float(total_fees_usd),
# Hedge Data (from JSON updated by clp_hedger)
'hedge_equity_usd': float(active_auto_pos.get('hedge_equity_usd', 0.0)),
'hedge_pnl_realized_usd': active_auto_pos.get('hedge_pnl_realized', 0.0),
'hedge_fees_paid_usd': active_auto_pos.get('hedge_fees_paid', 0.0)
}
# We use 'target_value' as a proxy for 'Initial Hedge Equity' + 'Initial Uni Val' if strictly tracking strategy?
# For now, let's pass what we have.
# To get 'hedge_equity', we ideally need clp_hedger to write it to JSON.
# Current implementation of kpi_tracker uses 'hedge_equity' in NAV.
# If we leave it 0, NAV will be underreported.
# WORKAROUND: Assume Hedge PnL Realized IS the equity change if we ignore margin.
log_kpi_snapshot(snapshot)
if not in_range and CLOSE_POSITION_ENABLED:
logger.warning(f"🛑 Closing Position {token_id} (Out of Range)")
update_position_status(token_id, "CLOSING")
if decrease_liquidity(w3, npm, account, token_id, pos_details['liquidity']):
# 1. Remove Liquidity
if decrease_liquidity(w3, npm, account, token_id, pos_details['liquidity'], pos_details['token0_decimals'], pos_details['token1_decimals']):
# 2. Collect Fees
collect_fees(w3, npm, account, token_id)
update_position_status(token_id, "CLOSED")
# 3. Optional Rebalance (Sell 50% WETH if fell below)
if REBALANCE_ON_CLOSE_BELOW_RANGE and current_tick < tick_lower:
# Simple rebalance logic here (similar to original check_and_swap surplus logic)
pass
elif OPEN_POSITION_ENABLED:
logger.info("🔍 No active position. Analyzing Aerodrome market...")
logger.info("🔍 No active position. Analyzing market (Fast scan: 37s)...")
token0 = clean_address(WETH_ADDRESS)
token1 = clean_address(USDC_ADDRESS)
fee = 500 # 0.05% fee tier on Aerodrome Slipstream WETH/USDC
# Setup logic for new position
tA = clean_address(WETH_ADDRESS)
tB = clean_address(USDC_ADDRESS)
if tA.lower() < tB.lower():
token0, token1 = tA, tB
else:
token0, token1 = tB, tA
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)
@ -664,39 +880,103 @@ def main():
if pool_data:
tick = pool_data['tick']
# Get Tick Spacing dynamically
tick_spacing = pool_c.functions.tickSpacing().call()
# 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))
# Fetch actual tick spacing from pool
tick_spacing = pool_c.functions.tickSpacing().call()
logger.info(f"📏 Tick Spacing: {tick_spacing}")
tick_lower = (tick - tick_delta) // tick_spacing * tick_spacing
tick_upper = (tick + tick_delta) // tick_spacing * tick_spacing
d0, d1 = 18, 6
investment_val_dec = Decimal(str(TARGET_INVESTMENT_VALUE_USDC))
# 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.
amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_dec, d0, d1, pool_data['sqrtPriceX96'])
# 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":
# ... (Existing MAX logic needs update too, but skipping for brevity as user uses fixed amount)
pass
else:
if is_t1_stable:
# T1 is stable (e.g. ETH/USDC). Target 2000 USD = 2000 Token1.
investment_val_token1 = target_usd
elif is_t0_stable:
# T0 is stable (e.g. USDT/BNB). Target 2000 USD = 2000 Token0.
# We need value in Token1.
# Price 0 in 1 = (BNB per USDT) approx 0.0012
# Val T1 = Val T0 * Price(0 in 1)
investment_val_token1 = target_usd * price_0_in_1
logger.info(f"💱 Converted ${target_usd} -> {investment_val_token1:.4f} {t1_sym} (Price: {price_0_in_1:.6f})")
else:
# Fallback: Assume T1 is Stable (Dangerous but standard default)
logger.warning("⚠️ Could not detect Stable token. Assuming T1 is stable.")
investment_val_token1 = target_usd
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):
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper)
minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper, d0, d1)
if minted:
entry_price = float(price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1))
# Calculate entry price and amounts for JSON compatibility
price_0_in_1 = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0))
fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1))
actual_value = (fmt_amt0 * entry_price) + fmt_amt1
if is_t1_stable:
entry_price = float(price_0_in_1)
actual_value = (fmt_amt0 * entry_price) + fmt_amt1
r_upper = float(price_from_tick(minted['tick_upper'], d0, d1))
r_lower = float(price_from_tick(minted['tick_lower'], d0, d1))
else:
# Inverted (T0 is stable)
entry_price = float(Decimal("1") / price_0_in_1)
actual_value = fmt_amt0 + (fmt_amt1 * entry_price)
r_upper = float(Decimal("1") / price_from_tick(minted['tick_lower'], d0, d1))
r_lower = float(Decimal("1") / price_from_tick(minted['tick_upper'], d0, d1))
# Prepare ordered data with specific rounding
new_position_data = {
"type": "AUTOMATIC",
"target_value": round(float(actual_value), 2),
"entry_price": round(entry_price, 2),
"entry_price": round(entry_price, 4),
"amount0_initial": round(fmt_amt0, 4),
"amount1_initial": round(fmt_amt1, 2),
"range_upper": round(float(price_from_tick(tick_upper, d0, d1)), 2),
"range_lower": round(float(price_from_tick(tick_lower, d0, d1)), 2),
"timestamp_open": int(time.time())
"amount1_initial": round(fmt_amt1, 4),
"liquidity": str(minted['liquidity']),
"range_upper": round(r_upper, 4),
"range_lower": round(r_lower, 4),
"token0_decimals": d0,
"token1_decimals": d1,
"timestamp_open": int(time.time()),
"time_open": datetime.now().strftime("%d.%m.%y %H:%M:%S")
}
update_position_status(minted['token_id'], "OPEN", new_position_data)
# Dynamic Sleep: 37s if no position, else configured interval
sleep_time = MONITOR_INTERVAL_SECONDS if active_auto_pos else 37
time.sleep(sleep_time)
@ -705,7 +985,7 @@ def main():
break
except Exception as e:
logger.error(f"❌ Main Loop Error: {e}")
time.sleep(60)
time.sleep(MONITOR_INTERVAL_SECONDS)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import os
import sys
import json
from dotenv import load_dotenv
from hyperliquid.info import Info
from hyperliquid.utils import constants
# Load env
load_dotenv()
address = os.environ.get("MAIN_WALLET_ADDRESS")
if not address:
print("No address found")
sys.exit(1)
info = Info(constants.MAINNET_API_URL, skip_ws=True)
try:
print(f"Fetching fills for {address}...")
fills = info.user_fills(address)
if fills:
print(f"Found {len(fills)} fills. Inspecting first one:")
print(json.dumps(fills[0], indent=2))
# Check for closedPnl
if 'closedPnl' in fills[0]:
print("'closedPnl' field FOUND!")
else:
print("'closedPnl' field NOT FOUND.")
else:
print("No fills found.")
except Exception as e:
print(f"Error: {e}")

View File

@ -0,0 +1,74 @@
import os
import sys
import json
import time
from decimal import Decimal
from dotenv import load_dotenv
from hyperliquid.info import Info
from hyperliquid.utils import constants
# Load env
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(current_dir)
load_dotenv(os.path.join(current_dir, '.env'))
address = os.environ.get("MAIN_WALLET_ADDRESS")
if not address:
print("No address found")
sys.exit(1)
info = Info(constants.MAINNET_API_URL, skip_ws=True)
# Target Start Time: 2025-12-30 21:32:52 (From user JSON)
START_TIME_MS = 1767126772 * 1000
COIN = "BNB"
print(f"--- DEBUG PnL CHECK ---")
print(f"Address: {address}")
print(f"Coin: {COIN}")
print(f"Start Time: {START_TIME_MS}")
try:
fills = info.user_fills(address)
valid_fills = []
total_closed_pnl = Decimal("0")
total_fees = Decimal("0")
print(f"\n--- FILLS FOUND ---")
print(f"{'Time':<20} | {'Side':<5} | {'Sz':<8} | {'Px':<8} | {'Fee':<8} | {'ClosedPnL':<10}")
print("-" * 80)
for fill in fills:
if fill['coin'] == COIN and fill['time'] >= START_TIME_MS:
valid_fills.append(fill)
fee = Decimal(str(fill['fee']))
pnl = Decimal(str(fill['closedPnl']))
total_closed_pnl += pnl
total_fees += fee
ts_str = time.strftime('%H:%M:%S', time.localtime(fill['time']/1000))
print(f"{ts_str:<20} | {fill['side']:<5} | {fill['sz']:<8} | {fill['px']:<8} | {fee:<8.4f} | {pnl:<10.4f}")
print("-" * 80)
print(f"Count: {len(valid_fills)}")
print(f"Sum Closed PnL (Gross): {total_closed_pnl:.4f}")
print(f"Sum Fees: {total_fees:.4f}")
net_realized = total_closed_pnl - total_fees
print(f"NET REALIZED (Gross - Fees): {net_realized:.4f}")
# Check JSON
json_path = os.path.join(current_dir, "PANCAKESWAP_BNB_status.json")
if os.path.exists(json_path):
with open(json_path, 'r') as f:
data = json.load(f)
last_pos = data[-1]
print(f"\n--- JSON STATE ---")
print(f"hedge_TotPnL: {last_pos.get('hedge_TotPnL')}")
print(f"hedge_fees_paid: {last_pos.get('hedge_fees_paid')}")
except Exception as e:
print(f"Error: {e}")

View File

@ -0,0 +1,142 @@
import json
import os
import math
import sys
from decimal import Decimal, getcontext
from web3 import Web3
from web3.middleware import ExtraDataToPOAMiddleware
from eth_account import Account
from dotenv import load_dotenv
# Add project root to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from clp_config import CLP_PROFILES
# Load Env
load_dotenv()
# Config for PancakeSwap
PROFILE = CLP_PROFILES["PANCAKESWAP_BNB"]
RPC_URL = os.environ.get(PROFILE["RPC_ENV_VAR"])
STATUS_FILE = "PANCAKESWAP_BNB_status.json"
# Minimal ABI for NPM
NPM_ABI = [
{
"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}],
"name": "positions",
"outputs": [
{"internalType": "uint96", "name": "nonce", "type": "uint96"},
{"internalType": "address", "name": "operator", "type": "address"},
{"internalType": "address", "name": "token0", "type": "address"},
{"internalType": "address", "name": "token1", "type": "address"},
{"internalType": "uint24", "name": "fee", "type": "uint24"},
{"internalType": "int24", "name": "tickLower", "type": "int24"},
{"internalType": "int24", "name": "tickUpper", "type": "int24"},
{"internalType": "uint128", "name": "liquidity", "type": "uint128"},
{"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"},
{"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"},
{"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"},
{"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}
],
"stateMutability": "view",
"type": "function"
}
]
def get_price_at_tick(tick):
return 1.0001 ** tick
def fetch_and_fix():
if not RPC_URL:
print("❌ Missing RPC URL in .env")
return
print(f"Connecting to RPC: {RPC_URL}")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
if not w3.is_connected():
print("❌ Failed to connect to Web3")
return
npm = w3.eth.contract(address=PROFILE["NPM_ADDRESS"], abi=NPM_ABI)
with open(STATUS_FILE, 'r') as f:
data = json.load(f)
updated_count = 0
for entry in data:
token_id = entry.get('token_id')
status = entry.get('status')
# We check ALL positions to be safe, or just the problematic ones.
# Let's check any that seem to have suspect data or just refresh all active/recently active.
# The user mentioned 6164702 specifically.
print(f"🔍 Checking Token ID: {token_id} ({status})")
try:
pos = npm.functions.positions(token_id).call()
# Pos structure:
# 0: nonce, 1: operator, 2: token0, 3: token1, 4: fee,
# 5: tickLower, 6: tickUpper, 7: liquidity ...
tick_lower = pos[5]
tick_upper = pos[6]
liquidity = pos[7]
# Calculate Ranges
price_lower = get_price_at_tick(tick_lower)
price_upper = get_price_at_tick(tick_upper)
# Format to 4 decimals
new_lower = round(price_lower, 4)
new_upper = round(price_upper, 4)
old_lower = entry.get('range_lower', 0)
old_upper = entry.get('range_upper', 0)
# Check deviation
if abs(new_lower - old_lower) > 0.1 or abs(new_upper - old_upper) > 0.1:
print(f" ⚠️ Mismatch Found!")
print(f" Old: {old_lower} - {old_upper}")
print(f" New: {new_lower} - {new_upper}")
entry['range_lower'] = new_lower
entry['range_upper'] = new_upper
entry['liquidity'] = str(liquidity)
# Fix Entry Price if it looks wrong (e.g. 0 or way off range)
# If single sided (e.g. 862-869), and spot is 860.
# If we provided only Token0 (BNB), we are selling BNB as it goes UP.
# So we entered 'below' the range.
# If we assume the user just opened it, the 'entry_price' should roughly match
# the current market price or at least be consistent.
# Since we don't know the exact historical price, we can't perfectly fix 'entry_price'
# without event logs.
# HOWEVER, for the bot's logic, 'range_lower' and 'range_upper' are critical for 'in_range' checks.
# 'entry_price' is mostly for PnL est.
# If entry_price is wildly different from range (e.g. 844 vs 862-869), it's confusing.
# Let's see if we can infer something.
# For now, we update ranges as that's the request.
updated_count += 1
else:
print(f" ✅ Data looks solid.")
except Exception as e:
print(f" ❌ Error fetching chain data: {e}")
if updated_count > 0:
with open(STATUS_FILE, 'w') as f:
json.dump(data, f, indent=2)
print(f"💾 Updated {updated_count} entries in {STATUS_FILE}")
else:
print("No updates needed.")
if __name__ == "__main__":
fetch_and_fix()

View File

@ -90,7 +90,7 @@ class CandleRecorder:
print(f"WebSocket Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
print("WebSocket Closed")
print(f"WebSocket Closed: {close_status_code} - {close_msg}")
def on_open(self, ws):
print("WebSocket Connected. Subscribing to allMids...")
@ -105,17 +105,25 @@ class CandleRecorder:
print(f"📂 Output: {self.output_file}")
print("Press Ctrl+C to stop.")
# Start WS in separate thread? No, run_forever is blocking usually.
# But we need to handle Ctrl+C.
self.ws = websocket.WebSocketApp(
WS_URL,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.ws.run_forever()
while self.running:
try:
self.ws = websocket.WebSocketApp(
WS_URL,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
# run_forever blocks until connection is lost
self.ws.run_forever(ping_interval=30, ping_timeout=10)
except Exception as e:
print(f"Critical Error in main loop: {e}")
if self.running:
print("Connection lost. Reconnecting in 5 seconds...")
time.sleep(5)
def signal_handler(sig, frame):
print("\nStopping recorder...")

View File

@ -0,0 +1,207 @@
import argparse
import csv
import os
import time
import json
import threading
import signal
import sys
import websocket
from datetime import datetime
# Setup
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MARKET_DATA_DIR = os.path.join(PROJECT_ROOT, 'market_data')
WS_URL = "wss://api.hyperliquid.xyz/ws"
class MarketDataRecorder:
def __init__(self, coin, file_prefix):
self.coin = coin
self.running = True
self.ws = None
# File paths
self.book_file = f"{file_prefix}_book.csv"
self.trades_file = f"{file_prefix}_trades.csv"
# Buffers
self.book_buffer = []
self.trades_buffer = []
self.buffer_limit = 10
# Ensure dir exists
if not os.path.exists(MARKET_DATA_DIR):
os.makedirs(MARKET_DATA_DIR)
# Init Book CSV
if not os.path.exists(self.book_file):
with open(self.book_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['timestamp_ms', 'local_time', 'bid_px', 'bid_sz', 'ask_px', 'ask_sz', 'mid_price'])
# Init Trades CSV
if not os.path.exists(self.trades_file):
with open(self.trades_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['timestamp_ms', 'local_time', 'price', 'size', 'side', 'hash'])
def on_message(self, ws, message):
try:
recv_ts = time.time()
msg = json.loads(message)
channel = msg.get('channel')
data = msg.get('data', {})
if channel == 'l2Book':
self.process_book(data, recv_ts)
elif channel == 'trades':
self.process_trades(data, recv_ts)
except Exception as e:
print(f"[{datetime.now()}] Error processing: {e}")
def process_book(self, data, recv_ts):
if data.get('coin') != self.coin:
return
levels = data.get('levels', [])
if levels and len(levels) >= 2:
bids = levels[0]
asks = levels[1]
if bids and asks:
# Hyperliquid L2 format: {px: float, sz: float, n: int}
best_bid = bids[0]
best_ask = asks[0]
bid_px = float(best_bid['px'])
bid_sz = float(best_bid['sz'])
ask_px = float(best_ask['px'])
ask_sz = float(best_ask['sz'])
mid = (bid_px + ask_px) / 2
row = [
int(recv_ts * 1000),
datetime.fromtimestamp(recv_ts).strftime('%H:%M:%S.%f')[:-3],
bid_px, bid_sz,
ask_px, ask_sz,
mid
]
self.book_buffer.append(row)
if len(self.book_buffer) >= self.buffer_limit:
self.flush_book()
def process_trades(self, data, recv_ts):
# Data is a list of trades
for trade in data:
if trade.get('coin') != self.coin:
continue
# trade format: {coin, side, px, sz, time, hash}
row = [
int(trade.get('time', int(recv_ts * 1000))),
datetime.fromtimestamp(trade.get('time', 0)/1000 or recv_ts).strftime('%H:%M:%S.%f')[:-3],
float(trade['px']),
float(trade['sz']),
trade['side'],
trade.get('hash', '')
]
self.trades_buffer.append(row)
if len(self.trades_buffer) >= self.buffer_limit:
self.flush_trades()
def flush_book(self):
try:
with open(self.book_file, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(self.book_buffer)
self.book_buffer = []
except Exception as e:
print(f"Error writing book: {e}")
def flush_trades(self):
try:
with open(self.trades_file, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(self.trades_buffer)
# Console Feedback
last_trade = self.trades_buffer[-1] if self.trades_buffer else "N/A"
if last_trade != "N/A":
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔫 Trade: {last_trade[2]} (x{last_trade[3]}) {last_trade[4]}")
self.trades_buffer = []
except Exception as e:
print(f"Error writing trades: {e}")
def on_open(self, ws):
print(f"[{datetime.now()}] Connected! Subscribing to l2Book & trades for {self.coin}...")
# Subscribe to Book
ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l2Book", "coin": self.coin}
}))
# Subscribe to Trades
ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "trades", "coin": self.coin}
}))
def on_error(self, ws, error):
print(f"WebSocket Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
print(f"WebSocket Closed: {close_status_code}")
self.flush_book()
self.flush_trades()
def start(self):
print(f"🔴 RECORDING RAW DATA for {self.coin}")
print(f"📘 Book Data: {self.book_file}")
print(f"🔫 Trades Data: {self.trades_file}")
print("Press Ctrl+C to stop.")
while self.running:
try:
self.ws = websocket.WebSocketApp(
WS_URL,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.ws.run_forever(ping_interval=15, ping_timeout=5)
except Exception as e:
print(f"Critical error: {e}")
if self.running:
print("Reconnecting in 1s...")
time.sleep(1)
def signal_handler(sig, frame):
print("\nStopping recorder...")
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
parser = argparse.ArgumentParser(description="Record RAW Book & Trades from Hyperliquid")
parser.add_argument("--coin", type=str, default="ETH", help="Coin symbol")
parser.add_argument("--output", type=str, help="Base filename (will append _book.csv and _trades.csv)")
args = parser.parse_args()
# Generate filename prefix
if args.output:
# Strip extension if user provided one like "data.csv" -> "data"
base = os.path.splitext(args.output)[0]
else:
date_str = datetime.now().strftime("%Y%m%d")
base = os.path.join(MARKET_DATA_DIR, f"{args.coin}_raw_{date_str}")
recorder = MarketDataRecorder(args.coin, base)
recorder.start()

394
telegram_monitor.py Normal file
View File

@ -0,0 +1,394 @@
#!/usr/bin/env python3
"""
Telegram Monitor for CLP Position Notifications
Monitors hedge_status.json file for new position openings and sends Telegram notifications
"""
import os
import sys
import time
import json
import logging
import hashlib
import requests
from datetime import datetime
from decimal import Decimal
from typing import Optional, Dict, List, Tuple, Any
from dotenv import load_dotenv
# --- SETUP PROJECT PATH ---
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(current_dir)
# --- LOGGING SETUP ---
os.makedirs(os.path.join(current_dir, 'logs'), exist_ok=True)
class UnixMsLogFilter(logging.Filter):
def filter(self, record):
record.unix_ms = int(record.created * 1000)
return True
logger = logging.getLogger("TELEGRAM_MONITOR")
logger.setLevel(logging.INFO)
logger.propagate = False
logger.handlers.clear()
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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')
file_handler.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter())
file_formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# --- CONFIGURATION ---
load_dotenv(os.path.join(current_dir, '.env'))
TELEGRAM_ENABLED = os.getenv('TELEGRAM_MONITOR_ENABLED', 'False').lower() == 'true'
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'))
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')
class TelegramNotifier:
"""Handles Telegram API communication"""
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
self.base_url = f"https://api.telegram.org/bot{bot_token}"
def test_connection(self) -> bool:
"""Test Telegram bot connection"""
try:
url = f"{self.base_url}/getMe"
response = requests.get(url, timeout=TELEGRAM_TIMEOUT)
return response.status_code == 200
except Exception as e:
logger.error(f"Telegram connection test failed: {e}")
return False
def send_message(self, text: str) -> bool:
"""Send message to Telegram chat"""
if not TELEGRAM_ENABLED or not self.bot_token or not self.chat_id:
logger.debug("Telegram notifications disabled or missing credentials")
return False
try:
url = f"{self.base_url}/sendMessage"
payload = {
'chat_id': self.chat_id,
'text': text,
'parse_mode': 'Markdown'
}
response = requests.post(url, json=payload, timeout=TELEGRAM_TIMEOUT)
result = response.json()
if result.get('ok'):
logger.info("Telegram notification sent successfully")
return True
else:
logger.error(f"Telegram API error: {result}")
return False
except Exception as e:
logger.error(f"Failed to send Telegram message: {e}")
return False
def format_position_message(self, last_closed: Optional[Dict], current_open: Dict) -> str:
"""Format position data into readable message"""
lines = ["NEW CLP POSITION DETECTED\n"]
# Previous closed position section
if last_closed:
lines.append("LAST CLOSED POSITION:")
lines.append(f"• Token ID: {last_closed.get('token_id', 'N/A')}")
lines.append(f"• Entry: ${last_closed.get('entry_price', 0):.2f}")
lines.append(f"• Target Value: ${last_closed.get('target_value', 0):.2f}")
# Add duration if timestamps available
if last_closed.get('timestamp_open') and last_closed.get('timestamp_close'):
duration = last_closed['timestamp_close'] - last_closed['timestamp_open']
hours = duration // 3600
minutes = (duration % 3600) // 60
lines.append(f"• Duration: {hours}h {minutes}m")
# Add hedge performance if available
hedge_pnl = last_closed.get('hedge_pnl_realized', 0)
if hedge_pnl != 0:
lines.append(f"• Hedge PnL: ${hedge_pnl:.2f}")
hedge_fees = last_closed.get('hedge_fees_paid', 0)
if hedge_fees != 0:
lines.append(f"• Hedge Fees: ${hedge_fees:.2f}")
else:
lines.append("LAST CLOSED POSITION: None")
lines.append("") # Empty line
# Current opened position section
lines.append("CURRENTLY OPENED:")
lines.append(f"• Token ID: {current_open.get('token_id', 'N/A')}")
lines.append(f"• Entry: ${current_open.get('entry_price', 0):.2f}")
lines.append(f"• Target Value: ${current_open.get('target_value', 0):.2f}")
# Range information
range_lower = current_open.get('range_lower', 0)
range_upper = current_open.get('range_upper', 0)
if range_lower and range_upper:
lines.append(f"• Range: ${range_lower:.2f} - ${range_upper:.2f}")
# Initial amounts
amount0 = current_open.get('amount0_initial', 0)
amount1 = current_open.get('amount1_initial', 0)
if amount0 and amount1:
lines.append(f"• Initial: {amount0:.4f} ETH + {amount1:.2f} USDC")
# Time since opening
if current_open.get('timestamp_open'):
age = int(time.time()) - current_open['timestamp_open']
hours = age // 3600
minutes = (age % 3600) // 60
lines.append(f"• Time: {hours}h {minutes}m ago")
# Performance comparison if we have both positions
if last_closed and current_open:
lines.append("") # Empty line
lines.append("PERFORMANCE COMPARISON:")
# Entry price change
last_entry = Decimal(str(last_closed.get('entry_price', 0)))
curr_entry = Decimal(str(current_open.get('entry_price', 0)))
if last_entry > 0:
entry_change = curr_entry - last_entry
entry_change_pct = (entry_change / last_entry) * 100
sign = "+" if entry_change >= 0 else ""
lines.append(f"• Entry Change: {sign}${entry_change:.2f} ({sign}{entry_change_pct:.2f}%)")
# Value change
last_value = Decimal(str(last_closed.get('target_value', 0)))
curr_value = Decimal(str(current_open.get('target_value', 0)))
if last_value > 0:
value_change = curr_value - last_value
value_change_pct = (value_change / last_value) * 100
sign = "+" if value_change >= 0 else ""
lines.append(f"• Value Change: {sign}${value_change:.2f} ({sign}{value_change_pct:.2f}%)")
# Hedge PnL trend
last_hedge_pnl = last_closed.get('hedge_pnl_realized', 0)
curr_hedge_equity = current_open.get('hedge_equity_usd', 0)
if last_hedge_pnl != 0 and curr_hedge_equity != 0:
hedge_trend = curr_hedge_equity - last_hedge_pnl
sign = "+" if hedge_trend >= 0 else ""
lines.append(f"• Hedge PnL Trend: ${last_hedge_pnl:.2f} -> ${curr_hedge_equity:.2f} ({sign}${hedge_trend:.2f})")
return "\n".join(lines)
class PositionMonitor:
"""Monitors hedge_status.json for changes"""
def __init__(self, json_file_path: str, state_file_path: str):
self.json_file_path = json_file_path
self.state_file_path = state_file_path
self.last_known_data = []
self.last_file_hash = ""
self.state = self.load_state()
def load_state(self) -> Dict[str, Any]:
"""Load monitor state from file"""
try:
if os.path.exists(self.state_file_path):
with open(self.state_file_path, 'r') as f:
return json.load(f)
except Exception as e:
logger.warning(f"Could not load state file: {e}")
return {
"last_known_open_positions": [],
"last_processed_timestamp": 0,
"last_file_hash": ""
}
def save_state(self):
"""Save monitor state to file"""
try:
with open(self.state_file_path, 'w') as f:
json.dump(self.state, f, indent=2)
except Exception as e:
logger.error(f"Could not save state file: {e}")
def get_file_hash(self, data: List[Dict]) -> str:
"""Generate hash of file content to detect changes"""
content = json.dumps(data, sort_keys=True)
return hashlib.md5(content.encode()).hexdigest()
def safe_read_json(self) -> List[Dict]:
"""Safely read JSON file with retry logic"""
attempts = 0
while attempts < 3:
try:
if not os.path.exists(self.json_file_path):
logger.warning(f"JSON file not found: {self.json_file_path}")
return []
with open(self.json_file_path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Attempt {attempts + 1}: Error reading JSON file: {e}")
time.sleep(1)
attempts += 1
logger.error("Failed to read JSON file after 3 attempts")
return []
def extract_notification_data(self, data: List[Dict]) -> Tuple[Optional[Dict], Optional[Dict]]:
"""Extract last closed and current open positions"""
current_open = None
last_closed = None
# Find current open position
for item in data:
if item.get('status') == 'OPEN':
current_open = item
break
# Find most recent closed position
closed_positions = [item for item in data if item.get('status') == 'CLOSED']
if closed_positions:
# Sort by timestamp_open (descending) to get most recent
closed_positions.sort(key=lambda x: x.get('timestamp_open', 0), reverse=True)
last_closed = closed_positions[0]
return last_closed, current_open
def check_for_changes(self) -> bool:
"""Check if there are changes requiring notification"""
current_data = self.safe_read_json()
if not current_data:
return False
# Check if file content actually changed
current_hash = self.get_file_hash(current_data)
if current_hash == self.state.get("last_file_hash", ""):
return False
# Extract positions
last_closed, current_open = self.extract_notification_data(current_data)
if not current_open:
# No open position, nothing to notify about
return False
current_open_id = current_open.get('token_id')
last_known_opens = self.state.get("last_known_open_positions", [])
# Check if this is a new open position
if current_open_id not in last_known_opens:
# New position detected!
self.last_known_data = current_data
return True
return False
def get_notification_data(self) -> Tuple[Optional[Dict], Optional[Dict]]:
"""Get data for notification"""
current_data = self.safe_read_json()
return self.extract_notification_data(current_data)
def update_state(self):
"""Update internal state after notification"""
current_data = self.safe_read_json()
if current_data:
_, current_open = self.extract_notification_data(current_data)
if current_open:
# Update state with current open positions
self.state["last_known_open_positions"] = [current_open.get('token_id')]
self.state["last_processed_timestamp"] = int(time.time())
self.state["last_file_hash"] = self.get_file_hash(current_data)
self.save_state()
def main():
"""Main monitoring loop"""
logger.info("🤖 Telegram Monitor Starting...")
notifier = None
if not TELEGRAM_ENABLED:
logger.info("📵 Telegram notifications disabled (TELEGRAM_MONITOR_ENABLED=False)")
else:
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
logger.error("❌ Telegram enabled but missing BOT_TOKEN or CHAT_ID")
return
# Initialize notifier and test connection
notifier = TelegramNotifier(TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
if not notifier.test_connection():
logger.error("❌ Telegram connection failed - check token and network")
return
logger.info(f"✅ Telegram connection established to chat ID: {TELEGRAM_CHAT_ID}")
# Initialize monitor
monitor = PositionMonitor(HEDGE_STATUS_FILE, TELEGRAM_STATE_FILE)
logger.info(f"Monitoring file: {HEDGE_STATUS_FILE}")
logger.info(f"Check interval: {TELEGRAM_CHECK_INTERVAL} seconds")
logger.info(f"State file: {TELEGRAM_STATE_FILE}")
try:
while True:
try:
if monitor.check_for_changes():
logger.info("New position opening detected!")
if TELEGRAM_ENABLED and notifier:
last_closed, current_open = monitor.get_notification_data()
if current_open:
message = notifier.format_position_message(last_closed, current_open)
success = notifier.send_message(message)
if success:
monitor.update_state()
logger.info(f"Notification sent for position {current_open.get('token_id')}")
else:
logger.error("Failed to send notification")
else:
logger.warning("Position change detected but no open position found")
else:
logger.info("Telegram disabled or notifier not available - skipping notification")
monitor.update_state() # Still update state to avoid loops
else:
logger.debug("No changes detected")
time.sleep(TELEGRAM_CHECK_INTERVAL)
except KeyboardInterrupt:
break
except Exception as e:
logger.error(f"Error in monitoring loop: {e}")
time.sleep(TELEGRAM_CHECK_INTERVAL) # Continue after error
except KeyboardInterrupt:
logger.info("Shutting down Telegram Monitor...")
logger.info("Telegram Monitor stopped")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,7 @@
{
"last_known_open_positions": [
5173016
],
"last_processed_timestamp": 1766572620,
"last_file_hash": "ebd0d929cd0b382919cfc9e5a9523006"
}

96
todo/fishing_orders.md Normal file
View File

@ -0,0 +1,96 @@
# [Feature Name / Analysis Topic] - fishing orders
**Status:** in progress
**Started:** 2025/12/23
**Last Updated:** `[YYYY-MM-DD]`
**Tags:** `[Tag1, Tag2]`
---
## 1. Origin & Context (The "Why")
*This section captures the inception of the idea. What was the problem, and what was the initial vision?*
* **Goal:** create additional reveniue, increase TotPnL,
* **Initial State:** currently we are under the water in total (clp position+fees+ TotPnL if closed hedge) ~$10-20, after 24h (clp position 2000, range +/-1%, fees $5)
* **Trigger:** to make a hedger real delta-zero to clp position
* **Original Hypothesis:** make small reveniue will help with it
---
## 2. Chronological Evolution (The Journey)
*A living diary of the process. Add new entries top-down as the feature evolves. This is the story of "How we got here".*
### 📅 Entry 1: 2025-12-23 - Initial Implementation
* **Objective:** Implement a "Fishing" mechanism to capture Maker rebates and reduce hedge size at break-even prices.
* **Investigation:**
* *Findings:* Identified that when the price is in the "Safe Zone" (near entry), the bot sits idle. This is an opportunity to place resting Maker orders to "fish" for better entries/exits than standard Taker rebalances.
* **Current Implementation:**
* **Logic:** Places a Maker BUY order (`Alo` - Add Liquidity Only) at the `hedge_entry_price` when current market price is above entry.
* **Size:** 10% of the target hedge size (`FISHING_ORDER_SIZE_PCT = 0.10`).
* **Management:** The order is tracked via `self.fishing_oid` and is automatically cancelled before any standard rebalance trade to avoid size conflicts.
* **Decision:** Start with a conservative 10% size and only for closing/reducing the hedge (Buy-to-Close when price is above entry) to validate the "Maker fill" probability without over-exposing.
### 📅 Entry 2: 2025-12-23 - Analysis of Symmetric Fishing (Bottom-Side)
* **Objective:** Analyze the potential of adding a "Sell" fishing order when price is below entry (Symmetric Grid).
* **Investigation:**
* *Scenario:* Price is 1900 (Down). Entry is 2000. Hedger is Short.
* *Logic:* A Limit SELL at 2000 is a valid Maker order (Above current price).
* *Effect:* If filled, it increases the Short position at 2000. Since the neutral target at 2000 is fixed, this results in being slightly "Over-Hedged" (Short Delta) at Entry.
* *Strategic Value:* This creates a "Mean Reversion" or "Grid" effect around the entry.
* *Top Side (Buy at Entry):* Profitable if price drops to Entry and bounces up.
* *Bottom Side (Sell at Entry):* Profitable if price rises to Entry and rejects down.
* *Verdict:* This is beneficial. It helps offset hedging costs (which typically lose money in chop) by actively trading the chop around the entry price.
* **Decision:** Implement the symmetric bottom-side fishing order.
* If `Price < Entry`: Place Maker SELL at Entry.
* If `Price > Entry`: Place Maker BUY at Entry.
### 📅 Entry 3: 2025-12-23 - Implementation of Symmetric Fishing
* **Action Taken:** Modified `clp_hedger.py` to allow fishing orders on both sides of the entry price.
* **Result:**
* When `Price > Entry`: Places Maker **BUY** at Entry (Reduce Short).
* When `Price < Entry`: Places Maker **SELL** at Entry (Increase Short).
* **Safety Features:**
* Added a 0.1% price buffer (`dist_pct > 0.001`) to prevent placing orders too close to the market price, which could result in unintended Taker trades or API rejections for `Alo` orders.
* Maintains the existing 10% size restriction to ensure fishing doesn't drastically warp the delta profile.
---
## 3. Final Technical Solution (The "How")
*The consolidated, final design documentation.*
### 3.1 Architecture & Logic
* **Core Components:** `ScalperHedger` class in `clp_hedger.py`.
* **Data Flow:**
1. Bot checks for active standard rebalance orders. If none exist, it enters "Fishing Mode".
2. Compares `current_mid_price` against the `hedge_entry_price` from the exchange.
3. Determines side: BUY if price is above entry, SELL if price is below entry.
4. Places an `Alo` (Add Liquidity Only) Limit order at the exact `hedge_entry_price`.
* **Configuration:**
* `FISHING_ORDER_SIZE_PCT = 0.10`: Size of the fishing order relative to the current target hedge.
### 3.2 Key Constraints & Edge Cases
* **Safe Zone Only:** Only active when no other orders are pending.
* **Rebalance Priority:** Automatically cancelled before any rebalance trade to avoid over-exposure.
* **Maker Buffer:** Only placed if market price is >0.1% away from entry to ensure Maker status.
---
## 4. Retrospective (The "Cookbook" of Fails & Ups)
*Summary of lessons learned. "Read this before touching this code again."*
### ❌ What Failed (Pitfalls to Avoid)
* *Trap:* [Don't try doing X, because Y will happen.]
* *False Assumption:* [We thought Z was true, but it wasn't.]
### ✅ What Worked (Success Factors)
* *Key Insight:* [The breakthrough was...]
* *Design Pattern:* [This specific pattern proved robust.]
---
## 5. Artifacts & References
* **Affected Files:**
* `[path/to/file.py]`
* **Commits:** `[git hash or description]`
* **External Links:** `[URLs]`

18
tools/.env.example Normal file
View File

@ -0,0 +1,18 @@
# Base Chain Configuration
BASE_RPC_URL=https://mainnet.base.org # or your preferred Base RPC provider
# Wallet Configuration
MAIN_WALLET_PRIVATE_KEY=your_private_key_here
# or
PRIVATE_KEY=your_private_key_here
# Telegram Notifications (Optional)
TELEGRAM_MONITOR_ENABLED=False
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHAT_ID=your_telegram_chat_id
TELEGRAM_CHECK_INTERVAL_SECONDS=60
TELEGRAM_TIMEOUT_SECONDS=10
TELEGRAM_STATE_FILE=telegram_monitor_state.json
# Optional: Custom hedge status file path
HEDGE_STATUS_FILE=hedge_status.json

194
tools/README_TELEGRAM.md Normal file
View File

@ -0,0 +1,194 @@
# CLP Telegram Notification Monitor
## Overview
Standalone Python script that monitors your CLP (Concentrated Liquidity Pool) positions and sends Telegram notifications when new positions are opened.
## Features
- 🔍 **File-based Monitoring**: Watches `hedge_status.json` every 60 seconds
- 📊 **Rich Notifications**: Shows last closed position vs currently opened position
- 🔧 **Zero Integration**: No modifications to existing trading logic required
- 🛡️ **Safe Operation**: Graceful error handling, won't affect trading system
- 📈 **Performance Tracking**: Compares entry prices, values, and hedge PnL
## Setup Instructions
### 1. Install Dependencies
```bash
pip install requests python-dotenv
```
### 2. Create Telegram Bot
1. Open Telegram and search for **@BotFather**
2. Send: `/newbot`
3. Choose a name for your bot (e.g., "CLP Position Monitor")
4. Choose a username (must end with 'bot', e.g., "clp_monitor_bot")
5. Copy the **HTTP API token** from BotFather
### 3. Get Your Chat ID
1. Send any message to your new bot first
2. Run the setup script: `python tools/telegram_setup.py`
3. Choose option "2. Get Chat ID" and enter your bot token
4. Copy your Chat ID
### 4. Configure Environment
Add to your `.env` file:
```bash
# Enable Telegram notifications
TELEGRAM_MONITOR_ENABLED=True
# Your bot credentials
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=123456789
# Optional settings
TELEGRAM_CHECK_INTERVAL_SECONDS=60
TELEGRAM_TIMEOUT_SECONDS=10
TELEGRAM_STATE_FILE=telegram_monitor_state.json
HEDGE_STATUS_FILE=hedge_status.json
```
### 5. Run the Monitor
```bash
python tools/telegram_monitor.py
```
## Usage
### Running as Background Process
**Option 1: Simple Background**
```bash
nohup python tools/telegram_monitor.py > logs/telegram_monitor.log 2>&1 &
```
**Option 2: Screen Session**
```bash
screen -S telegram_monitor
python tools/telegram_monitor.py
# Press Ctrl+A, D to detach
```
**Option 3: Systemd Service** (Linux)
Create `/etc/systemd/system/clp-telegram-monitor.service`:
```ini
[Unit]
Description=CLP Telegram Monitor
After=network.target
[Service]
Type=simple
User=your_username
WorkingDirectory=/path/to/uniswap_auto_clp
ExecStart=/usr/bin/python3 tools/telegram_monitor.py
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
```
Enable and start:
```bash
sudo systemctl enable clp-telegram-monitor
sudo systemctl start clp-telegram-monitor
```
## Message Format Example
When a new position opens, you'll receive:
```
🚀 NEW CLP POSITION DETECTED
📊 LAST CLOSED POSITION:
• Token ID: 5171687
• Entry: $3043.86
• Target Value: $1985.02
• Duration: 2h 14m
• Hedge PnL: -$20.99
• Hedge Fees: $2.30
🔥 CURRENTLY OPENED:
• Token ID: 5173016
• Entry: $2993.72
• Target Value: $1935.04
• Range: $2849.80 - $3140.07
• Initial: 0.3181 ETH + 982.71 USDC
• Time: 2h 15m ago
📈 PERFORMANCE COMPARISON:
• Entry Change: -$50.14 (-1.65%)
• Value Change: -$49.98 (-2.52%)
• Hedge PnL Trend: -$20.99 → $2.75 (+$23.74)
```
## Configuration Options
| Variable | Default | Description |
|----------|----------|-------------|
| `TELEGRAM_MONITOR_ENABLED` | `False` | Enable/disable notifications |
| `TELEGRAM_BOT_TOKEN` | Required | Your bot's HTTP API token |
| `TELEGRAM_CHAT_ID` | Required | Your personal chat ID |
| `TELEGRAM_CHECK_INTERVAL_SECONDS` | `60` | How often to check for changes |
| `TELEGRAM_TIMEOUT_SECONDS` | `10` | Telegram API timeout |
| `TELEGRAM_STATE_FILE` | `telegram_monitor_state.json` | Internal state tracking |
| `HEDGE_STATUS_FILE` | `hedge_status.json` | File to monitor |
## Troubleshooting
### Bot Not Receiving Messages
- Ensure you've sent a message to your bot first
- Check that `TELEGRAM_CHAT_ID` is correct (no quotes)
- Verify bot token has no extra spaces
### No Notifications When Position Opens
- Check `TELEGRAM_MONITOR_ENABLED=True`
- Verify the script is running: `ps aux | grep telegram_monitor`
- Check logs: `tail -f logs/telegram_monitor.log`
- Ensure `hedge_status.json` path is correct
### Telegram API Errors
- Verify bot token format (should have colon: `123:ABC`)
- Check internet connectivity
- Try running setup script to test connection
### File Not Found Errors
- Make sure you're running from project root directory
- Verify `hedge_status.json` exists and has correct permissions
- Check that your trading system is generating the status file
## Security Notes
- **Never share your bot token** - it's like a password
- **Store credentials in `.env`** - never commit to git
- **Limit bot permissions** - only send messages, no admin rights
- **Monitor logs regularly** for any unusual activity
## Files Created
- `tools/telegram_monitor.py` - Main monitoring script
- `tools/telegram_setup.py` - Setup helper script
- `tools/.env.example` - Environment template
- `telegram_monitor_state.json` - Internal state (auto-created)
- `logs/telegram_monitor.log` - Monitor logs (auto-created)
## Integration Notes
This monitor is **completely standalone** and:
- ✅ Does not modify your trading logic
- ✅ Does not interfere with uniswap_manager.py
- ✅ Does not affect clp_hedger.py
- ✅ Safe to run alongside existing processes
- ✅ Can be stopped/started independently
- ✅ Uses minimal system resources
The monitor simply reads the status file that your existing trading system already generates, making it safe and non-intrusive.

394
tools/telegram_monitor.py Normal file
View File

@ -0,0 +1,394 @@
#!/usr/bin/env python3
"""
Telegram Monitor for CLP Position Notifications
Monitors hedge_status.json file for new position openings and sends Telegram notifications
"""
import os
import sys
import time
import json
import logging
import hashlib
import requests
from datetime import datetime
from decimal import Decimal
from typing import Optional, Dict, List, Tuple, Any
from dotenv import load_dotenv
# --- SETUP PROJECT PATH ---
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(current_dir)
# --- LOGGING SETUP ---
os.makedirs(os.path.join(current_dir, 'logs'), exist_ok=True)
class UnixMsLogFilter(logging.Filter):
def filter(self, record):
record.unix_ms = int(record.created * 1000)
return True
logger = logging.getLogger("TELEGRAM_MONITOR")
logger.setLevel(logging.INFO)
logger.propagate = False
logger.handlers.clear()
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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')
file_handler.setLevel(logging.INFO)
file_handler.addFilter(UnixMsLogFilter())
file_formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# --- CONFIGURATION ---
load_dotenv(os.path.join(current_dir, '.env'))
TELEGRAM_ENABLED = os.getenv('TELEGRAM_MONITOR_ENABLED', 'False').lower() == 'true'
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'))
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')
class TelegramNotifier:
"""Handles Telegram API communication"""
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
self.base_url = f"https://api.telegram.org/bot{bot_token}"
def test_connection(self) -> bool:
"""Test Telegram bot connection"""
try:
url = f"{self.base_url}/getMe"
response = requests.get(url, timeout=TELEGRAM_TIMEOUT)
return response.status_code == 200
except Exception as e:
logger.error(f"Telegram connection test failed: {e}")
return False
def send_message(self, text: str) -> bool:
"""Send message to Telegram chat"""
if not TELEGRAM_ENABLED or not self.bot_token or not self.chat_id:
logger.debug("Telegram notifications disabled or missing credentials")
return False
try:
url = f"{self.base_url}/sendMessage"
payload = {
'chat_id': self.chat_id,
'text': text,
'parse_mode': 'Markdown'
}
response = requests.post(url, json=payload, timeout=TELEGRAM_TIMEOUT)
result = response.json()
if result.get('ok'):
logger.info("Telegram notification sent successfully")
return True
else:
logger.error(f"Telegram API error: {result}")
return False
except Exception as e:
logger.error(f"Failed to send Telegram message: {e}")
return False
def format_position_message(self, last_closed: Optional[Dict], current_open: Dict) -> str:
"""Format position data into readable message"""
lines = ["NEW CLP POSITION DETECTED\n"]
# Previous closed position section
if last_closed:
lines.append("LAST CLOSED POSITION:")
lines.append(f"• Token ID: {last_closed.get('token_id', 'N/A')}")
lines.append(f"• Entry: ${last_closed.get('entry_price', 0):.2f}")
lines.append(f"• Target Value: ${last_closed.get('target_value', 0):.2f}")
# Add duration if timestamps available
if last_closed.get('timestamp_open') and last_closed.get('timestamp_close'):
duration = last_closed['timestamp_close'] - last_closed['timestamp_open']
hours = duration // 3600
minutes = (duration % 3600) // 60
lines.append(f"• Duration: {hours}h {minutes}m")
# Add hedge performance if available
hedge_pnl = last_closed.get('hedge_pnl_realized', 0)
if hedge_pnl != 0:
lines.append(f"• Hedge PnL: ${hedge_pnl:.2f}")
hedge_fees = last_closed.get('hedge_fees_paid', 0)
if hedge_fees != 0:
lines.append(f"• Hedge Fees: ${hedge_fees:.2f}")
else:
lines.append("LAST CLOSED POSITION: None")
lines.append("") # Empty line
# Current opened position section
lines.append("CURRENTLY OPENED:")
lines.append(f"• Token ID: {current_open.get('token_id', 'N/A')}")
lines.append(f"• Entry: ${current_open.get('entry_price', 0):.2f}")
lines.append(f"• Target Value: ${current_open.get('target_value', 0):.2f}")
# Range information
range_lower = current_open.get('range_lower', 0)
range_upper = current_open.get('range_upper', 0)
if range_lower and range_upper:
lines.append(f"• Range: ${range_lower:.2f} - ${range_upper:.2f}")
# Initial amounts
amount0 = current_open.get('amount0_initial', 0)
amount1 = current_open.get('amount1_initial', 0)
if amount0 and amount1:
lines.append(f"• Initial: {amount0:.4f} ETH + {amount1:.2f} USDC")
# Time since opening
if current_open.get('timestamp_open'):
age = int(time.time()) - current_open['timestamp_open']
hours = age // 3600
minutes = (age % 3600) // 60
lines.append(f"• Time: {hours}h {minutes}m ago")
# Performance comparison if we have both positions
if last_closed and current_open:
lines.append("") # Empty line
lines.append("PERFORMANCE COMPARISON:")
# Entry price change
last_entry = Decimal(str(last_closed.get('entry_price', 0)))
curr_entry = Decimal(str(current_open.get('entry_price', 0)))
if last_entry > 0:
entry_change = curr_entry - last_entry
entry_change_pct = (entry_change / last_entry) * 100
sign = "+" if entry_change >= 0 else ""
lines.append(f"• Entry Change: {sign}${entry_change:.2f} ({sign}{entry_change_pct:.2f}%)")
# Value change
last_value = Decimal(str(last_closed.get('target_value', 0)))
curr_value = Decimal(str(current_open.get('target_value', 0)))
if last_value > 0:
value_change = curr_value - last_value
value_change_pct = (value_change / last_value) * 100
sign = "+" if value_change >= 0 else ""
lines.append(f"• Value Change: {sign}${value_change:.2f} ({sign}{value_change_pct:.2f}%)")
# Hedge PnL trend
last_hedge_pnl = last_closed.get('hedge_pnl_realized', 0)
curr_hedge_equity = current_open.get('hedge_equity_usd', 0)
if last_hedge_pnl != 0 and curr_hedge_equity != 0:
hedge_trend = curr_hedge_equity - last_hedge_pnl
sign = "+" if hedge_trend >= 0 else ""
lines.append(f"• Hedge PnL Trend: ${last_hedge_pnl:.2f} -> ${curr_hedge_equity:.2f} ({sign}${hedge_trend:.2f})")
return "\n".join(lines)
class PositionMonitor:
"""Monitors hedge_status.json for changes"""
def __init__(self, json_file_path: str, state_file_path: str):
self.json_file_path = json_file_path
self.state_file_path = state_file_path
self.last_known_data = []
self.last_file_hash = ""
self.state = self.load_state()
def load_state(self) -> Dict[str, Any]:
"""Load monitor state from file"""
try:
if os.path.exists(self.state_file_path):
with open(self.state_file_path, 'r') as f:
return json.load(f)
except Exception as e:
logger.warning(f"Could not load state file: {e}")
return {
"last_known_open_positions": [],
"last_processed_timestamp": 0,
"last_file_hash": ""
}
def save_state(self):
"""Save monitor state to file"""
try:
with open(self.state_file_path, 'w') as f:
json.dump(self.state, f, indent=2)
except Exception as e:
logger.error(f"Could not save state file: {e}")
def get_file_hash(self, data: List[Dict]) -> str:
"""Generate hash of file content to detect changes"""
content = json.dumps(data, sort_keys=True)
return hashlib.md5(content.encode()).hexdigest()
def safe_read_json(self) -> List[Dict]:
"""Safely read JSON file with retry logic"""
attempts = 0
while attempts < 3:
try:
if not os.path.exists(self.json_file_path):
logger.warning(f"JSON file not found: {self.json_file_path}")
return []
with open(self.json_file_path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Attempt {attempts + 1}: Error reading JSON file: {e}")
time.sleep(1)
attempts += 1
logger.error("Failed to read JSON file after 3 attempts")
return []
def extract_notification_data(self, data: List[Dict]) -> Tuple[Optional[Dict], Optional[Dict]]:
"""Extract last closed and current open positions"""
current_open = None
last_closed = None
# Find current open position
for item in data:
if item.get('status') == 'OPEN':
current_open = item
break
# Find most recent closed position
closed_positions = [item for item in data if item.get('status') == 'CLOSED']
if closed_positions:
# Sort by timestamp_open (descending) to get most recent
closed_positions.sort(key=lambda x: x.get('timestamp_open', 0), reverse=True)
last_closed = closed_positions[0]
return last_closed, current_open
def check_for_changes(self) -> bool:
"""Check if there are changes requiring notification"""
current_data = self.safe_read_json()
if not current_data:
return False
# Check if file content actually changed
current_hash = self.get_file_hash(current_data)
if current_hash == self.state.get("last_file_hash", ""):
return False
# Extract positions
last_closed, current_open = self.extract_notification_data(current_data)
if not current_open:
# No open position, nothing to notify about
return False
current_open_id = current_open.get('token_id')
last_known_opens = self.state.get("last_known_open_positions", [])
# Check if this is a new open position
if current_open_id not in last_known_opens:
# New position detected!
self.last_known_data = current_data
return True
return False
def get_notification_data(self) -> Tuple[Optional[Dict], Optional[Dict]]:
"""Get data for notification"""
current_data = self.safe_read_json()
return self.extract_notification_data(current_data)
def update_state(self):
"""Update internal state after notification"""
current_data = self.safe_read_json()
if current_data:
_, current_open = self.extract_notification_data(current_data)
if current_open:
# Update state with current open positions
self.state["last_known_open_positions"] = [current_open.get('token_id')]
self.state["last_processed_timestamp"] = int(time.time())
self.state["last_file_hash"] = self.get_file_hash(current_data)
self.save_state()
def main():
"""Main monitoring loop"""
logger.info("🤖 Telegram Monitor Starting...")
notifier = None
if not TELEGRAM_ENABLED:
logger.info("📵 Telegram notifications disabled (TELEGRAM_MONITOR_ENABLED=False)")
else:
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
logger.error("❌ Telegram enabled but missing BOT_TOKEN or CHAT_ID")
return
# Initialize notifier and test connection
notifier = TelegramNotifier(TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
if not notifier.test_connection():
logger.error("❌ Telegram connection failed - check token and network")
return
logger.info(f"✅ Telegram connection established to chat ID: {TELEGRAM_CHAT_ID}")
# Initialize monitor
monitor = PositionMonitor(HEDGE_STATUS_FILE, TELEGRAM_STATE_FILE)
logger.info(f"Monitoring file: {HEDGE_STATUS_FILE}")
logger.info(f"Check interval: {TELEGRAM_CHECK_INTERVAL} seconds")
logger.info(f"State file: {TELEGRAM_STATE_FILE}")
try:
while True:
try:
if monitor.check_for_changes():
logger.info("New position opening detected!")
if TELEGRAM_ENABLED and notifier:
last_closed, current_open = monitor.get_notification_data()
if current_open:
message = notifier.format_position_message(last_closed, current_open)
success = notifier.send_message(message)
if success:
monitor.update_state()
logger.info(f"Notification sent for position {current_open.get('token_id')}")
else:
logger.error("Failed to send notification")
else:
logger.warning("Position change detected but no open position found")
else:
logger.info("Telegram disabled or notifier not available - skipping notification")
monitor.update_state() # Still update state to avoid loops
else:
logger.debug("No changes detected")
time.sleep(TELEGRAM_CHECK_INTERVAL)
except KeyboardInterrupt:
break
except Exception as e:
logger.error(f"Error in monitoring loop: {e}")
time.sleep(TELEGRAM_CHECK_INTERVAL) # Continue after error
except KeyboardInterrupt:
logger.info("Shutting down Telegram Monitor...")
logger.info("Telegram Monitor stopped")
if __name__ == "__main__":
main()

109
tools/telegram_setup.py Normal file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Setup script for Telegram Bot notifications
Helps users create a Telegram bot and get required credentials
"""
import webbrowser
import time
def print_instructions():
"""Print setup instructions"""
print("🤖 TELEGRAM BOT SETUP INSTRUCTIONS")
print("=" * 50)
print()
print("1⃣ CREATE YOUR TELEGRAM BOT:")
print(" • Open Telegram and search for @BotFather")
print(" • Send: /newbot")
print(" • Choose a name for your bot")
print(" • Choose a username (must end with 'bot')")
print(" • BotFather will give you a HTTP API token")
print(" • Copy this token (it looks like: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz)")
print()
print("2⃣ GET YOUR CHAT ID:")
print(" • Send a message to your new bot first (any message)")
print(" • Your bot will not work if you haven't messaged it first")
print(" • Use the script below to get your Chat ID")
print()
print("3⃣ CONFIGURE ENVIRONMENT:")
print(" • Add to your .env file:")
print(" TELEGRAM_MONITOR_ENABLED=True")
print(" TELEGRAM_BOT_TOKEN=your_bot_token_here")
print(" TELEGRAM_CHAT_ID=your_chat_id_here")
print()
print("4⃣ RUN THE MONITOR:")
print(" • python tools/telegram_monitor.py")
print()
print("=" * 50)
def get_chat_id():
"""Get chat ID using bot token"""
print("🔍 CHAT ID FINDER")
print("-" * 20)
token = input("Enter your bot token: ").strip()
if not token:
print("❌ No token provided")
return
try:
import requests
import json
url = f"https://api.telegram.org/bot{token}/getUpdates"
response = requests.get(url, timeout=10)
data = response.json()
if data.get('ok'):
updates = data.get('result', [])
if updates:
chat_id = updates[-1]['message']['chat']['id']
print(f"✅ Your Chat ID: {chat_id}")
print()
print("Add this to your .env file:")
print(f"TELEGRAM_CHAT_ID={chat_id}")
else:
print("❌ No messages found")
print("Send a message to your bot first, then try again")
else:
print(f"❌ Error: {data.get('description', 'Unknown error')}")
except Exception as e:
print(f"❌ Error getting Chat ID: {e}")
def main():
"""Main setup menu"""
print("🤖 CLP TELEGRAM NOTIFICATION SETUP")
print("=" * 40)
print()
while True:
print("Choose an option:")
print("1. Show setup instructions")
print("2. Get Chat ID")
print("3. Open BotFather (to create bot)")
print("4. Exit")
print()
choice = input("Enter choice (1-4): ").strip()
if choice == '1':
print_instructions()
elif choice == '2':
get_chat_id()
elif choice == '3':
print("Opening BotFather in Telegram...")
try:
webbrowser.open('https://t.me/BotFather')
except:
print("Could not open browser. Go to https://t.me/BotFather manually")
elif choice == '4':
print("Goodbye! 🚀")
break
else:
print("Invalid choice. Try again.")
print("\n" + "=" * 40 + "\n")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Test script for Telegram Monitor functionality
Validates JSON parsing and message formatting
"""
import sys
import os
# Add the tools directory to the path
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
from telegram_monitor import TelegramNotifier, PositionMonitor
def test_json_parsing():
"""Test JSON parsing from hedge_status.json"""
print("Testing JSON parsing...")
monitor = PositionMonitor("../hedge_status.json", "test_state.json")
data = monitor.safe_read_json()
if not data:
print("❌ Failed to read JSON data")
return False
print(f"✅ Successfully read {len(data)} positions")
# Find open and closed positions
last_closed, current_open = monitor.extract_notification_data(data)
if current_open:
print(f"✅ Found open position: Token ID {current_open.get('token_id')}")
else:
print(" No open position found")
if last_closed:
print(f"✅ Found last closed: Token ID {last_closed.get('token_id')}")
else:
print(" No closed positions found")
return True
def test_message_formatting():
"""Test message formatting"""
print("\n🧪 Testing message formatting...")
# Create test data
last_closed = {
"token_id": 1234567,
"entry_price": 3000.0,
"target_value": 2000.0,
"hedge_pnl_realized": -15.50,
"hedge_fees_paid": 2.25,
"timestamp_open": 1766328197,
"timestamp_close": 1766331797
}
current_open = {
"token_id": 1234568,
"entry_price": 2980.0,
"target_value": 1950.0,
"range_lower": 2900.0,
"range_upper": 3060.0,
"amount0_initial": 0.3,
"amount1_initial": 900.0,
"timestamp_open": 1766335400
}
notifier = TelegramNotifier("test_token", "test_chat")
message = notifier.format_position_message(last_closed, current_open)
print("📱 Sample message:")
print("-" * 40)
print(message)
print("-" * 40)
return True
def main():
print("Telegram Monitor Test Suite")
print("=" * 50)
success = True
try:
if not test_json_parsing():
success = False
if not test_message_formatting():
success = False
except Exception as e:
print(f"❌ Test failed with error: {e}")
success = False
if success:
print("\n✅ All tests passed! Telegram monitor is ready.")
else:
print("\n❌ Some tests failed. Check the errors above.")
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
Simple test for Telegram Monitor functionality
"""
import sys
import os
# Add tools directory to path
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
try:
from telegram_monitor import TelegramNotifier, PositionMonitor
print("Testing Telegram Monitor components...")
# Test with sample data
notifier = TelegramNotifier("test_token", "test_chat")
last_closed = {
"token_id": 1234567,
"entry_price": 3000.0,
"target_value": 2000.0,
"hedge_pnl_realized": -15.50,
"hedge_fees_paid": 2.25,
"timestamp_open": 1766328197,
"timestamp_close": 1766331797
}
current_open = {
"token_id": 1234568,
"entry_price": 2980.0,
"target_value": 1950.0,
"range_lower": 2900.0,
"range_upper": 3060.0,
"amount0_initial": 0.3,
"amount1_initial": 900.0,
"timestamp_open": 1766335400
}
message = notifier.format_position_message(last_closed, current_open)
print("Message formatting test PASSED")
print("Sample message:")
print("-" * 40)
print(message)
print("-" * 40)
# Test JSON reading if file exists
if os.path.exists("../hedge_status.json"):
monitor = PositionMonitor("../hedge_status.json", "test_state.json")
data = monitor.safe_read_json()
if data:
print(f"JSON reading test PASSED - found {len(data)} positions")
else:
print("JSON reading test FAILED")
print("All tests completed successfully!")
except ImportError as e:
print(f"Import error: {e}")
print("Make sure telegram_monitor.py is in the same directory")
except Exception as e:
print(f"Test error: {e}")