12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978 |
- #!/usr/bin/env python3
- """
- Telegram Bot for Hyperliquid Trading
- This module provides a Telegram interface for manual Hyperliquid trading
- with comprehensive statistics tracking and phone-friendly controls.
- """
- import logging
- import asyncio
- import re
- 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 using the new configuration system
- logger = setup_logging().getChild(__name__)
- class TelegramTradingBot:
- """Telegram bot for manual trading with comprehensive statistics."""
-
- def __init__(self):
- """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
-
- # Order monitoring
- self.monitoring_active = False
- 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()
-
- def _initialize_stats(self):
- """Initialize stats with current balance."""
- try:
- balance = self.client.get_balance()
- if balance and balance.get('total'):
- # Get USDC balance as the main balance
- usdc_balance = float(balance['total'].get('USDC', 0))
- self.stats.set_initial_balance(usdc_balance)
- except Exception as e:
- logger.error(f"Could not initialize stats: {e}")
-
- def is_authorized(self, chat_id: str) -> bool:
- """Check if the chat ID is authorized to use the bot."""
- return str(chat_id) == str(self.authorized_chat_id)
-
- async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
- """Send a message to the authorized chat."""
- if self.application and self.authorized_chat_id:
- try:
- await self.application.bot.send_message(
- chat_id=self.authorized_chat_id,
- text=text,
- parse_mode=parse_mode
- )
- except Exception as e:
- logger.error(f"Failed to send message: {e}")
-
- async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /start command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- welcome_text = """
- 🤖 <b>Hyperliquid Manual Trading Bot</b>
- Welcome to your personal trading assistant! Control your Hyperliquid account directly from your phone.
- <b>📱 Quick Actions:</b>
- Tap the buttons below for instant access to key functions.
- <b>💼 Account Commands:</b>
- /balance - Account balance
- /positions - Open positions
- /orders - Open orders
- /stats - Trading statistics
- <b>📊 Market Commands:</b>
- /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)
- • /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
- • /short ETH 50 - Short ETH with $50 USDC (Market Order)
- • /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
- • /exit BTC - Close BTC position with Market Order
- <b>🛡️ Risk Management:</b>
- • /sl BTC 44000 - Set stop loss for BTC at $44,000
- • /tp BTC 50000 - Set take profit for BTC at $50,000
- <b>🚨 Automatic Stop Loss:</b>
- • Enabled: {risk_enabled}
- • Stop Loss: {stop_loss}% (automatic execution)
- • Monitoring: Every {heartbeat} seconds
- <b>📋 Order Management:</b>
- • /orders - Show all open orders
- • /orders BTC - Show open orders for BTC only
- • /coo BTC - Cancel all open orders for BTC
- <b>📈 Statistics & Analytics:</b>
- • /stats - Complete trading statistics
- • /performance - Token performance ranking & detailed stats
- • /daily - Daily performance (last 10 days)
- • /weekly - Weekly performance (last 10 weeks)
- • /monthly - Monthly performance (last 10 months)
- • /risk - Sharpe ratio, drawdown, VaR
- • /trades - Recent trade history
- <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
- • Price alarm triggers
- • External trade detection & sync
- • Auto stats synchronization
- • {heartbeat}-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}
- • Default Token: {symbol}
- • Network: {network}
- <b>🛡️ Safety Features:</b>
- • All trades logged automatically
- • Comprehensive performance tracking
- • Real-time balance monitoring
- • Risk metrics calculation
- <b>📱 Mobile Optimized:</b>
- • Quick action buttons
- • Instant notifications
- • Clean, readable layout
- • One-tap commands
- For support, contact your bot administrator.
- """.format(
- symbol=Config.DEFAULT_TRADING_TOKEN,
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
- )
-
- keyboard = [
- [
- InlineKeyboardButton("💰 Balance", callback_data="balance"),
- InlineKeyboardButton("📊 Stats", callback_data="stats")
- ],
- [
- InlineKeyboardButton("📈 Positions", callback_data="positions"),
- InlineKeyboardButton("📋 Orders", callback_data="orders")
- ],
- [
- InlineKeyboardButton("💵 Price", callback_data="price"),
- InlineKeyboardButton("📊 Market", callback_data="market")
- ],
- [
- InlineKeyboardButton("🔄 Recent Trades", callback_data="trades"),
- InlineKeyboardButton("⚙️ Help", callback_data="help")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
-
- async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /help command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- help_text = """
- 🔧 <b>Hyperliquid Trading Bot - Complete Guide</b>
- <b>💼 Account Management:</b>
- • /balance - Show account balance
- • /positions - Show open positions
- • /orders - Show open orders
- <b>📊 Market Data:</b>
- • /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)
- • /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
- • /short ETH 50 - Short ETH with $50 USDC (Market Order)
- • /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
- • /exit BTC - Close BTC position with Market Order
- <b>🛡️ Risk Management:</b>
- • /sl BTC 44000 - Set stop loss for BTC at $44,000
- • /tp BTC 50000 - Set take profit for BTC at $50,000
- <b>🚨 Automatic Stop Loss:</b>
- • Enabled: {risk_enabled}
- • Stop Loss: {stop_loss}% (automatic execution)
- • Monitoring: Every {heartbeat} seconds
- <b>📋 Order Management:</b>
- • /orders - Show all open orders
- • /orders BTC - Show open orders for BTC only
- • /coo BTC - Cancel all open orders for BTC
- <b>📈 Statistics & Analytics:</b>
- • /stats - Complete trading statistics
- • /performance - Win rate, profit factor, etc.
- • /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}
- • Default Token: {symbol}
- • Network: {network}
- <b>🛡️ Safety Features:</b>
- • All trades logged automatically
- • Comprehensive performance tracking
- • Real-time balance monitoring
- • Risk metrics calculation
- <b>📱 Mobile Optimized:</b>
- • Quick action buttons
- • Instant notifications
- • Clean, readable layout
- • One-tap commands
- For support, contact your bot administrator.
- """.format(
- symbol=Config.DEFAULT_TRADING_TOKEN,
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
- )
-
- await update.message.reply_text(help_text, parse_mode='HTML')
-
- async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /stats command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- # Get current balance for stats
- balance = self.client.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- stats_message = self.stats.format_stats_message(current_balance)
- await update.message.reply_text(stats_message, parse_mode='HTML')
-
- async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /trades command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- recent_trades = self.stats.get_recent_trades(10)
-
- if not recent_trades:
- await update.message.reply_text("📝 No trades recorded yet.")
- return
-
- trades_text = "🔄 <b>Recent Trades</b>\n\n"
-
- for trade in reversed(recent_trades[-5:]): # Show last 5 trades
- timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
- side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
-
- trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
- trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
- trades_text += f" 📅 {timestamp}\n\n"
-
- await update.message.reply_text(trades_text, parse_mode='HTML')
-
- async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /balance command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- balance = self.client.get_balance()
- if balance:
- balance_text = "💰 <b>Account Balance</b>\n\n"
-
- # CCXT balance structure includes 'free', 'used', and 'total'
- total_balance = balance.get('total', {})
- free_balance = balance.get('free', {})
- used_balance = balance.get('used', {})
-
- if total_balance:
- total_value = 0
- available_value = 0
-
- # Display individual assets
- for asset, amount in total_balance.items():
- if float(amount) > 0:
- free_amount = float(free_balance.get(asset, 0))
- used_amount = float(used_balance.get(asset, 0))
-
- balance_text += f"💵 <b>{asset}:</b>\n"
- balance_text += f" 📊 Total: {amount}\n"
- balance_text += f" ✅ Available: {free_amount}\n"
-
- if used_amount > 0:
- balance_text += f" 🔒 In Use: {used_amount}\n"
-
- balance_text += "\n"
-
- # Calculate totals for USDC (main trading currency)
- if asset == 'USDC':
- total_value += float(amount)
- available_value += free_amount
-
- # Summary section
- balance_text += f"💼 <b>Portfolio Summary:</b>\n"
- balance_text += f" 💰 Total Value: ${total_value:,.2f}\n"
- balance_text += f" 🚀 Available for Trading: ${available_value:,.2f}\n"
-
- if total_value - available_value > 0:
- balance_text += f" 🔒 In Active Use: ${total_value - available_value:,.2f}\n"
-
- # Add P&L summary
- basic_stats = self.stats.get_basic_stats()
- if basic_stats['initial_balance'] > 0:
- pnl = total_value - basic_stats['initial_balance']
- pnl_percent = (pnl / basic_stats['initial_balance']) * 100
-
- balance_text += f"\n📊 <b>Performance:</b>\n"
- balance_text += f" 💵 P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
- balance_text += f" 📈 Initial: ${basic_stats['initial_balance']:,.2f}"
- else:
- balance_text += "📭 No balance data available"
- else:
- balance_text = "❌ Could not fetch balance data"
-
- await update.message.reply_text(balance_text, parse_mode='HTML')
-
- async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /positions command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- positions = self.client.get_positions()
-
- if positions is not None: # Successfully fetched (could be empty list)
- positions_text = "📈 <b>Open Positions</b>\n\n"
-
- # Filter for actual open positions
- open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-
- if open_positions:
- total_unrealized = 0
- total_position_value = 0
-
- for position in open_positions:
- symbol = position.get('symbol', 'Unknown')
- contracts = float(position.get('contracts', 0))
- unrealized_pnl = float(position.get('unrealizedPnl', 0))
- entry_price = float(position.get('entryPx', 0))
-
- # Calculate position value and P&L percentage
- position_value = abs(contracts) * entry_price
- pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
-
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-
- # Extract token name for cleaner display
- token = symbol.split('/')[0] if '/' in symbol else symbol
- position_type = "LONG" if contracts > 0 else "SHORT"
-
- positions_text += f"📊 <b>{token}</b> ({position_type})\n"
- positions_text += f" 📏 Size: {abs(contracts):.6f} {token}\n"
- positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
- positions_text += f" 💵 Value: ${position_value:,.2f}\n"
- positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n"
-
- total_unrealized += unrealized_pnl
- total_position_value += position_value
-
- # Calculate overall P&L percentage
- total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
- total_pnl_emoji = "🟢" if total_unrealized >= 0 else "🔴"
-
- positions_text += f"💼 <b>Total Portfolio:</b>\n"
- positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
- positions_text += f" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)"
- else:
- positions_text += "📭 <b>No open positions currently</b>\n\n"
- positions_text += "🚀 Ready to start trading!\n"
- positions_text += "Use /buy or /sell commands to open positions."
- else:
- # Actual API error
- positions_text = "❌ <b>Could not fetch positions data</b>\n\n"
- positions_text += "🔄 Please try again in a moment.\n"
- positions_text += "If the issue persists, check your connection."
-
- await update.message.reply_text(positions_text, parse_mode='HTML')
-
- async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /orders command with optional token filter."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- # Check if token filter is provided
- token_filter = None
- if context.args and len(context.args) >= 1:
- token_filter = context.args[0].upper()
-
- orders = self.client.get_open_orders()
-
- if orders is not None: # Successfully fetched (could be empty list)
- if token_filter:
- orders_text = f"📋 <b>Open Orders - {token_filter}</b>\n\n"
- # Filter orders for specific token
- target_symbol = f"{token_filter}/USDC:USDC"
- filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
- else:
- orders_text = "📋 <b>All Open Orders</b>\n\n"
- filtered_orders = orders
-
- if filtered_orders and len(filtered_orders) > 0:
- for order in filtered_orders:
- symbol = order.get('symbol', 'Unknown')
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
-
- # Extract token from symbol for display
- token = symbol.split('/')[0] if '/' in symbol else symbol
-
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
-
- orders_text += f"{side_emoji} <b>{token}</b>\n"
- orders_text += f" 📊 {side.upper()} {amount} @ ${price:,.2f}\n"
- orders_text += f" 💵 Value: ${float(amount) * float(price):,.2f}\n"
- orders_text += f" 🔑 ID: <code>{order_id}</code>\n\n"
-
- # Add helpful commands
- if token_filter:
- orders_text += f"💡 <b>Quick Actions:</b>\n"
- orders_text += f"• <code>/coo {token_filter}</code> - Cancel all {token_filter} orders\n"
- orders_text += f"• <code>/orders</code> - View all orders"
- else:
- orders_text += f"💡 <b>Filter by token:</b> <code>/orders BTC</code>, <code>/orders ETH</code>"
- else:
- if token_filter:
- orders_text += f"📭 <b>No open orders for {token_filter}</b>\n\n"
- orders_text += f"💡 No pending {token_filter} orders found.\n"
- orders_text += f"Use <code>/long {token_filter} 100</code> or <code>/short {token_filter} 100</code> to create new orders."
- else:
- orders_text += "📭 <b>No open orders currently</b>\n\n"
- orders_text += "💡 All clear! No pending orders.\n"
- orders_text += "Use /long or /short commands to place new orders."
- else:
- # Actual API error
- orders_text = "❌ <b>Could not fetch orders data</b>\n\n"
- orders_text += "🔄 Please try again in a moment.\n"
- orders_text += "If the issue persists, check your connection."
-
- await update.message.reply_text(orders_text, parse_mode='HTML')
-
- async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /market command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- # Check if token is provided as argument
- if context.args and len(context.args) >= 1:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- # Convert token to full symbol format for API
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- 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>Market Data - {token}</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 {token}</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 = f"❌ <b>Could not fetch market data for {token}</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')
-
- async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /price command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- # Check if token is provided as argument
- if context.args and len(context.args) >= 1:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- # Convert token to full symbol format for API
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- 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>{token}</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"❌ <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')
-
- async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle inline keyboard button presses."""
- query = update.callback_query
- await query.answer()
-
- if not self.is_authorized(query.message.chat_id):
- await query.edit_message_text("❌ Unauthorized access.")
- return
-
- callback_data = query.data
-
- # Handle trading confirmations
- if callback_data.startswith('confirm_long_'):
- parts = callback_data.split('_')
- token = parts[2]
- usdc_amount = float(parts[3])
- price = float(parts[4])
- is_limit = len(parts) > 5 and parts[5] == 'limit'
- await self._execute_long_order(query, token, usdc_amount, price, is_limit)
- return
-
- elif callback_data.startswith('confirm_short_'):
- parts = callback_data.split('_')
- token = parts[2]
- usdc_amount = float(parts[3])
- price = float(parts[4])
- is_limit = len(parts) > 5 and parts[5] == 'limit'
- await self._execute_short_order(query, token, usdc_amount, price, is_limit)
- return
-
- elif callback_data.startswith('confirm_exit_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_exit_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data.startswith('confirm_coo_'):
- parts = callback_data.split('_')
- token = parts[2]
- await self._execute_coo(query, token)
- return
-
- elif callback_data.startswith('confirm_sl_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_sl_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data.startswith('confirm_tp_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_tp_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data == 'cancel_order':
- await query.edit_message_text("❌ Order cancelled.")
- return
-
- # Create a fake update object for reusing command handlers
- fake_update = Update(
- update_id=update.update_id,
- message=query.message,
- callback_query=query
- )
-
- # Handle regular button callbacks
- if callback_data == "balance":
- await self.balance_command(fake_update, context)
- elif callback_data == "stats":
- await self.stats_command(fake_update, context)
- elif callback_data == "positions":
- await self.positions_command(fake_update, context)
- elif callback_data == "orders":
- await self.orders_command(fake_update, context)
- elif callback_data == "market":
- await self.market_command(fake_update, context)
- elif callback_data == "price":
- await self.price_command(fake_update, context)
- elif callback_data == "trades":
- await self.trades_command(fake_update, context)
- elif callback_data == "help":
- await self.help_command(fake_update, context)
-
- async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
- """Execute a long order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Opening long position...")
-
- # Calculate token amount based on USDC value and price
- token_amount = usdc_amount / price
-
- # Place order (limit or market)
- if is_limit:
- order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
- else:
- order = self.client.place_market_order(symbol, 'buy', token_amount)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, 'buy', token_amount, actual_price, order_id)
-
- success_message = f"""
- ✅ <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
- 📊 <b>Order Details:</b>
- • Token: {token}
- • Direction: LONG (Buy)
- • Amount: {token_amount:.6f} {token}
- • Price: ${price:,.2f}
- • USDC Value: ~${usdc_amount:,.2f}
- • Order Type: {'Limit' if is_limit else 'Market'} Order
- • Order ID: <code>{order_id}</code>
- 🚀 Your {'limit order has been placed' if is_limit else 'long position is now active'}!
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
- else:
- await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.")
-
- except Exception as e:
- error_message = f"❌ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in long order: {e}")
-
- async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
- """Execute a short order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Opening short position...")
-
- # Calculate token amount based on USDC value and price
- token_amount = usdc_amount / price
-
- # Place order (limit or market)
- if is_limit:
- order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
- else:
- order = self.client.place_market_order(symbol, 'sell', token_amount)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, 'sell', token_amount, actual_price, order_id)
-
- success_message = f"""
- ✅ <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
- 📊 <b>Order Details:</b>
- • Token: {token}
- • Direction: SHORT (Sell)
- • Amount: {token_amount:.6f} {token}
- • Price: ${price:,.2f}
- • USDC Value: ~${usdc_amount:,.2f}
- • Order Type: {'Limit' if is_limit else 'Market'} Order
- • Order ID: <code>{order_id}</code>
- 📉 Your {'limit order has been placed' if is_limit else 'short position is now active'}!
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
- else:
- await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.")
-
- except Exception as e:
- error_message = f"❌ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in short order: {e}")
-
- async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute an exit order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Closing position...")
-
- # Place market order to close position
- order = self.client.place_market_order(symbol, exit_side, contracts)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- ✅ <b>Position Closed Successfully!</b>
- 📊 <b>Exit Details:</b>
- • Token: {token}
- • Position Closed: {position_type}
- • Exit Side: {exit_side.upper()}
- • Amount: {contracts} {token}
- • Est. Price: ~${price:,.2f}
- • Order Type: Market Order
- • Order ID: <code>{order_id}</code>
- 🎯 <b>Position Summary:</b>
- • Status: CLOSED
- • Exit Value: ~${contracts * price:,.2f}
- 📊 Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}")
- else:
- await query.edit_message_text("❌ Failed to close position. Please try again.")
-
- except Exception as e:
- error_message = f"❌ Error closing position: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error closing position: {e}")
-
- async def _execute_coo(self, query, token: str):
- """Execute cancel open orders for a specific token."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Cancelling all orders...")
-
- # Get current orders for this token
- all_orders = self.client.get_open_orders()
- if all_orders is None:
- await query.edit_message_text(f"❌ Could not fetch orders to cancel {token} orders")
- return
-
- # Filter orders for the specific token
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
-
- if not token_orders:
- await query.edit_message_text(f"📭 No open orders found for {token}")
- return
-
- # Cancel each order
- cancelled_orders = []
- failed_orders = []
-
- for order in token_orders:
- order_id = order.get('id')
- if order_id:
- try:
- success = self.client.cancel_order(order_id, symbol)
- if success:
- cancelled_orders.append(order)
- else:
- failed_orders.append(order)
- except Exception as e:
- logger.error(f"Failed to cancel order {order_id}: {e}")
- failed_orders.append(order)
-
- # Create result message
- result_message = f"""
- ✅ <b>Cancel Orders Results</b>
- 📊 <b>Summary:</b>
- • Token: {token}
- • Cancelled: {len(cancelled_orders)} orders
- • Failed: {len(failed_orders)} orders
- • Total Attempted: {len(token_orders)} orders
- """
-
- if cancelled_orders:
- result_message += f"\n🗑️ <b>Successfully Cancelled:</b>\n"
- for order in cancelled_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n"
-
- if failed_orders:
- result_message += f"\n❌ <b>Failed to Cancel:</b>\n"
- for order in failed_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n"
-
- if len(cancelled_orders) == len(token_orders):
- result_message += f"\n🎉 All {token} orders successfully cancelled!"
- elif len(cancelled_orders) > 0:
- result_message += f"\n⚠️ Some orders cancelled. Check failed orders above."
- else:
- result_message += f"\n❌ Could not cancel any {token} orders."
-
- await query.edit_message_text(result_message, parse_mode='HTML')
- logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled")
-
- except Exception as e:
- error_message = f"❌ Error cancelling {token} orders: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in COO execution: {e}")
-
- async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute a stop loss order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Setting stop loss...")
-
- # Place stop loss order
- order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- ✅ <b>Stop Loss Order Set Successfully!</b>
- 📊 <b>Stop Loss Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Size: {contracts} contracts
- • Stop Price: ${price:,.2f}
- • Action: {exit_side.upper()} (Close {position_type})
- • Amount: {contracts} {token}
- • Order Type: Limit Order
- • Order ID: <code>{order_id}</code>
- 🎯 <b>Stop Loss Execution:</b>
- • Status: SET
- • Exit Value: ~${contracts * price:,.2f}
- 📊 Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}")
- else:
- await query.edit_message_text("❌ Failed to set stop loss. Please try again.")
-
- except Exception as e:
- error_message = f"❌ Error setting stop loss: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error setting stop loss: {e}")
-
- async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute a take profit order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Setting take profit...")
-
- # Place take profit order
- order = self.client.place_take_profit_order(symbol, exit_side, contracts, price)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- ✅ <b>Take Profit Order Set Successfully!</b>
- 📊 <b>Take Profit Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Size: {contracts} contracts
- • Target Price: ${price:,.2f}
- • Action: {exit_side.upper()} (Close {position_type})
- • Amount: {contracts} {token}
- • Order Type: Limit Order
- • Order ID: <code>{order_id}</code>
- 🎯 <b>Take Profit Execution:</b>
- • Status: SET
- • Exit Value: ~${contracts * price:,.2f}
- 📊 Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
- else:
- await query.edit_message_text("❌ Failed to set take profit. Please try again.")
-
- except Exception as e:
- error_message = f"❌ Error setting take profit: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error setting take profit: {e}")
-
- async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle unknown commands."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- await update.message.reply_text(
- "❓ Unknown command. Use /help to see available commands or tap the buttons in /start."
- )
-
- def setup_handlers(self):
- """Set up command handlers for the bot."""
- if not self.application:
- return
-
- # Command handlers
- self.application.add_handler(CommandHandler("start", self.start_command))
- self.application.add_handler(CommandHandler("help", self.help_command))
- self.application.add_handler(CommandHandler("balance", self.balance_command))
- self.application.add_handler(CommandHandler("positions", self.positions_command))
- self.application.add_handler(CommandHandler("orders", self.orders_command))
- self.application.add_handler(CommandHandler("market", self.market_command))
- self.application.add_handler(CommandHandler("price", self.price_command))
- self.application.add_handler(CommandHandler("stats", self.stats_command))
- self.application.add_handler(CommandHandler("trades", self.trades_command))
- self.application.add_handler(CommandHandler("long", self.long_command))
- self.application.add_handler(CommandHandler("short", self.short_command))
- self.application.add_handler(CommandHandler("exit", self.exit_command))
- self.application.add_handler(CommandHandler("coo", self.coo_command))
- 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))
- self.application.add_handler(CommandHandler("performance", self.performance_command))
- self.application.add_handler(CommandHandler("daily", self.daily_command))
- self.application.add_handler(CommandHandler("weekly", self.weekly_command))
- self.application.add_handler(CommandHandler("monthly", self.monthly_command))
-
- # Callback query handler for inline keyboards
- self.application.add_handler(CallbackQueryHandler(self.button_callback))
-
- # Handle unknown commands
- self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
-
- async def run(self):
- """Run the Telegram bot."""
- if not Config.TELEGRAM_BOT_TOKEN:
- logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
- return
-
- if not Config.TELEGRAM_CHAT_ID:
- logger.error("❌ TELEGRAM_CHAT_ID not configured")
- return
-
- try:
- # Create application
- self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
-
- # Set up handlers
- self.setup_handlers()
-
- logger.info("🚀 Starting Telegram trading bot...")
-
- # Initialize the application
- await self.application.initialize()
-
- # Send startup notification
- await self.send_message(
- "🤖 <b>Manual Trading Bot Started</b>\n\n"
- f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
- f"📊 Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n"
- f"📱 Manual trading ready!\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()
-
- # Start order monitoring
- await self.start_order_monitoring()
-
- # Start polling for updates manually
- logger.info("🔄 Starting update polling...")
-
- # Get updates in a loop
- last_update_id = 0
- while True:
- try:
- # Get updates from Telegram
- updates = await self.application.bot.get_updates(
- offset=last_update_id + 1,
- timeout=30,
- allowed_updates=None
- )
-
- # Process each update
- for update in updates:
- last_update_id = update.update_id
-
- # Process the update through the application
- await self.application.process_update(update)
-
- except Exception as e:
- logger.error(f"Error processing updates: {e}")
- await asyncio.sleep(5) # Wait before retrying
-
- except asyncio.CancelledError:
- logger.info("🛑 Bot polling cancelled")
- raise
-
- except Exception as e:
- logger.error(f"❌ Error in telegram bot: {e}")
- raise
-
- finally:
- # Clean shutdown
- try:
- await self.stop_order_monitoring()
- if self.application:
- await self.application.stop()
- await self.application.shutdown()
- except Exception as e:
- logger.error(f"Error during shutdown: {e}")
- async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /long command for opening long positions."""
- 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) < 2:
- await update.message.reply_text(
- "❌ Usage: /long [token] [USDC amount] [price (optional)]\n"
- "Examples:\n"
- "• /long BTC 100 - Market order\n"
- "• /long BTC 100 45000 - Limit order at $45,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Check if price is provided for limit order
- limit_price = None
- if len(context.args) >= 3:
- limit_price = float(context.args[2])
- order_type = "Limit"
- order_description = f"at ${limit_price:,.2f}"
- else:
- order_type = "Market"
- order_description = "at current market price"
-
- # Convert token to full symbol format for Hyperliquid
- symbol = f"{token}/USDC:USDC"
-
- # Get current market price to calculate amount and for display
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- await update.message.reply_text(f"❌ Could not fetch price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"❌ Invalid price for {token}")
- return
-
- # Calculate token amount based on price (market or limit)
- calculation_price = limit_price if limit_price else current_price
- token_amount = usdc_amount / calculation_price
-
- # Create confirmation message
- confirmation_text = f"""
- 🟢 <b>Long Position Confirmation</b>
- 📊 <b>Order Details:</b>
- • Token: {token}
- • Direction: LONG (Buy)
- • USDC Value: ${usdc_amount:,.2f}
- • Current Price: ${current_price:,.2f}
- • Order Type: {order_type} Order
- • Token Amount: {token_amount:.6f} {token}
- 🎯 <b>Execution:</b>
- • Will buy {token_amount:.6f} {token} {order_description}
- • Est. Value: ${token_amount * calculation_price:,.2f}
- ⚠️ <b>Are you sure you want to open this long position?</b>
- """
-
- # Use limit_price for callback if provided, otherwise current_price
- callback_price = limit_price if limit_price else current_price
- callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}"
- if limit_price:
- callback_data += "_limit"
-
- keyboard = [
- [
- InlineKeyboardButton("✅ Confirm Long", callback_data=callback_data),
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing long command: {e}")
-
- async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /short command for opening short positions."""
- 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) < 2:
- await update.message.reply_text(
- "❌ Usage: /short [token] [USDC amount] [price (optional)]\n"
- "Examples:\n"
- "• /short BTC 100 - Market order\n"
- "• /short BTC 100 46000 - Limit order at $46,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Check if price is provided for limit order
- limit_price = None
- if len(context.args) >= 3:
- limit_price = float(context.args[2])
- order_type = "Limit"
- order_description = f"at ${limit_price:,.2f}"
- else:
- order_type = "Market"
- order_description = "at current market price"
-
- # Convert token to full symbol format for Hyperliquid
- symbol = f"{token}/USDC:USDC"
-
- # Get current market price to calculate amount and for display
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- await update.message.reply_text(f"❌ Could not fetch price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"❌ Invalid price for {token}")
- return
-
- # Calculate token amount based on price (market or limit)
- calculation_price = limit_price if limit_price else current_price
- token_amount = usdc_amount / calculation_price
-
- # Create confirmation message
- confirmation_text = f"""
- 🔴 <b>Short Position Confirmation</b>
- 📊 <b>Order Details:</b>
- • Token: {token}
- • Direction: SHORT (Sell)
- • USDC Value: ${usdc_amount:,.2f}
- • Current Price: ${current_price:,.2f}
- • Order Type: {order_type} Order
- • Token Amount: {token_amount:.6f} {token}
- 🎯 <b>Execution:</b>
- • Will sell {token_amount:.6f} {token} {order_description}
- • Est. Value: ${token_amount * calculation_price:,.2f}
- ⚠️ <b>Are you sure you want to open this short position?</b>
- """
-
- # Use limit_price for callback if provided, otherwise current_price
- callback_price = limit_price if limit_price else current_price
- callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}"
- if limit_price:
- callback_data += "_limit"
-
- keyboard = [
- [
- InlineKeyboardButton("✅ Confirm Short", callback_data=callback_data),
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing short command: {e}")
-
- async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /exit command for closing positions."""
- 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) < 1:
- await update.message.reply_text(
- "❌ Usage: /exit [token]\n"
- "Example: /exit BTC"
- )
- return
-
- token = context.args[0].upper()
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"📭 No open position found for {token}")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and exit details
- if contracts > 0:
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "🔴"
- else:
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "🟢"
- contracts = abs(contracts) # Make positive for display
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- 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
-
- # Calculate estimated exit value
- exit_value = contracts * current_price
-
- # Create confirmation message
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-
- confirmation_text = f"""
- {exit_emoji} <b>Exit Position Confirmation</b>
- 📊 <b>Position Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Size: {contracts} contracts
- • Entry Price: ${entry_price:,.2f}
- • Current Price: ${current_price:,.2f}
- • {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
- 🎯 <b>Exit Order:</b>
- • Action: {exit_side.upper()} (Close {position_type})
- • Amount: {contracts} {token}
- • Est. Value: ~${exit_value:,.2f}
- • Order Type: Market Order
- ⚠️ <b>Are you sure you want to close this {position_type} position?</b>
- This will place a market {exit_side} order to close your entire {token} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"),
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing exit command: {e}")
- async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /coo (cancel open orders) command for a specific token."""
- 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) < 1:
- await update.message.reply_text(
- "❌ Usage: /coo [token]\n"
- "Example: /coo BTC\n\n"
- "This command cancels ALL open orders for the specified token."
- )
- return
-
- token = context.args[0].upper()
- symbol = f"{token}/USDC:USDC"
-
- # Get current orders for this token
- all_orders = self.client.get_open_orders()
- if all_orders is None:
- await update.message.reply_text(f"❌ Could not fetch orders to cancel {token} orders")
- return
-
- # Filter orders for the specific token
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
-
- if not token_orders:
- await update.message.reply_text(f"📭 No open orders found for {token}")
- return
-
- # Create confirmation message with order details
- confirmation_text = f"""
- ⚠️ <b>Cancel All {token} Orders</b>
- 📋 <b>Orders to Cancel:</b>
- """
-
- total_value = 0
- for order in token_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
-
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- order_value = float(amount) * float(price)
- total_value += order_value
-
- confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n"
-
- confirmation_text += f"""
- 💰 <b>Total Value:</b> ${total_value:,.2f}
- 🔢 <b>Orders Count:</b> {len(token_orders)}
- ⚠️ <b>Are you sure you want to cancel ALL {token} orders?</b>
- This action cannot be undone.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"✅ Cancel All {token}", callback_data=f"confirm_coo_{token}"),
- InlineKeyboardButton("❌ Keep Orders", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing cancel orders command: {e}")
- async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /sl (stop loss) command for setting stop loss orders."""
- 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) < 2:
- await update.message.reply_text(
- "❌ Usage: /sl [token] [price]\n"
- "Example: /sl BTC 44000\n\n"
- "This creates a stop loss order at the specified price."
- )
- return
-
- token = context.args[0].upper()
- stop_price = float(context.args[1])
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a stop loss.")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and validate stop loss price
- if contracts > 0:
- # Long position - stop loss should be below entry price
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "🔴"
- contracts_abs = contracts
-
- if stop_price >= entry_price:
- await update.message.reply_text(
- f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
- f"📊 Your {token} LONG position:\n"
- f"• Entry Price: ${entry_price:,.2f}\n"
- f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
- f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
- )
- return
- else:
- # Short position - stop loss should be above entry price
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "🟢"
- contracts_abs = abs(contracts)
-
- if stop_price <= entry_price:
- await update.message.reply_text(
- f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
- f"📊 Your {token} SHORT position:\n"
- f"• Entry Price: ${entry_price:,.2f}\n"
- f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
- f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
- )
- return
-
- # Get current market price for reference
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- # Calculate estimated P&L at stop loss
- if contracts > 0: # Long position
- pnl_at_stop = (stop_price - entry_price) * contracts_abs
- else: # Short position
- pnl_at_stop = (entry_price - stop_price) * contracts_abs
-
- # Create confirmation message
- pnl_emoji = "🟢" if pnl_at_stop >= 0 else "🔴"
-
- confirmation_text = f"""
- 🛑 <b>Stop Loss Order Confirmation</b>
- 📊 <b>Position Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Size: {contracts_abs} contracts
- • Entry Price: ${entry_price:,.2f}
- • Current Price: ${current_price:,.2f}
- 🎯 <b>Stop Loss Order:</b>
- • Stop Price: ${stop_price:,.2f}
- • Action: {exit_side.upper()} (Close {position_type})
- • Amount: {contracts_abs} {token}
- • Order Type: Limit Order
- • {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
- ⚠️ <b>Are you sure you want to set this stop loss?</b>
- This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"✅ Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"),
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing stop loss command: {e}")
- async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /tp (take profit) command for setting take profit orders."""
- 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) < 2:
- await update.message.reply_text(
- "❌ Usage: /tp [token] [price]\n"
- "Example: /tp BTC 50000\n\n"
- "This creates a take profit order at the specified price."
- )
- return
-
- token = context.args[0].upper()
- profit_price = float(context.args[1])
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a take profit.")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and validate take profit price
- if contracts > 0:
- # Long position - take profit should be above entry price
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "🔴"
- contracts_abs = contracts
-
- if profit_price <= entry_price:
- await update.message.reply_text(
- f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n"
- f"📊 Your {token} LONG position:\n"
- f"• Entry Price: ${entry_price:,.2f}\n"
- f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
- f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
- )
- return
- else:
- # Short position - take profit should be below entry price
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "🟢"
- contracts_abs = abs(contracts)
-
- if profit_price >= entry_price:
- await update.message.reply_text(
- f"⚠️ Take profit price should be BELOW entry price for short positions\n\n"
- f"📊 Your {token} SHORT position:\n"
- f"• Entry Price: ${entry_price:,.2f}\n"
- f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
- f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
- )
- return
-
- # Get current market price for reference
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- # Calculate estimated P&L at take profit
- if contracts > 0: # Long position
- pnl_at_tp = (profit_price - entry_price) * contracts_abs
- else: # Short position
- pnl_at_tp = (entry_price - profit_price) * contracts_abs
-
- # Create confirmation message
- pnl_emoji = "🟢" if pnl_at_tp >= 0 else "🔴"
-
- confirmation_text = f"""
- 🎯 <b>Take Profit Order Confirmation</b>
- 📊 <b>Position Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Size: {contracts_abs} contracts
- • Entry Price: ${entry_price:,.2f}
- • Current Price: ${current_price:,.2f}
- 💰 <b>Take Profit Order:</b>
- • Target Price: ${profit_price:,.2f}
- • Action: {exit_side.upper()} (Close {position_type})
- • Amount: {contracts_abs} {token}
- • Order Type: Limit Order
- • {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
- ⚠️ <b>Are you sure you want to set this take profit?</b>
- This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"✅ Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"),
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"❌ Error processing take profit command: {e}")
- async def start_order_monitoring(self):
- """Start the order monitoring background task."""
- if self.monitoring_active:
- return
-
- self.monitoring_active = True
- logger.info("🔄 Starting order monitoring...")
-
- # Initialize tracking data
- await self._initialize_order_tracking()
-
- # Start monitoring loop
- asyncio.create_task(self._order_monitoring_loop())
-
- async def stop_order_monitoring(self):
- """Stop the order monitoring background task."""
- self.monitoring_active = False
- logger.info("⏹️ Stopping order monitoring...")
-
- async def _initialize_order_tracking(self):
- """Initialize order and position tracking."""
- try:
- # Get current open orders to initialize tracking
- orders = self.client.get_open_orders()
- if orders:
- self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
- logger.info(f"📋 Initialized tracking with {len(self.last_known_orders)} open orders")
-
- # Get current positions for P&L tracking
- positions = self.client.get_positions()
- if positions:
- for position in positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if symbol and contracts != 0:
- self.last_known_positions[symbol] = {
- 'contracts': contracts,
- 'entry_price': entry_price
- }
- logger.info(f"📊 Initialized tracking with {len(self.last_known_positions)} positions")
-
- except Exception as e:
- logger.error(f"❌ Error initializing order tracking: {e}")
-
- async def _order_monitoring_loop(self):
- """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
- while self.monitoring_active:
- try:
- await self._check_order_fills()
- 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(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."""
- try:
- # Get current orders and positions
- current_orders = self.client.get_open_orders() or []
- current_positions = self.client.get_positions() or []
-
- # Get current order IDs
- current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
-
- # Find filled orders (orders that were in last_known_orders but not in current_orders)
- filled_order_ids = self.last_known_orders - current_order_ids
-
- if filled_order_ids:
- logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders")
- await self._process_filled_orders(filled_order_ids, current_positions)
-
- # Update tracking data
- 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()
-
- # Check stop losses (if risk management is enabled)
- if Config.RISK_MANAGEMENT_ENABLED:
- await self._check_stop_losses(current_positions)
-
- 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:
- # Convert timestamps to comparable format
- try:
- # Convert fill_time to string if it's not already
- if isinstance(fill_time, (int, float)):
- # Assume it's a unix timestamp
- fill_time_str = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time).isoformat()
- else:
- fill_time_str = str(fill_time)
-
- # Compare as strings
- if fill_time_str > self.last_processed_trade_time:
- new_trades.append(fill)
- if fill_time_str > latest_trade_time:
- latest_trade_time = fill_time_str
- except Exception as timestamp_error:
- logger.warning(f"⚠️ Error processing timestamp {fill_time}: {timestamp_error}")
- continue
-
- 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 _check_stop_losses(self, current_positions: list):
- """Check all positions for stop loss triggers and execute automatic exits."""
- try:
- if not current_positions:
- return
-
- stop_loss_triggers = []
-
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if not symbol or contracts == 0 or entry_price == 0:
- continue
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- if not market_data or not market_data.get('ticker'):
- continue
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price == 0:
- continue
-
- # Calculate current P&L percentage
- if contracts > 0: # Long position
- pnl_percent = ((current_price - entry_price) / entry_price) * 100
- else: # Short position
- pnl_percent = ((entry_price - current_price) / entry_price) * 100
-
- # Check if stop loss should trigger
- if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE:
- token = symbol.split('/')[0] if '/' in symbol else symbol
- stop_loss_triggers.append({
- 'symbol': symbol,
- 'token': token,
- 'contracts': contracts,
- 'entry_price': entry_price,
- 'current_price': current_price,
- 'pnl_percent': pnl_percent
- })
-
- # Execute stop losses
- for trigger in stop_loss_triggers:
- await self._execute_automatic_stop_loss(trigger)
-
- except Exception as e:
- logger.error(f"❌ Error checking stop losses: {e}")
- async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]):
- """Execute an automatic stop loss order."""
- try:
- symbol = trigger['symbol']
- token = trigger['token']
- contracts = trigger['contracts']
- entry_price = trigger['entry_price']
- current_price = trigger['current_price']
- pnl_percent = trigger['pnl_percent']
-
- # Determine the exit side (opposite of position)
- exit_side = 'sell' if contracts > 0 else 'buy'
- contracts_abs = abs(contracts)
-
- # Send notification before executing
- await self._send_stop_loss_notification(trigger, "triggered")
-
- # Execute the stop loss order (market order for immediate execution)
- try:
- if exit_side == 'sell':
- order = self.client.create_market_sell_order(symbol, contracts_abs)
- else:
- order = self.client.create_market_buy_order(symbol, contracts_abs)
-
- if order:
- logger.info(f"🛑 Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}")
-
- # Record the trade in stats
- self.stats.record_trade(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'))
-
- # Send success notification
- await self._send_stop_loss_notification(trigger, "executed", order)
- else:
- logger.error(f"❌ Stop loss order failed for {token}")
- await self._send_stop_loss_notification(trigger, "failed")
-
- except Exception as order_error:
- logger.error(f"❌ Stop loss order execution failed for {token}: {order_error}")
- await self._send_stop_loss_notification(trigger, "failed", error=str(order_error))
-
- except Exception as e:
- logger.error(f"❌ Error executing automatic stop loss: {e}")
- async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None):
- """Send notification for stop loss events."""
- try:
- token = trigger['token']
- contracts = trigger['contracts']
- entry_price = trigger['entry_price']
- current_price = trigger['current_price']
- pnl_percent = trigger['pnl_percent']
-
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- if status == "triggered":
- title = "🛑 Stop Loss Triggered"
- status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss"
- emoji = "🚨"
- elif status == "executed":
- title = "✅ Stop Loss Executed"
- status_text = "Position closed automatically"
- emoji = "🛑"
- elif status == "failed":
- title = "❌ Stop Loss Failed"
- status_text = f"Stop loss execution failed{': ' + error if error else ''}"
- emoji = "⚠️"
- else:
- return
-
- # Calculate loss
- loss_value = contracts_abs * abs(current_price - entry_price)
-
- message = f"""
- {title}
- {emoji} <b>Risk Management Alert</b>
- 📊 <b>Position Details:</b>
- • Token: {token}
- • Direction: {position_type}
- • Size: {contracts_abs} contracts
- • Entry Price: ${entry_price:,.2f}
- • Current Price: ${current_price:,.2f}
- 🔴 <b>Loss Details:</b>
- • Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
- • Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
- 📋 <b>Action:</b> {status_text}
- ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- """
-
- if order and status == "executed":
- order_id = order.get('id', 'N/A')
- message += f"\n🆔 <b>Order ID:</b> {order_id}"
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent stop loss notification: {token} {status}")
-
- except Exception as e:
- logger.error(f"❌ Error sending stop loss 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:
- # Create a map of current positions
- current_position_map = {}
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- if symbol:
- current_position_map[symbol] = contracts
-
- # For each symbol, check if position size changed
- for symbol, old_position_data in self.last_known_positions.items():
- old_contracts = old_position_data['contracts']
- current_contracts = current_position_map.get(symbol, 0)
-
- if old_contracts != current_contracts:
- # Position changed - determine if it's open or close
- await self._handle_position_change(symbol, old_position_data, current_contracts)
-
- # Check for new positions (symbols not in last_known_positions)
- for symbol, current_contracts in current_position_map.items():
- if symbol not in self.last_known_positions and current_contracts != 0:
- # New position opened
- await self._handle_new_position(symbol, current_contracts)
-
- except Exception as e:
- logger.error(f"❌ Error processing filled orders: {e}")
-
- async def _handle_position_change(self, symbol: str, old_position_data: dict, current_contracts: float):
- """Handle when an existing position changes size."""
- old_contracts = old_position_data['contracts']
- old_entry_price = old_position_data['entry_price']
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- token = symbol.split('/')[0] if '/' in symbol else symbol
-
- if current_contracts == 0 and old_contracts != 0:
- # Position closed
- await self._send_close_trade_notification(token, old_contracts, old_entry_price, current_price)
- elif abs(current_contracts) > abs(old_contracts):
- # Position increased
- added_contracts = current_contracts - old_contracts
- await self._send_open_trade_notification(token, added_contracts, current_price, "increased")
- elif abs(current_contracts) < abs(old_contracts):
- # Position decreased (partial close)
- closed_contracts = old_contracts - current_contracts
- await self._send_partial_close_notification(token, closed_contracts, old_entry_price, current_price)
-
- async def _handle_new_position(self, symbol: str, contracts: float):
- """Handle when a new position is opened."""
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- token = symbol.split('/')[0] if '/' in symbol else symbol
- await self._send_open_trade_notification(token, contracts, current_price, "opened")
-
- async def _update_position_tracking(self, current_positions: list):
- """Update the position tracking data."""
- new_position_map = {}
-
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if symbol and contracts != 0:
- new_position_map[symbol] = {
- 'contracts': contracts,
- 'entry_price': entry_price
- }
-
- self.last_known_positions = new_position_map
-
- async def _send_open_trade_notification(self, token: str, contracts: float, price: float, action: str):
- """Send notification for opened/increased position."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
- value = contracts_abs * price
-
- if action == "opened":
- title = "🚀 Position Opened"
- action_text = f"New {position_type} position opened"
- else:
- title = "📈 Position Increased"
- action_text = f"{position_type} position increased"
-
- message = f"""
- {title}
- 📊 <b>Trade Details:</b>
- • Token: {token}
- • Direction: {position_type}
- • Size: {contracts_abs} contracts
- • Entry Price: ${price:,.2f}
- • Value: ${value:,.2f}
- ✅ <b>Status:</b> {action_text}
- ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- 📱 Use /positions to view all positions
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent open trade notification: {token} {position_type} {contracts_abs} @ ${price}")
-
- async def _send_close_trade_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
- """Send notification for closed position with P&L."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- # Calculate P&L
- if contracts > 0: # Long position
- pnl = (exit_price - entry_price) * contracts_abs
- else: # Short position
- pnl = (entry_price - exit_price) * contracts_abs
-
- pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- exit_value = contracts_abs * exit_price
-
- message = f"""
- 🎯 <b>Position Closed</b>
- 📊 <b>Trade Summary:</b>
- • Token: {token}
- • Direction: {position_type}
- • Size: {contracts_abs} contracts
- • Entry Price: ${entry_price:,.2f}
- • Exit Price: ${exit_price:,.2f}
- • Exit Value: ${exit_value:,.2f}
- {pnl_emoji} <b>Profit & Loss:</b>
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- ✅ <b>Status:</b> Position fully closed
- ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- 📊 Use /stats to view updated performance
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent close trade notification: {token} {position_type} P&L: ${pnl:.2f}")
-
- async def _send_partial_close_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
- """Send notification for partially closed position."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- # Calculate P&L for closed portion
- if contracts > 0: # Long position
- pnl = (exit_price - entry_price) * contracts_abs
- else: # Short position
- pnl = (entry_price - exit_price) * contracts_abs
-
- pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- message = f"""
- 📉 <b>Position Partially Closed</b>
- 📊 <b>Partial Close Details:</b>
- • Token: {token}
- • Direction: {position_type}
- • Closed Size: {contracts_abs} contracts
- • Entry Price: ${entry_price:,.2f}
- • Exit Price: ${exit_price:,.2f}
- {pnl_emoji} <b>Partial P&L:</b>
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- ✅ <b>Status:</b> Partial position closed
- ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- 📈 Use /positions to view remaining position
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent partial close notification: {token} {position_type} Partial P&L: ${pnl:.2f}")
- async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /monitoring command to show monitoring status."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- # Get alarm statistics
- alarm_stats = self.alarm_manager.get_statistics()
-
- status_text = f"""
- 🔄 <b>System Monitoring Status</b>
- 📊 <b>Order Monitoring:</b>
- • Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
- • 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>Risk Management:</b>
- • Automatic Stop Loss: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
- • Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
- • Position Monitoring: {'✅ Active' if Config.RISK_MANAGEMENT_ENABLED else '❌ Inactive'}
- 📈 <b>Notifications:</b>
- • 🚀 Position Opened/Increased
- • 📉 Position Partially/Fully Closed
- • 🎯 P&L Calculations
- • 🔔 Price Alarm Triggers
- • 🔄 External Trade Detection
- • 🛑 Automatic Stop Loss Triggers
- ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
- 💡 <b>Monitoring Features:</b>
- • 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 performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /performance command to show token performance ranking or detailed stats."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Check if specific token is requested
- if context.args and len(context.args) >= 1:
- # Detailed performance for specific token
- token = context.args[0].upper()
- await self._show_token_performance(update, token)
- else:
- # Show token performance ranking
- await self._show_performance_ranking(update)
-
- except Exception as e:
- error_message = f"❌ Error processing performance command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in performance command: {e}")
- async def _show_performance_ranking(self, update: Update):
- """Show token performance ranking (compressed view)."""
- token_performance = self.stats.get_token_performance()
-
- if not token_performance:
- await update.message.reply_text(
- "📊 <b>Token Performance</b>\n\n"
- "📭 No trading data available yet.\n\n"
- "💡 Performance tracking starts after your first completed trades.\n"
- "Use /long or /short to start trading!",
- parse_mode='HTML'
- )
- return
-
- # Sort tokens by total P&L (best to worst)
- sorted_tokens = sorted(
- token_performance.items(),
- key=lambda x: x[1]['total_pnl'],
- reverse=True
- )
-
- performance_text = "🏆 <b>Token Performance Ranking</b>\n\n"
-
- # Add ranking with emojis
- for i, (token, stats) in enumerate(sorted_tokens, 1):
- # Ranking emoji
- if i == 1:
- rank_emoji = "🥇"
- elif i == 2:
- rank_emoji = "🥈"
- elif i == 3:
- rank_emoji = "🥉"
- else:
- rank_emoji = f"#{i}"
-
- # P&L emoji
- pnl_emoji = "🟢" if stats['total_pnl'] >= 0 else "🔴"
-
- # Format the line
- performance_text += f"{rank_emoji} <b>{token}</b>\n"
- performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n"
- performance_text += f" 📊 Trades: {stats['completed_trades']}"
-
- # Add win rate if there are completed trades
- if stats['completed_trades'] > 0:
- performance_text += f" | Win: {stats['win_rate']:.0f}%"
-
- performance_text += "\n\n"
-
- # Add summary
- total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
- total_trades = sum(stats['completed_trades'] for stats in token_performance.values())
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-
- performance_text += f"💼 <b>Portfolio Summary:</b>\n"
- performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
- performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
-
- performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
-
- await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
- async def _show_token_performance(self, update: Update, token: str):
- """Show detailed performance for a specific token."""
- token_stats = self.stats.get_token_detailed_stats(token)
-
- # Check if token has any data
- if token_stats.get('total_trades', 0) == 0:
- await update.message.reply_text(
- f"📊 <b>{token} Performance</b>\n\n"
- f"📭 No trading history found for {token}.\n\n"
- f"💡 Start trading {token} with:\n"
- f"• <code>/long {token} 100</code>\n"
- f"• <code>/short {token} 100</code>\n\n"
- f"🔄 Use <code>/performance</code> to see all token rankings.",
- parse_mode='HTML'
- )
- return
-
- # Check if there's a message (no completed trades)
- if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
- await update.message.reply_text(
- f"📊 <b>{token} Performance</b>\n\n"
- f"{token_stats['message']}\n\n"
- f"📈 <b>Current Activity:</b>\n"
- f"• Total Trades: {token_stats['total_trades']}\n"
- f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
- f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
- f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
- f"💡 Complete some trades to see P&L statistics!\n"
- f"🔄 Use <code>/performance</code> to see all token rankings.",
- parse_mode='HTML'
- )
- return
-
- # Detailed stats display
- pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
-
- performance_text = f"""
- 📊 <b>{token} Detailed Performance</b>
- 💰 <b>P&L Summary:</b>
- • {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
- • 💵 Total Volume: ${token_stats['completed_volume']:,.2f}
- • 📈 Expectancy: ${token_stats['expectancy']:,.2f}
- 📊 <b>Trading Activity:</b>
- • Total Trades: {token_stats['total_trades']}
- • Completed: {token_stats['completed_trades']}
- • Buy Orders: {token_stats['buy_trades']}
- • Sell Orders: {token_stats['sell_trades']}
- 🏆 <b>Performance Metrics:</b>
- • Win Rate: {token_stats['win_rate']:.1f}%
- • Profit Factor: {token_stats['profit_factor']:.2f}
- • Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
- 💡 <b>Best/Worst:</b>
- • Largest Win: ${token_stats['largest_win']:,.2f}
- • Largest Loss: ${token_stats['largest_loss']:,.2f}
- • Avg Win: ${token_stats['avg_win']:,.2f}
- • Avg Loss: ${token_stats['avg_loss']:,.2f}
- """
-
- # Add recent trades if available
- if token_stats.get('recent_trades'):
- performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
- for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
- trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
- side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
- pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
-
- performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
-
- performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
-
- await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
- async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /daily command to show daily performance stats."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- daily_stats = self.stats.get_daily_stats(10)
-
- if not daily_stats:
- await update.message.reply_text(
- "📅 <b>Daily Performance</b>\n\n"
- "📭 No daily performance data available yet.\n\n"
- "💡 Daily stats are calculated from completed trades.\n"
- "Start trading to see daily performance!",
- parse_mode='HTML'
- )
- return
-
- daily_text = "📅 <b>Daily Performance (Last 10 Days)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
-
- for day_stats in daily_stats:
- pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
-
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
- daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
-
- total_pnl += day_stats['pnl']
- total_trades += day_stats['trades']
-
- # Add summary
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
- daily_text += f"💼 <b>10-Day Summary:</b>\n"
- daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- daily_text += f" 🔄 Total Trades: {total_trades}\n"
- daily_text += f" 📊 Avg per Day: ${total_pnl/len(daily_stats):,.2f}"
-
- await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing daily command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in daily command: {e}")
- async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /weekly command to show weekly performance stats."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- weekly_stats = self.stats.get_weekly_stats(10)
-
- if not weekly_stats:
- await update.message.reply_text(
- "📊 <b>Weekly Performance</b>\n\n"
- "📭 No weekly performance data available yet.\n\n"
- "💡 Weekly stats are calculated from completed trades.\n"
- "Start trading to see weekly performance!",
- parse_mode='HTML'
- )
- return
-
- weekly_text = "📊 <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
-
- for week_stats in weekly_stats:
- pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
-
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
- weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
-
- total_pnl += week_stats['pnl']
- total_trades += week_stats['trades']
-
- # Add summary
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
- weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- weekly_text += f" 🔄 Total Trades: {total_trades}\n"
- weekly_text += f" 📊 Avg per Week: ${total_pnl/len(weekly_stats):,.2f}"
-
- await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing weekly command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in weekly command: {e}")
- async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /monthly command to show monthly performance stats."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- monthly_stats = self.stats.get_monthly_stats(10)
-
- if not monthly_stats:
- await update.message.reply_text(
- "📆 <b>Monthly Performance</b>\n\n"
- "📭 No monthly performance data available yet.\n\n"
- "💡 Monthly stats are calculated from completed trades.\n"
- "Start trading to see monthly performance!",
- parse_mode='HTML'
- )
- return
-
- monthly_text = "📆 <b>Monthly Performance (Last 10 Months)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
-
- for month_stats in monthly_stats:
- pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
-
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
- monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
-
- total_pnl += month_stats['pnl']
- total_trades += month_stats['trades']
-
- # Add summary
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
- monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- monthly_text += f" 🔄 Total Trades: {total_trades}\n"
- monthly_text += f" 📊 Avg per Month: ${total_pnl/len(monthly_stats):,.2f}"
-
- await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing monthly command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in monthly command: {e}")
- async def main_async():
- """Async main entry point for the Telegram bot."""
- try:
- # Validate configuration
- if not Config.validate():
- logger.error("❌ Configuration validation failed!")
- return
-
- if not Config.TELEGRAM_ENABLED:
- logger.error("❌ Telegram is not enabled in configuration")
- return
-
- # Create and run the bot
- bot = TelegramTradingBot()
- await bot.run()
-
- except KeyboardInterrupt:
- logger.info("👋 Bot stopped by user")
- except Exception as e:
- logger.error(f"❌ Unexpected error: {e}")
- raise
- def main():
- """Main entry point for the Telegram bot."""
- try:
- # Check if we're already in an asyncio context
- try:
- loop = asyncio.get_running_loop()
- # If we get here, we're already in an asyncio context
- logger.error("❌ Cannot run main() from within an asyncio context. Use main_async() instead.")
- return
- except RuntimeError:
- # No running loop, safe to use asyncio.run()
- pass
-
- # Run the async main function
- asyncio.run(main_async())
-
- except Exception as e:
- logger.error(f"❌ Failed to start telegram bot: {e}")
- raise
- if __name__ == "__main__":
- main()
|