|
@@ -9,20 +9,18 @@ with comprehensive statistics tracking and phone-friendly controls.
|
|
|
import logging
|
|
|
import asyncio
|
|
|
import re
|
|
|
-from datetime import datetime
|
|
|
+from datetime import datetime, timedelta
|
|
|
from typing import Optional, Dict, Any
|
|
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
|
|
|
from hyperliquid_client import HyperliquidClient
|
|
|
from trading_stats import TradingStats
|
|
|
from config import Config
|
|
|
+from alarm_manager import AlarmManager
|
|
|
+from logging_config import setup_logging, cleanup_logs, format_log_stats
|
|
|
|
|
|
-# Set up logging
|
|
|
-logging.basicConfig(
|
|
|
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
- level=getattr(logging, Config.LOG_LEVEL)
|
|
|
-)
|
|
|
-logger = logging.getLogger(__name__)
|
|
|
+# Set up logging using the new configuration system
|
|
|
+logger = setup_logging().getChild(__name__)
|
|
|
|
|
|
class TelegramTradingBot:
|
|
|
"""Telegram bot for manual trading with comprehensive statistics."""
|
|
@@ -31,6 +29,7 @@ class TelegramTradingBot:
|
|
|
"""Initialize the Telegram trading bot."""
|
|
|
self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
|
|
|
self.stats = TradingStats()
|
|
|
+ self.alarm_manager = AlarmManager()
|
|
|
self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
|
|
|
self.application = None
|
|
|
|
|
@@ -39,6 +38,9 @@ class TelegramTradingBot:
|
|
|
self.last_known_orders = set() # Track order IDs we've seen
|
|
|
self.last_known_positions = {} # Track position sizes for P&L calculation
|
|
|
|
|
|
+ # External trade monitoring
|
|
|
+ self.last_processed_trade_time = None # Track last processed external trade
|
|
|
+
|
|
|
# Initialize stats with current balance
|
|
|
self._initialize_stats()
|
|
|
|
|
@@ -90,8 +92,10 @@ Tap the buttons below for instant access to key functions.
|
|
|
/stats - Trading statistics
|
|
|
|
|
|
<b>📊 Market Commands:</b>
|
|
|
-/market - Market data
|
|
|
-/price - Current price
|
|
|
+/market - Market data (default token)
|
|
|
+/market SOL - Market data for SOL
|
|
|
+/price - Current price (default token)
|
|
|
+/price BTC - Price for BTC
|
|
|
|
|
|
<b>🚀 Perps Trading:</b>
|
|
|
• /long BTC 100 - Long BTC with $100 USDC (Market Order)
|
|
@@ -114,16 +118,32 @@ Tap the buttons below for instant access to key functions.
|
|
|
/performance - Performance metrics
|
|
|
/risk - Risk analysis
|
|
|
|
|
|
-<b>🔄 Automatic Notifications:</b>
|
|
|
+<b>🔔 Price Alerts:</b>
|
|
|
+• /alarm - List all alarms
|
|
|
+• /alarm BTC 50000 - Set alarm for BTC at $50,000
|
|
|
+• /alarm BTC - Show BTC alarms only
|
|
|
+• /alarm 3 - Remove alarm ID 3
|
|
|
+
|
|
|
+<b>🔄 Automatic Monitoring:</b>
|
|
|
• Real-time order fill alerts
|
|
|
• Position opened/closed notifications
|
|
|
• P&L calculations on trade closure
|
|
|
-• 30-second monitoring interval
|
|
|
+• Price alarm triggers
|
|
|
+• External trade detection & sync
|
|
|
+• Auto stats synchronization
|
|
|
+• {Config.BOT_HEARTBEAT_SECONDS}-second monitoring interval
|
|
|
+
|
|
|
+<b>📊 Universal Trade Tracking:</b>
|
|
|
+• Bot trades: Full logging & notifications
|
|
|
+• Platform trades: Auto-detected & synced
|
|
|
+• Mobile app trades: Monitored & recorded
|
|
|
+• API trades: Tracked & included in stats
|
|
|
|
|
|
Type /help for detailed command information.
|
|
|
|
|
|
<b>🔄 Order Monitoring:</b>
|
|
|
• /monitoring - View monitoring status
|
|
|
+• /logs - View log file statistics and cleanup
|
|
|
|
|
|
<b>⚙️ Configuration:</b>
|
|
|
• Symbol: {symbol}
|
|
@@ -186,8 +206,10 @@ For support, contact your bot administrator.
|
|
|
• /orders - Show open orders
|
|
|
|
|
|
<b>📊 Market Data:</b>
|
|
|
-• /market - Detailed market data
|
|
|
-• /price - Quick price check
|
|
|
+• /market - Detailed market data (default token)
|
|
|
+• /market BTC - Market data for specific token
|
|
|
+• /price - Quick price check (default token)
|
|
|
+• /price SOL - Price for specific token
|
|
|
|
|
|
<b>🚀 Perps Trading:</b>
|
|
|
• /long BTC 100 - Long BTC with $100 USDC (Market Order)
|
|
@@ -211,8 +233,15 @@ For support, contact your bot administrator.
|
|
|
• /risk - Sharpe ratio, drawdown, VaR
|
|
|
• /trades - Recent trade history
|
|
|
|
|
|
+<b>🔔 Price Alerts:</b>
|
|
|
+• /alarm - List all active alarms
|
|
|
+• /alarm BTC 50000 - Set alarm for BTC at $50,000
|
|
|
+• /alarm BTC - Show all BTC alarms
|
|
|
+• /alarm 3 - Remove alarm ID 3
|
|
|
+
|
|
|
<b>🔄 Order Monitoring:</b>
|
|
|
• /monitoring - View monitoring status
|
|
|
+• /logs - View log file statistics and cleanup
|
|
|
|
|
|
<b>⚙️ Configuration:</b>
|
|
|
• Symbol: {symbol}
|
|
@@ -457,35 +486,83 @@ For support, contact your bot administrator.
|
|
|
await update.message.reply_text("❌ Unauthorized access.")
|
|
|
return
|
|
|
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+ # Check if token is provided as argument
|
|
|
+ if context.args and len(context.args) >= 1:
|
|
|
+ symbol = context.args[0].upper()
|
|
|
+ else:
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+
|
|
|
market_data = self.client.get_market_data(symbol)
|
|
|
|
|
|
- if market_data:
|
|
|
- ticker = market_data['ticker']
|
|
|
- orderbook = market_data['orderbook']
|
|
|
-
|
|
|
- # Calculate 24h change
|
|
|
- current_price = float(ticker.get('last', 0))
|
|
|
- high_24h = float(ticker.get('high', 0))
|
|
|
- low_24h = float(ticker.get('low', 0))
|
|
|
-
|
|
|
- market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
|
|
|
- market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
|
|
|
- market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
|
|
|
- market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
|
|
|
- market_text += f"📊 <b>24h Volume:</b> {ticker.get('baseVolume', 'N/A')}\n\n"
|
|
|
-
|
|
|
- if orderbook.get('bids') and orderbook.get('asks'):
|
|
|
- best_bid = float(orderbook['bids'][0][0]) if orderbook['bids'] else 0
|
|
|
- best_ask = float(orderbook['asks'][0][0]) if orderbook['asks'] else 0
|
|
|
- spread = best_ask - best_bid
|
|
|
- spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
|
|
|
+ if market_data and market_data.get('ticker'):
|
|
|
+ try:
|
|
|
+ ticker = market_data['ticker']
|
|
|
+ orderbook = market_data.get('orderbook', {})
|
|
|
+
|
|
|
+ # Safely extract ticker data with fallbacks
|
|
|
+ current_price = float(ticker.get('last') or 0)
|
|
|
+ high_24h = float(ticker.get('high') or 0)
|
|
|
+ low_24h = float(ticker.get('low') or 0)
|
|
|
+ volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
|
|
|
|
|
|
- market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
|
|
|
- market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
|
|
|
- market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
|
|
|
+ market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
|
|
|
+
|
|
|
+ if current_price > 0:
|
|
|
+ market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
|
|
|
+ else:
|
|
|
+ market_text += f"💵 <b>Current Price:</b> N/A\n"
|
|
|
+
|
|
|
+ if high_24h > 0:
|
|
|
+ market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
|
|
|
+ else:
|
|
|
+ market_text += f"📈 <b>24h High:</b> N/A\n"
|
|
|
+
|
|
|
+ if low_24h > 0:
|
|
|
+ market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
|
|
|
+ else:
|
|
|
+ market_text += f"📉 <b>24h Low:</b> N/A\n"
|
|
|
+
|
|
|
+ market_text += f"📊 <b>24h Volume:</b> {volume_24h}\n\n"
|
|
|
+
|
|
|
+ # Handle orderbook data safely
|
|
|
+ if orderbook and orderbook.get('bids') and orderbook.get('asks'):
|
|
|
+ try:
|
|
|
+ bids = orderbook.get('bids', [])
|
|
|
+ asks = orderbook.get('asks', [])
|
|
|
+
|
|
|
+ if bids and asks and len(bids) > 0 and len(asks) > 0:
|
|
|
+ best_bid = float(bids[0][0]) if bids[0][0] else 0
|
|
|
+ best_ask = float(asks[0][0]) if asks[0][0] else 0
|
|
|
+
|
|
|
+ if best_bid > 0 and best_ask > 0:
|
|
|
+ spread = best_ask - best_bid
|
|
|
+ spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
|
|
|
+
|
|
|
+ market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
|
|
|
+ market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
|
|
|
+ market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
|
|
|
+ else:
|
|
|
+ market_text += f"📋 <b>Orderbook:</b> Data unavailable\n"
|
|
|
+ else:
|
|
|
+ market_text += f"📋 <b>Orderbook:</b> No orders available\n"
|
|
|
+ except (IndexError, ValueError, TypeError) as e:
|
|
|
+ market_text += f"📋 <b>Orderbook:</b> Error parsing data\n"
|
|
|
+ else:
|
|
|
+ market_text += f"📋 <b>Orderbook:</b> Not available\n"
|
|
|
+
|
|
|
+ # Add usage hint
|
|
|
+ market_text += f"\n💡 <b>Usage:</b> <code>/market {symbol}</code> or <code>/market</code> for default"
|
|
|
+
|
|
|
+ except (ValueError, TypeError) as e:
|
|
|
+ market_text = f"❌ <b>Error parsing market data</b>\n\n"
|
|
|
+ market_text += f"🔧 Raw data received but couldn't parse values.\n"
|
|
|
+ market_text += f"📞 Please try again or contact support if this persists."
|
|
|
else:
|
|
|
- market_text = "❌ Could not fetch market data"
|
|
|
+ market_text = f"❌ <b>Could not fetch market data for {symbol}</b>\n\n"
|
|
|
+ market_text += f"🔄 Please try again in a moment.\n"
|
|
|
+ market_text += f"🌐 Check your network connection.\n"
|
|
|
+ market_text += f"📡 API may be temporarily unavailable.\n\n"
|
|
|
+ market_text += f"💡 <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
|
|
|
|
|
|
await update.message.reply_text(market_text, parse_mode='HTML')
|
|
|
|
|
@@ -495,18 +572,37 @@ For support, contact your bot administrator.
|
|
|
await update.message.reply_text("❌ Unauthorized access.")
|
|
|
return
|
|
|
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+ # Check if token is provided as argument
|
|
|
+ if context.args and len(context.args) >= 1:
|
|
|
+ symbol = context.args[0].upper()
|
|
|
+ else:
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+
|
|
|
market_data = self.client.get_market_data(symbol)
|
|
|
|
|
|
- if market_data:
|
|
|
- price = float(market_data['ticker'].get('last', 0))
|
|
|
- price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
|
|
|
-
|
|
|
- # Add timestamp
|
|
|
- timestamp = datetime.now().strftime('%H:%M:%S')
|
|
|
- price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
|
|
|
+ if market_data and market_data.get('ticker'):
|
|
|
+ try:
|
|
|
+ ticker = market_data['ticker']
|
|
|
+ price_value = ticker.get('last')
|
|
|
+
|
|
|
+ if price_value is not None:
|
|
|
+ price = float(price_value)
|
|
|
+ price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
|
|
|
+
|
|
|
+ # Add timestamp
|
|
|
+ timestamp = datetime.now().strftime('%H:%M:%S')
|
|
|
+ price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
|
|
|
+
|
|
|
+ # Add usage hint
|
|
|
+ price_text += f"\n💡 <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
|
|
|
+ else:
|
|
|
+ price_text = f"💵 <b>{symbol}</b>: Price not available\n⚠️ <i>Data temporarily unavailable</i>"
|
|
|
+
|
|
|
+ except (ValueError, TypeError) as e:
|
|
|
+ price_text = f"❌ <b>Error parsing price for {symbol}</b>\n🔧 <i>Please try again</i>"
|
|
|
else:
|
|
|
- price_text = f"❌ Could not fetch price for {symbol}"
|
|
|
+ price_text = f"❌ <b>Could not fetch price for {symbol}</b>\n🔄 <i>Please try again in a moment</i>\n\n"
|
|
|
+ price_text += f"💡 <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
|
|
|
|
|
|
await update.message.reply_text(price_text, parse_mode='HTML')
|
|
|
|
|
@@ -953,6 +1049,8 @@ For support, contact your bot administrator.
|
|
|
self.application.add_handler(CommandHandler("sl", self.sl_command))
|
|
|
self.application.add_handler(CommandHandler("tp", self.tp_command))
|
|
|
self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
|
|
|
+ self.application.add_handler(CommandHandler("alarm", self.alarm_command))
|
|
|
+ self.application.add_handler(CommandHandler("logs", self.logs_command))
|
|
|
|
|
|
# Callback query handler for inline keyboards
|
|
|
self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
|
@@ -988,11 +1086,22 @@ For support, contact your bot administrator.
|
|
|
f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
|
|
|
f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
|
|
|
f"📱 Manual trading ready!\n"
|
|
|
- f"🔄 Order monitoring: Active\n"
|
|
|
+ f"🔄 Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
|
|
|
+ f"🔄 External trade monitoring: Active\n"
|
|
|
+ f"🔔 Price alarms: Active\n"
|
|
|
+ f"📊 Auto stats sync: Enabled\n"
|
|
|
+ f"📝 Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
|
|
|
f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
|
"Use /start for quick actions or /help for all commands."
|
|
|
)
|
|
|
|
|
|
+ # Perform initial log cleanup
|
|
|
+ try:
|
|
|
+ cleanup_logs(days_to_keep=30)
|
|
|
+ logger.info("🧹 Initial log cleanup completed")
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"⚠️ Initial log cleanup failed: {e}")
|
|
|
+
|
|
|
# Start the application
|
|
|
await self.application.start()
|
|
|
|
|
@@ -1696,17 +1805,17 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
logger.error(f"❌ Error initializing order tracking: {e}")
|
|
|
|
|
|
async def _order_monitoring_loop(self):
|
|
|
- """Main monitoring loop that runs every 30 seconds."""
|
|
|
+ """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
|
|
|
while self.monitoring_active:
|
|
|
try:
|
|
|
await self._check_order_fills()
|
|
|
- await asyncio.sleep(30) # Wait 30 seconds
|
|
|
+ await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval
|
|
|
except asyncio.CancelledError:
|
|
|
logger.info("🛑 Order monitoring cancelled")
|
|
|
break
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Error in order monitoring loop: {e}")
|
|
|
- await asyncio.sleep(30) # Continue monitoring even if there's an error
|
|
|
+ await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error
|
|
|
|
|
|
async def _check_order_fills(self):
|
|
|
"""Check for filled orders and send notifications."""
|
|
@@ -1729,9 +1838,168 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
self.last_known_orders = current_order_ids
|
|
|
await self._update_position_tracking(current_positions)
|
|
|
|
|
|
+ # Check price alarms
|
|
|
+ await self._check_price_alarms()
|
|
|
+
|
|
|
+ # Check external trades (trades made outside the bot)
|
|
|
+ await self._check_external_trades()
|
|
|
+
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Error checking order fills: {e}")
|
|
|
-
|
|
|
+
|
|
|
+ async def _check_price_alarms(self):
|
|
|
+ """Check all active price alarms."""
|
|
|
+ try:
|
|
|
+ # Get all active alarms
|
|
|
+ active_alarms = self.alarm_manager.get_all_active_alarms()
|
|
|
+ if not active_alarms:
|
|
|
+ return
|
|
|
+
|
|
|
+ # Get unique tokens from alarms
|
|
|
+ tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
|
|
|
+
|
|
|
+ # Fetch current prices for all tokens
|
|
|
+ price_data = {}
|
|
|
+ for token in tokens_to_check:
|
|
|
+ symbol = f"{token}/USDC:USDC"
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
+
|
|
|
+ if market_data and market_data.get('ticker'):
|
|
|
+ current_price = market_data['ticker'].get('last')
|
|
|
+ if current_price is not None:
|
|
|
+ price_data[token] = float(current_price)
|
|
|
+
|
|
|
+ # Check alarms against current prices
|
|
|
+ triggered_alarms = self.alarm_manager.check_alarms(price_data)
|
|
|
+
|
|
|
+ # Send notifications for triggered alarms
|
|
|
+ for alarm in triggered_alarms:
|
|
|
+ await self._send_alarm_notification(alarm)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error checking price alarms: {e}")
|
|
|
+
|
|
|
+ async def _send_alarm_notification(self, alarm: Dict[str, Any]):
|
|
|
+ """Send notification for triggered alarm."""
|
|
|
+ try:
|
|
|
+ message = self.alarm_manager.format_triggered_alarm(alarm)
|
|
|
+ await self.send_message(message)
|
|
|
+ logger.info(f"📢 Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error sending alarm notification: {e}")
|
|
|
+
|
|
|
+ async def _check_external_trades(self):
|
|
|
+ """Check for trades made outside the Telegram bot and update stats."""
|
|
|
+ try:
|
|
|
+ # Get recent fills from Hyperliquid
|
|
|
+ recent_fills = self.client.get_recent_fills()
|
|
|
+
|
|
|
+ if not recent_fills:
|
|
|
+ return
|
|
|
+
|
|
|
+ # Initialize last processed time if first run
|
|
|
+ if self.last_processed_trade_time is None:
|
|
|
+ # Set to current time minus 1 hour to catch recent activity
|
|
|
+ self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat()
|
|
|
+
|
|
|
+ # Filter for new trades since last check
|
|
|
+ new_trades = []
|
|
|
+ latest_trade_time = self.last_processed_trade_time
|
|
|
+
|
|
|
+ for fill in recent_fills:
|
|
|
+ fill_time = fill.get('timestamp')
|
|
|
+ if fill_time and fill_time > self.last_processed_trade_time:
|
|
|
+ new_trades.append(fill)
|
|
|
+ if fill_time > latest_trade_time:
|
|
|
+ latest_trade_time = fill_time
|
|
|
+
|
|
|
+ if not new_trades:
|
|
|
+ return
|
|
|
+
|
|
|
+ # Process new trades
|
|
|
+ for trade in new_trades:
|
|
|
+ await self._process_external_trade(trade)
|
|
|
+
|
|
|
+ # Update last processed time
|
|
|
+ self.last_processed_trade_time = latest_trade_time
|
|
|
+
|
|
|
+ if new_trades:
|
|
|
+ logger.info(f"📊 Processed {len(new_trades)} external trades")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error checking external trades: {e}")
|
|
|
+
|
|
|
+ async def _process_external_trade(self, trade: Dict[str, Any]):
|
|
|
+ """Process an individual external trade."""
|
|
|
+ try:
|
|
|
+ # Extract trade information
|
|
|
+ symbol = trade.get('symbol', '')
|
|
|
+ side = trade.get('side', '')
|
|
|
+ amount = float(trade.get('amount', 0))
|
|
|
+ price = float(trade.get('price', 0))
|
|
|
+ trade_id = trade.get('id', 'external')
|
|
|
+ timestamp = trade.get('timestamp', '')
|
|
|
+
|
|
|
+ if not all([symbol, side, amount, price]):
|
|
|
+ return
|
|
|
+
|
|
|
+ # Record trade in stats
|
|
|
+ self.stats.record_trade(symbol, side, amount, price, trade_id)
|
|
|
+
|
|
|
+ # Send notification for significant trades
|
|
|
+ await self._send_external_trade_notification(trade)
|
|
|
+
|
|
|
+ logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error processing external trade: {e}")
|
|
|
+
|
|
|
+ async def _send_external_trade_notification(self, trade: Dict[str, Any]):
|
|
|
+ """Send notification for external trades."""
|
|
|
+ try:
|
|
|
+ symbol = trade.get('symbol', '')
|
|
|
+ side = trade.get('side', '')
|
|
|
+ amount = float(trade.get('amount', 0))
|
|
|
+ price = float(trade.get('price', 0))
|
|
|
+ timestamp = trade.get('timestamp', '')
|
|
|
+
|
|
|
+ # Extract token from symbol
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+
|
|
|
+ # Format timestamp
|
|
|
+ try:
|
|
|
+ trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
|
+ time_str = trade_time.strftime('%H:%M:%S')
|
|
|
+ except:
|
|
|
+ time_str = "Unknown"
|
|
|
+
|
|
|
+ # Determine trade type and emoji
|
|
|
+ side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
+ trade_value = amount * price
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+🔄 <b>External Trade Detected</b>
|
|
|
+
|
|
|
+📊 <b>Trade Details:</b>
|
|
|
+• Token: {token}
|
|
|
+• Side: {side.upper()}
|
|
|
+• Amount: {amount} {token}
|
|
|
+• Price: ${price:,.2f}
|
|
|
+• Value: ${trade_value:,.2f}
|
|
|
+
|
|
|
+{side_emoji} <b>Source:</b> Direct Platform Trade
|
|
|
+⏰ <b>Time:</b> {time_str}
|
|
|
+
|
|
|
+📈 <b>Note:</b> This trade was executed outside the Telegram bot
|
|
|
+📊 Stats have been automatically updated
|
|
|
+ """
|
|
|
+
|
|
|
+ await self.send_message(message.strip())
|
|
|
+ logger.info(f"📢 Sent external trade notification: {side} {amount} {token}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error sending external trade notification: {e}")
|
|
|
+
|
|
|
async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
|
|
|
"""Process filled orders and determine if they opened or closed positions."""
|
|
|
try:
|
|
@@ -1928,20 +2196,35 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
await update.message.reply_text("❌ Unauthorized access.")
|
|
|
return
|
|
|
|
|
|
+ # Get alarm statistics
|
|
|
+ alarm_stats = self.alarm_manager.get_statistics()
|
|
|
+
|
|
|
status_text = f"""
|
|
|
-🔄 <b>Order Monitoring Status</b>
|
|
|
+🔄 <b>System Monitoring Status</b>
|
|
|
|
|
|
-📊 <b>Current Status:</b>
|
|
|
+📊 <b>Order Monitoring:</b>
|
|
|
• Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
|
|
|
-• Check Interval: 30 seconds
|
|
|
+• Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
|
|
|
• Orders Tracked: {len(self.last_known_orders)}
|
|
|
• Positions Tracked: {len(self.last_known_positions)}
|
|
|
|
|
|
+🔔 <b>Price Alarms:</b>
|
|
|
+• Active Alarms: {alarm_stats['total_active']}
|
|
|
+• Triggered Today: {alarm_stats['total_triggered']}
|
|
|
+• Tokens Monitored: {alarm_stats['tokens_tracked']}
|
|
|
+• Next Alarm ID: {alarm_stats['next_id']}
|
|
|
+
|
|
|
+🔄 <b>External Trade Monitoring:</b>
|
|
|
+• Last Check: {self.last_processed_trade_time or 'Not started'}
|
|
|
+• Auto Stats Update: ✅ Enabled
|
|
|
+• External Notifications: ✅ Enabled
|
|
|
+
|
|
|
📈 <b>Notifications:</b>
|
|
|
-• 🚀 Position Opened
|
|
|
-• 📈 Position Increased
|
|
|
-• 📉 Position Partially Closed
|
|
|
-• 🎯 Position Closed (with P&L)
|
|
|
+• 🚀 Position Opened/Increased
|
|
|
+• 📉 Position Partially/Fully Closed
|
|
|
+• 🎯 P&L Calculations
|
|
|
+• 🔔 Price Alarm Triggers
|
|
|
+• 🔄 External Trade Detection
|
|
|
|
|
|
⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
|
|
@@ -1949,11 +2232,177 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
• Real-time order fill detection
|
|
|
• Automatic P&L calculation
|
|
|
• Position change tracking
|
|
|
+• Price alarm monitoring
|
|
|
+• External trade monitoring
|
|
|
+• Auto stats synchronization
|
|
|
• Instant Telegram notifications
|
|
|
"""
|
|
|
|
|
|
+ if alarm_stats['token_breakdown']:
|
|
|
+ status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
|
|
|
+ for token, count in alarm_stats['token_breakdown'].items():
|
|
|
+ status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
|
|
|
+
|
|
|
await update.message.reply_text(status_text.strip(), parse_mode='HTML')
|
|
|
|
|
|
+ async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /alarm command for price alerts."""
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ if not context.args or len(context.args) == 0:
|
|
|
+ # No arguments - list all alarms
|
|
|
+ alarms = self.alarm_manager.get_all_active_alarms()
|
|
|
+ message = self.alarm_manager.format_alarm_list(alarms)
|
|
|
+ await update.message.reply_text(message, parse_mode='HTML')
|
|
|
+ return
|
|
|
+
|
|
|
+ elif len(context.args) == 1:
|
|
|
+ arg = context.args[0]
|
|
|
+
|
|
|
+ # Check if argument is a number (alarm ID to remove)
|
|
|
+ try:
|
|
|
+ alarm_id = int(arg)
|
|
|
+ # Remove alarm by ID
|
|
|
+ if self.alarm_manager.remove_alarm(alarm_id):
|
|
|
+ await update.message.reply_text(f"✅ Alarm ID {alarm_id} has been removed.")
|
|
|
+ else:
|
|
|
+ await update.message.reply_text(f"❌ Alarm ID {alarm_id} not found.")
|
|
|
+ return
|
|
|
+ except ValueError:
|
|
|
+ # Not a number, treat as token
|
|
|
+ token = arg.upper()
|
|
|
+ alarms = self.alarm_manager.get_alarms_by_token(token)
|
|
|
+ message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
|
|
|
+ await update.message.reply_text(message, parse_mode='HTML')
|
|
|
+ return
|
|
|
+
|
|
|
+ elif len(context.args) == 2:
|
|
|
+ # Set new alarm: /alarm TOKEN PRICE
|
|
|
+ token = context.args[0].upper()
|
|
|
+ target_price = float(context.args[1])
|
|
|
+
|
|
|
+ # Get current market price
|
|
|
+ symbol = f"{token}/USDC:USDC"
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
+
|
|
|
+ if not market_data or not market_data.get('ticker'):
|
|
|
+ await update.message.reply_text(f"❌ Could not fetch current price for {token}")
|
|
|
+ return
|
|
|
+
|
|
|
+ current_price = float(market_data['ticker'].get('last', 0))
|
|
|
+ if current_price <= 0:
|
|
|
+ await update.message.reply_text(f"❌ Invalid current price for {token}")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Create the alarm
|
|
|
+ alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
|
|
|
+
|
|
|
+ # Format confirmation message
|
|
|
+ direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
|
|
|
+ price_diff = abs(target_price - current_price)
|
|
|
+ price_diff_percent = (price_diff / current_price) * 100
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+✅ <b>Price Alarm Created</b>
|
|
|
+
|
|
|
+📊 <b>Alarm Details:</b>
|
|
|
+• Alarm ID: {alarm['id']}
|
|
|
+• Token: {token}
|
|
|
+• Target Price: ${target_price:,.2f}
|
|
|
+• Current Price: ${current_price:,.2f}
|
|
|
+• Direction: {alarm['direction'].upper()}
|
|
|
+
|
|
|
+{direction_emoji} <b>Alert Condition:</b>
|
|
|
+Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
+
|
|
|
+💰 <b>Price Difference:</b>
|
|
|
+• Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
|
|
|
+• Status: ACTIVE ✅
|
|
|
+
|
|
|
+⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+
|
|
|
+💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
|
|
|
+ """
|
|
|
+
|
|
|
+ await update.message.reply_text(message.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ else:
|
|
|
+ # Too many arguments
|
|
|
+ await update.message.reply_text(
|
|
|
+ "❌ Invalid usage. Examples:\n\n"
|
|
|
+ "• <code>/alarm</code> - List all alarms\n"
|
|
|
+ "• <code>/alarm BTC</code> - List BTC alarms\n"
|
|
|
+ "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
|
|
|
+ "• <code>/alarm 3</code> - Remove alarm ID 3",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ except ValueError:
|
|
|
+ await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing alarm command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in alarm command: {e}")
|
|
|
+
|
|
|
+ async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /logs command to show log file statistics and cleanup options."""
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Check for cleanup argument
|
|
|
+ if context.args and len(context.args) >= 1:
|
|
|
+ if context.args[0].lower() == 'cleanup':
|
|
|
+ # Get days parameter (default 30)
|
|
|
+ days_to_keep = 30
|
|
|
+ if len(context.args) >= 2:
|
|
|
+ try:
|
|
|
+ days_to_keep = int(context.args[1])
|
|
|
+ except ValueError:
|
|
|
+ await update.message.reply_text("❌ Invalid number of days. Using default (30).")
|
|
|
+
|
|
|
+ # Perform cleanup
|
|
|
+ await update.message.reply_text(f"🧹 Cleaning up log files older than {days_to_keep} days...")
|
|
|
+ cleanup_logs(days_to_keep)
|
|
|
+ await update.message.reply_text(f"✅ Log cleanup completed!")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Show log statistics
|
|
|
+ log_stats_text = format_log_stats()
|
|
|
+
|
|
|
+ # Add additional info
|
|
|
+ status_text = f"""
|
|
|
+📊 <b>System Logging Status</b>
|
|
|
+
|
|
|
+{log_stats_text}
|
|
|
+
|
|
|
+📈 <b>Log Configuration:</b>
|
|
|
+• Log Level: {Config.LOG_LEVEL}
|
|
|
+• Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
|
|
|
+• Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
+
|
|
|
+💡 <b>Log Management:</b>
|
|
|
+• <code>/logs cleanup</code> - Clean old logs (30 days)
|
|
|
+• <code>/logs cleanup 7</code> - Clean logs older than 7 days
|
|
|
+• Log rotation happens automatically
|
|
|
+• Old backups are removed automatically
|
|
|
+
|
|
|
+🔧 <b>Configuration:</b>
|
|
|
+• Rotation Type: {Config.LOG_ROTATION_TYPE}
|
|
|
+• Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
|
|
|
+• Backup Count: {Config.LOG_BACKUP_COUNT}
|
|
|
+ """
|
|
|
+
|
|
|
+ await update.message.reply_text(status_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing logs command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in logs command: {e}")
|
|
|
|
|
|
async def main_async():
|
|
|
"""Async main entry point for the Telegram bot."""
|