1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454 |
- #!/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 trading bot for manual trading operations."""
-
- def __init__(self):
- """Initialize the Telegram trading bot."""
- self.client = HyperliquidClient()
- self.application = None
- self.order_monitoring_task = None
- self.last_filled_orders = set()
- self.alarms = [] # List to store price alarms
- self.bot_heartbeat_seconds = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10)
- self.external_trade_timestamps = set() # Track external trade timestamps to avoid duplicates
- self.last_position_check = {} # Track last position state for comparison
- self._position_tracker = {} # For enhanced position tracking
- self.stats = None
- self.version = "Unknown" # Will be set by launcher
-
- # Initialize stats
- 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(Config.TELEGRAM_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 Config.TELEGRAM_CHAT_ID:
- try:
- await self.application.bot.send_message(
- chat_id=Config.TELEGRAM_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 = f"""
- 🤖 <b>Welcome to Hyperliquid Trading Bot</b>
- 📱 <b>Quick Actions:</b>
- • Trading: /long BTC 100 or /short ETH 50
- • Exit: /exit BTC (closes position)
- • Info: /balance, /positions, /orders
- 📊 <b>Market Data:</b>
- • /market - Detailed market overview
- • /price - Quick price check
- <b>⚡ Quick Commands:</b>
- • /balance - Account balance
- • /positions - Open positions
- • /orders - Active orders
- • /market - Market data & prices
- <b>🚀 Trading:</b>
- • /long BTC 100 - Long position
- • /short ETH 50 - Short position
- • /exit BTC - Close position
- • /coo BTC - Cancel open orders
- <b>🛡️ Risk Management:</b>
- • Enabled: {risk_enabled}
- • Auto Stop Loss: {stop_loss}%
- • /sl BTC 44000 - Manual stop loss
- • /tp BTC 50000 - Take profit order
- <b>📈 Performance & 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
- • /version - Bot version & system information
- • /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>🔄 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
- <b>💡 Quick Access:</b>
- • /commands or /c - One-tap button menu for all commands
- • Buttons below for instant access to key functions
- 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
- • /version - Bot version & system information
- • /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 commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /commands and /c command with quick action buttons."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- commands_text = """
- 📱 <b>Quick Commands</b>
- Tap any button below for instant access to bot functions:
- 💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
- """
-
- keyboard = [
- [
- InlineKeyboardButton("💰 Balance", callback_data="balance"),
- InlineKeyboardButton("📈 Positions", callback_data="positions")
- ],
- [
- InlineKeyboardButton("📋 Orders", callback_data="orders"),
- InlineKeyboardButton("📊 Stats", callback_data="stats")
- ],
- [
- InlineKeyboardButton("💵 Price", callback_data="price"),
- InlineKeyboardButton("📊 Market", callback_data="market")
- ],
- [
- InlineKeyboardButton("🏆 Performance", callback_data="performance"),
- InlineKeyboardButton("🔔 Alarms", callback_data="alarm")
- ],
- [
- InlineKeyboardButton("📅 Daily", callback_data="daily"),
- InlineKeyboardButton("📊 Weekly", callback_data="weekly")
- ],
- [
- InlineKeyboardButton("📆 Monthly", callback_data="monthly"),
- InlineKeyboardButton("🔄 Trades", callback_data="trades")
- ],
- [
- InlineKeyboardButton("🔄 Monitoring", callback_data="monitoring"),
- InlineKeyboardButton("📝 Logs", callback_data="logs")
- ],
- [
- InlineKeyboardButton("⚙️ Help", callback_data="help")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)
-
- 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)
- elif callback_data == "performance":
- await self.performance_command(fake_update, context)
- elif callback_data == "alarm":
- await self.alarm_command(fake_update, context)
- elif callback_data == "daily":
- await self.daily_command(fake_update, context)
- elif callback_data == "weekly":
- await self.weekly_command(fake_update, context)
- elif callback_data == "monthly":
- await self.monthly_command(fake_update, context)
- elif callback_data == "monitoring":
- await self.monitoring_command(fake_update, context)
- elif callback_data == "logs":
- await self.logs_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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
-
- 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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
-
- 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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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("commands", self.commands_command))
- self.application.add_handler(CommandHandler("c", self.commands_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))
- self.application.add_handler(CommandHandler("risk", self.risk_command))
- self.application.add_handler(CommandHandler("version", self.version_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(
- f"🤖 <b>Manual Trading Bot v{self.version} 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 and determine if it's opening or closing a position."""
- 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 and get action type using enhanced tracking
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external")
-
- # Send enhanced notification based on action type
- await self._send_enhanced_trade_notification(symbol, side, amount, price, action_type, timestamp)
-
- logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price} ({action_type})")
-
- except Exception as e:
- logger.error(f"❌ Error processing external trade: {e}")
- async def _send_enhanced_trade_notification(self, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str = None):
- """Send enhanced trade notification based on position action type."""
- try:
- token = symbol.split('/')[0] if '/' in symbol else symbol
- position = self.stats.get_enhanced_position_state(symbol)
-
- if timestamp is None:
- time_str = datetime.now().strftime('%H:%M:%S')
- else:
- try:
- time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
- time_str = time_obj.strftime('%H:%M:%S')
- except:
- time_str = "Unknown"
-
- # Handle different action types
- if action_type in ['long_opened', 'short_opened']:
- await self._send_position_opened_notification(token, side, amount, price, action_type, time_str)
-
- elif action_type in ['long_increased', 'short_increased']:
- await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str)
-
- elif action_type in ['long_reduced', 'short_reduced']:
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
- await self._send_position_reduced_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
-
- elif action_type in ['long_closed', 'short_closed']:
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
- await self._send_position_closed_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
-
- elif action_type in ['long_closed_and_short_opened', 'short_closed_and_long_opened']:
- await self._send_position_flipped_notification(token, side, amount, price, action_type, time_str)
-
- else:
- # Fallback to generic notification
- await self._send_external_trade_notification({
- 'symbol': symbol,
- 'side': side,
- 'amount': amount,
- 'price': price,
- 'timestamp': timestamp or datetime.now().isoformat()
- })
-
- except Exception as e:
- logger.error(f"❌ Error sending enhanced trade notification: {e}")
- async def _send_position_opened_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
- """Send notification for newly opened position."""
- position_type = "LONG" if action_type == 'long_opened' else "SHORT"
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- trade_value = amount * price
-
- message = f"""
- 🚀 <b>Position Opened</b>
- 📊 <b>New {position_type} Position:</b>
- • Token: {token}
- • Direction: {position_type}
- • Entry Size: {amount} {token}
- • Entry Price: ${price:,.2f}
- • Position Value: ${trade_value:,.2f}
- {side_emoji} <b>Trade Details:</b>
- • Side: {side.upper()}
- • Order Type: Market/Limit
- • Status: OPENED ✅
- ⏰ <b>Time:</b> {time_str}
- 📈 <b>Note:</b> New {position_type} position established
- 📊 Use /positions to view current holdings
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position opened: {token} {position_type} {amount} @ ${price}")
- async def _send_position_increased_notification(self, token: str, side: str, amount: float, price: float, position: Dict, action_type: str, time_str: str):
- """Send notification for position increase (additional entry)."""
- position_type = "LONG" if action_type == 'long_increased' else "SHORT"
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
-
- total_size = abs(position['contracts'])
- avg_entry = position['avg_entry_price']
- entry_count = position['entry_count']
- total_value = total_size * avg_entry
-
- message = f"""
- 📈 <b>Position Increased</b>
- 📊 <b>{position_type} Position Updated:</b>
- • Token: {token}
- • Direction: {position_type}
- • Added Size: {amount} {token} @ ${price:,.2f}
- • New Total Size: {total_size} {token}
- • Average Entry: ${avg_entry:,.2f}
- {side_emoji} <b>Position Summary:</b>
- • Total Value: ${total_value:,.2f}
- • Entry Points: {entry_count}
- • Last Entry: ${price:,.2f}
- • Status: INCREASED ⬆️
- ⏰ <b>Time:</b> {time_str}
- 💡 <b>Strategy:</b> Multiple entry averaging
- 📊 Use /positions for complete position details
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position increased: {token} {position_type} +{amount} @ ${price} (total: {total_size})")
- async def _send_position_reduced_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
- """Send notification for partial position close."""
- position_type = "LONG" if action_type == 'long_reduced' else "SHORT"
-
- remaining_size = abs(position['contracts'])
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
- pnl = pnl_data['pnl']
- pnl_percent = pnl_data['pnl_percent']
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- partial_value = amount * price
-
- message = f"""
- 📉 <b>Position Partially Closed</b>
- 📊 <b>{position_type} Partial Exit:</b>
- • Token: {token}
- • Direction: {position_type}
- • Closed Size: {amount} {token}
- • Exit Price: ${price:,.2f}
- • Remaining Size: {remaining_size} {token}
- {pnl_emoji} <b>Partial P&L:</b>
- • Entry Price: ${avg_entry:,.2f}
- • Exit Value: ${partial_value:,.2f}
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- 💰 <b>Position Status:</b>
- • Status: PARTIALLY CLOSED 📉
- • Take Profit Strategy: Active
- ⏰ <b>Time:</b> {time_str}
- 📊 Use /positions to view remaining position
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position reduced: {token} {position_type} -{amount} @ ${price} P&L: ${pnl:.2f}")
- async def _send_position_closed_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
- """Send notification for fully closed position."""
- position_type = "LONG" if action_type == 'long_closed' else "SHORT"
-
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
- pnl = pnl_data['pnl']
- pnl_percent = pnl_data['pnl_percent']
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- entry_count = position.get('entry_count', 1)
- exit_value = amount * price
-
- message = f"""
- 🎯 <b>Position Fully Closed</b>
- 📊 <b>{position_type} Position Summary:</b>
- • Token: {token}
- • Direction: {position_type}
- • Total Size: {amount} {token}
- • Average Entry: ${avg_entry:,.2f}
- • Exit Price: ${price:,.2f}
- • Exit Value: ${exit_value:,.2f}
- {pnl_emoji} <b>Total P&L:</b>
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- • Entry Points Used: {entry_count}
- ✅ <b>Trade Complete:</b>
- • Status: FULLY CLOSED 🎯
- • Position: FLAT
- ⏰ <b>Time:</b> {time_str}
- 📊 Use /stats to view updated performance
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position closed: {token} {position_type} {amount} @ ${price} Total P&L: ${pnl:.2f}")
- async def _send_position_flipped_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
- """Send notification for position flip (close and reverse)."""
- if action_type == 'long_closed_and_short_opened':
- old_type = "LONG"
- new_type = "SHORT"
- else:
- old_type = "SHORT"
- new_type = "LONG"
-
- message = f"""
- 🔄 <b>Position Flipped</b>
- 📊 <b>Direction Change:</b>
- • Token: {token}
- • Previous: {old_type} position
- • New: {new_type} position
- • Size: {amount} {token}
- • Price: ${price:,.2f}
- 🎯 <b>Trade Summary:</b>
- • {old_type} position: CLOSED ✅
- • {new_type} position: OPENED 🚀
- • Flip Price: ${price:,.2f}
- • Status: POSITION REVERSED
- ⏰ <b>Time:</b> {time_str}
- 💡 <b>Strategy:</b> Directional change
- 📊 Use /positions to view new position
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position flipped: {token} {old_type} -> {new_type} @ ${price}")
- 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
- trading_days = 0
-
- for day_stats in daily_stats:
- if day_stats['has_trades']:
- # Day with completed trades
- 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']
- trading_days += 1
- else:
- # Day with no trades
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_days > 0:
- 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" 📈 Trading Days: {trading_days}/10\n"
- daily_text += f" 📊 Avg per Trading Day: ${total_pnl/trading_days:,.2f}"
- else:
- daily_text += f"💼 <b>10-Day Summary:</b>\n"
- daily_text += f" 📭 No completed trades in the last 10 days\n"
- daily_text += f" 💡 Start trading to see daily performance!"
-
- 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
- trading_weeks = 0
-
- for week_stats in weekly_stats:
- if week_stats['has_trades']:
- # Week with completed trades
- 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']
- trading_weeks += 1
- else:
- # Week with no trades
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_weeks > 0:
- 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" 📈 Trading Weeks: {trading_weeks}/10\n"
- weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
- else:
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
- weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
- weekly_text += f" 💡 Start trading to see weekly performance!"
-
- 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
- trading_months = 0
-
- for month_stats in monthly_stats:
- if month_stats['has_trades']:
- # Month with completed trades
- 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']
- trading_months += 1
- else:
- # Month with no trades
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_months > 0:
- 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" 📈 Trading Months: {trading_months}/10\n"
- monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
- else:
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
- monthly_text += f" 📭 No completed trades in the last 10 months\n"
- monthly_text += f" 💡 Start trading to see monthly performance!"
-
- 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 risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /risk command to show advanced risk metrics."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Get current balance for context
- balance = self.client.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- # Get risk metrics and basic stats
- risk_metrics = self.stats.get_risk_metrics()
- basic_stats = self.stats.get_basic_stats()
-
- # Check if we have enough data for risk calculations
- if basic_stats['completed_trades'] < 2:
- await update.message.reply_text(
- "📊 <b>Risk Analysis</b>\n\n"
- "📭 <b>Insufficient Data</b>\n\n"
- f"• Current completed trades: {basic_stats['completed_trades']}\n"
- f"• Required for risk analysis: 2+ trades\n"
- f"• Daily balance snapshots: {len(self.stats.data.get('daily_balances', []))}\n\n"
- "💡 <b>To enable risk analysis:</b>\n"
- "• Complete more trades to generate returns data\n"
- "• Bot automatically records daily balance snapshots\n"
- "• Risk metrics will be available after sufficient trading history\n\n"
- "📈 Use /stats for current performance metrics",
- parse_mode='HTML'
- )
- return
-
- # Format the risk analysis message
- risk_text = f"""
- 📊 <b>Risk Analysis & Advanced Metrics</b>
- 🎯 <b>Risk-Adjusted Performance:</b>
- • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
- • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
- • Annual Volatility: {risk_metrics['volatility']:.2f}%
- 📉 <b>Drawdown Analysis:</b>
- • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
- • Value at Risk (95%): {risk_metrics['var_95']:.2f}%
- 💰 <b>Portfolio Context:</b>
- • Current Balance: ${current_balance:,.2f}
- • Initial Balance: ${basic_stats['initial_balance']:,.2f}
- • Total P&L: ${basic_stats['total_pnl']:,.2f}
- • Days Active: {basic_stats['days_active']}
- 📊 <b>Risk Interpretation:</b>
- """
-
- # Add interpretive guidance
- sharpe = risk_metrics['sharpe_ratio']
- if sharpe > 2.0:
- risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
- elif sharpe > 1.0:
- risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
- elif sharpe > 0.5:
- risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
- elif sharpe > 0:
- risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
- else:
- risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
-
- max_dd = risk_metrics['max_drawdown']
- if max_dd < 5:
- risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
- elif max_dd < 15:
- risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
- elif max_dd < 30:
- risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
- else:
- risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
-
- volatility = risk_metrics['volatility']
- if volatility < 10:
- risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
- elif volatility < 25:
- risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
- elif volatility < 50:
- risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
- else:
- risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
-
- risk_text += f"""
- 💡 <b>Risk Definitions:</b>
- • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
- • <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
- • <b>Max Drawdown:</b> Largest peak-to-trough decline
- • <b>VaR 95%:</b> Maximum expected loss 95% of the time
- • <b>Volatility:</b> Annualized standard deviation of returns
- 📈 <b>Data Based On:</b>
- • Completed Trades: {basic_stats['completed_trades']}
- • Daily Balance Records: {len(self.stats.data.get('daily_balances', []))}
- • Trading Period: {basic_stats['days_active']} days
- 🔄 Use /stats for trading performance metrics
- """
-
- await update.message.reply_text(risk_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing risk command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in risk command: {e}")
-
- async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /version command to show bot version and system info."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Get system info
- import platform
- import sys
- from datetime import datetime
-
- uptime_info = "Unknown"
- try:
- # Try to get process uptime if available
- import psutil
- process = psutil.Process()
- create_time = datetime.fromtimestamp(process.create_time())
- uptime = datetime.now() - create_time
- days = uptime.days
- hours, remainder = divmod(uptime.seconds, 3600)
- minutes, _ = divmod(remainder, 60)
- uptime_info = f"{days}d {hours}h {minutes}m"
- except ImportError:
- # psutil not available, skip uptime
- pass
-
- # Get stats info
- basic_stats = self.stats.get_basic_stats()
-
- version_text = f"""
- 🤖 <b>Trading Bot Version & System Info</b>
- 📱 <b>Bot Information:</b>
- • Version: <code>{self.version}</code>
- • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
- • Uptime: {uptime_info}
- • Default Token: {Config.DEFAULT_TRADING_TOKEN}
- 💻 <b>System Information:</b>
- • Python: {sys.version.split()[0]}
- • Platform: {platform.system()} {platform.release()}
- • Architecture: {platform.machine()}
- 📊 <b>Trading Stats:</b>
- • Total Orders: {basic_stats['total_trades']}
- • Completed Trades: {basic_stats['completed_trades']}
- • Days Active: {basic_stats['days_active']}
- • Start Date: {basic_stats['start_date']}
- 🔄 <b>Monitoring Status:</b>
- • Order Monitoring: {'✅ Active' if self.order_monitoring_task and not self.order_monitoring_task.done() else '❌ Inactive'}
- • External Trades: ✅ Active
- • Price Alarms: ✅ Active ({len(self.alarms)} active)
- • Risk Management: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
- ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
- """
-
- await update.message.reply_text(version_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing version command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in version command: {e}")
- def _get_position_state(self, symbol: str) -> Dict[str, Any]:
- """Get current position state for a symbol."""
- if symbol not in self.position_tracker:
- self.position_tracker[symbol] = {
- 'contracts': 0.0,
- 'avg_entry_price': 0.0,
- 'total_cost_basis': 0.0,
- 'entry_count': 0,
- 'entry_history': [], # List of {price, amount, timestamp}
- 'last_update': datetime.now().isoformat()
- }
- return self.position_tracker[symbol]
-
- def _update_position_state(self, symbol: str, side: str, amount: float, price: float, timestamp: str = None):
- """Update position state with a new trade."""
- if timestamp is None:
- timestamp = datetime.now().isoformat()
-
- position = self._get_position_state(symbol)
-
- if side.lower() == 'buy':
- # Adding to long position or reducing short position
- if position['contracts'] >= 0:
- # Opening/adding to long position
- new_cost = amount * price
- old_cost = position['total_cost_basis']
- old_contracts = position['contracts']
-
- position['contracts'] += amount
- position['total_cost_basis'] += new_cost
- position['avg_entry_price'] = position['total_cost_basis'] / position['contracts'] if position['contracts'] > 0 else 0
- position['entry_count'] += 1
- position['entry_history'].append({
- 'price': price,
- 'amount': amount,
- 'timestamp': timestamp,
- 'side': 'buy'
- })
-
- logger.info(f"📈 Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}")
- return 'long_opened' if old_contracts == 0 else 'long_increased'
- else:
- # Reducing short position
- reduction = min(amount, abs(position['contracts']))
- position['contracts'] += reduction
-
- if position['contracts'] >= 0:
- # Short position fully closed or flipped to long
- if position['contracts'] == 0:
- self._reset_position_state(symbol)
- return 'short_closed'
- else:
- # Flipped to long - need to track new long position
- remaining_amount = amount - reduction
- position['contracts'] = remaining_amount
- position['total_cost_basis'] = remaining_amount * price
- position['avg_entry_price'] = price
- return 'short_closed_and_long_opened'
- else:
- return 'short_reduced'
-
- elif side.lower() == 'sell':
- # Adding to short position or reducing long position
- if position['contracts'] <= 0:
- # Opening/adding to short position
- position['contracts'] -= amount
- position['entry_count'] += 1
- position['entry_history'].append({
- 'price': price,
- 'amount': amount,
- 'timestamp': timestamp,
- 'side': 'sell'
- })
-
- logger.info(f"📉 Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}")
- return 'short_opened' if position['contracts'] == -amount else 'short_increased'
- else:
- # Reducing long position
- reduction = min(amount, position['contracts'])
- position['contracts'] -= reduction
-
- # Adjust cost basis proportionally
- if position['contracts'] > 0:
- reduction_ratio = reduction / (position['contracts'] + reduction)
- position['total_cost_basis'] *= (1 - reduction_ratio)
- return 'long_reduced'
- else:
- # Long position fully closed
- if position['contracts'] == 0:
- self._reset_position_state(symbol)
- return 'long_closed'
- else:
- # Flipped to short
- remaining_amount = amount - reduction
- position['contracts'] = -remaining_amount
- return 'long_closed_and_short_opened'
-
- position['last_update'] = timestamp
- return 'unknown'
-
- def _reset_position_state(self, symbol: str):
- """Reset position state when position is fully closed."""
- if symbol in self.position_tracker:
- del self.position_tracker[symbol]
-
- def _calculate_position_pnl(self, symbol: str, exit_amount: float, exit_price: float) -> Dict[str, float]:
- """Calculate P&L for a position exit."""
- position = self._get_position_state(symbol)
-
- if position['contracts'] == 0:
- return {'pnl': 0.0, 'pnl_percent': 0.0}
-
- avg_entry = position['avg_entry_price']
-
- if position['contracts'] > 0: # Long position
- pnl = exit_amount * (exit_price - avg_entry)
- else: # Short position
- pnl = exit_amount * (avg_entry - exit_price)
-
- cost_basis = exit_amount * avg_entry
- pnl_percent = (pnl / cost_basis * 100) if cost_basis > 0 else 0
-
- return {
- 'pnl': pnl,
- 'pnl_percent': pnl_percent,
- 'avg_entry_price': avg_entry
- }
- async def _send_external_trade_notification(self, trade: Dict[str, Any]):
- """Send generic notification for external trades (fallback)."""
- 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> External 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 generic 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 and update position tracking
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'), "auto_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 using enhanced position tracking."""
- try:
- # For bot-initiated orders, we'll detect changes in position size
- # and send appropriate notifications using the enhanced system
-
- # This method will be triggered when orders placed through the bot are filled
- # The external trade monitoring will handle trades made outside the bot
-
- # Update position tracking based on current positions
- await self._update_position_tracking(current_positions)
-
- except Exception as e:
- logger.error(f"❌ Error processing filled orders: {e}")
-
- async def _update_position_tracking(self, current_positions: list):
- """Update the legacy position tracking data for compatibility."""
- 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
- }
-
- # Also update our enhanced position tracker if not already present
- if symbol not in self.position_tracker:
- self._get_position_state(symbol)
- self.position_tracker[symbol]['contracts'] = contracts
- self.position_tracker[symbol]['avg_entry_price'] = entry_price
- self.position_tracker[symbol]['total_cost_basis'] = contracts * entry_price
-
- self.last_known_positions = new_position_map
- 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()
|