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

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()