telegram_bot.py 160 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700
  1. #!/usr/bin/env python3
  2. """
  3. Telegram Bot for Hyperliquid Trading
  4. This module provides a Telegram interface for manual Hyperliquid trading
  5. with comprehensive statistics tracking and phone-friendly controls.
  6. """
  7. import logging
  8. import asyncio
  9. import re
  10. from datetime import datetime, timedelta
  11. from typing import Optional, Dict, Any
  12. from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
  13. from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
  14. from hyperliquid_client import HyperliquidClient
  15. from trading_stats import TradingStats
  16. from config import Config
  17. from alarm_manager import AlarmManager
  18. from logging_config import setup_logging, cleanup_logs, format_log_stats
  19. # Set up logging using the new configuration system
  20. logger = setup_logging().getChild(__name__)
  21. class TelegramTradingBot:
  22. """Telegram trading bot for manual trading operations."""
  23. def __init__(self):
  24. """Initialize the Telegram trading bot."""
  25. self.client = HyperliquidClient()
  26. self.application = None
  27. self.order_monitoring_task = None
  28. self.last_filled_orders = set()
  29. self.alarms = [] # List to store price alarms
  30. self.bot_heartbeat_seconds = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10)
  31. self.external_trade_timestamps = set() # Track external trade timestamps to avoid duplicates
  32. self.last_position_check = {} # Track last position state for comparison
  33. self._position_tracker = {} # For enhanced position tracking
  34. self.stats = None
  35. self.version = "Unknown" # Will be set by launcher
  36. # Order monitoring attributes
  37. self.monitoring_active = False
  38. self.last_known_orders = set() # Track order IDs we've seen
  39. self.last_known_positions = {} # Track position sizes for P&L calculation
  40. # External trade monitoring
  41. self.last_processed_trade_time = None # Track last processed external trade
  42. # Deposit/Withdrawal monitoring
  43. self.last_deposit_withdrawal_check = None # Track last deposit/withdrawal check
  44. self.deposit_withdrawal_check_interval = 3600 # Check every hour (3600 seconds)
  45. # Alarm management
  46. self.alarm_manager = AlarmManager()
  47. # Initialize stats
  48. self._initialize_stats()
  49. def _initialize_stats(self):
  50. """Initialize stats with current balance."""
  51. try:
  52. # Initialize TradingStats object first
  53. self.stats = TradingStats()
  54. # Get current balance and set it as initial balance
  55. balance = self.client.get_balance()
  56. if balance and balance.get('total'):
  57. # Get USDC balance as the main balance
  58. usdc_balance = float(balance['total'].get('USDC', 0))
  59. self.stats.set_initial_balance(usdc_balance)
  60. except Exception as e:
  61. logger.error(f"Could not initialize stats: {e}")
  62. def is_authorized(self, chat_id: str) -> bool:
  63. """Check if the chat ID is authorized to use the bot."""
  64. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  65. async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
  66. """Send a message to the authorized chat."""
  67. if self.application and Config.TELEGRAM_CHAT_ID:
  68. try:
  69. await self.application.bot.send_message(
  70. chat_id=Config.TELEGRAM_CHAT_ID,
  71. text=text,
  72. parse_mode=parse_mode
  73. )
  74. except Exception as e:
  75. logger.error(f"Failed to send message: {e}")
  76. async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  77. """Handle the /start command."""
  78. if not self.is_authorized(update.effective_chat.id):
  79. await update.message.reply_text("❌ Unauthorized access.")
  80. return
  81. welcome_text = f"""
  82. 🤖 <b>Welcome to Hyperliquid Trading Bot</b>
  83. 📱 <b>Quick Actions:</b>
  84. • Trading: /long BTC 100 or /short ETH 50
  85. • Exit: /exit BTC (closes position)
  86. • Info: /balance, /positions, /orders
  87. 📊 <b>Market Data:</b>
  88. • /market - Detailed market overview
  89. • /price - Quick price check
  90. <b>⚡ Quick Commands:</b>
  91. • /balance - Account balance
  92. • /positions - Open positions
  93. • /orders - Active orders
  94. • /market - Market data & prices
  95. <b>🚀 Trading:</b>
  96. • /long BTC 100 - Long position
  97. • /short ETH 50 - Short position
  98. • /exit BTC - Close position
  99. • /coo BTC - Cancel open orders
  100. <b>🛡️ Risk Management:</b>
  101. • Enabled: {risk_enabled}
  102. • Auto Stop Loss: {stop_loss}%
  103. • /sl BTC 44000 - Manual stop loss
  104. • /tp BTC 50000 - Take profit order
  105. <b>📈 Performance & Analytics:</b>
  106. • /stats - Complete trading statistics
  107. • /performance - Token performance ranking & detailed stats
  108. • /daily - Daily performance (last 10 days)
  109. • /weekly - Weekly performance (last 10 weeks)
  110. • /monthly - Monthly performance (last 10 months)
  111. • /risk - Sharpe ratio, drawdown, VaR
  112. • /version - Bot version & system information
  113. • /trades - Recent trade history
  114. <b>🔔 Price Alerts:</b>
  115. • /alarm - List all active alarms
  116. • /alarm BTC 50000 - Set alarm for BTC at $50,000
  117. • /alarm BTC - Show all BTC alarms
  118. • /alarm 3 - Remove alarm ID 3
  119. <b>🔄 Automatic Monitoring:</b>
  120. • Real-time order fill alerts
  121. • Position opened/closed notifications
  122. • P&L calculations on trade closure
  123. • Price alarm triggers
  124. • External trade detection & sync
  125. • Auto stats synchronization
  126. • {heartbeat}-second monitoring interval
  127. <b>📊 Universal Trade Tracking:</b>
  128. • Bot trades: Full logging & notifications
  129. • Platform trades: Auto-detected & synced
  130. • Mobile app trades: Monitored & recorded
  131. • API trades: Tracked & included in stats
  132. Type /help for detailed command information.
  133. <b>🔄 Order Monitoring:</b>
  134. • /monitoring - View monitoring status
  135. • /logs - View log file statistics and cleanup
  136. <b>⚙️ Configuration:</b>
  137. • Symbol: {symbol}
  138. • Default Token: {symbol}
  139. • Network: {network}
  140. <b>🛡️ Safety Features:</b>
  141. • All trades logged automatically
  142. • Comprehensive performance tracking
  143. • Real-time balance monitoring
  144. • Risk metrics calculation
  145. <b>📱 Mobile Optimized:</b>
  146. • Quick action buttons
  147. • Instant notifications
  148. • Clean, readable layout
  149. • One-tap commands
  150. <b>💡 Quick Access:</b>
  151. • /commands or /c - One-tap button menu for all commands
  152. • Buttons below for instant access to key functions
  153. For support, contact your bot administrator.
  154. """.format(
  155. symbol=Config.DEFAULT_TRADING_TOKEN,
  156. network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
  157. risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
  158. stop_loss=Config.STOP_LOSS_PERCENTAGE,
  159. heartbeat=Config.BOT_HEARTBEAT_SECONDS
  160. )
  161. keyboard = [
  162. [
  163. InlineKeyboardButton("💰 Balance", callback_data="balance"),
  164. InlineKeyboardButton("📊 Stats", callback_data="stats")
  165. ],
  166. [
  167. InlineKeyboardButton("📈 Positions", callback_data="positions"),
  168. InlineKeyboardButton("📋 Orders", callback_data="orders")
  169. ],
  170. [
  171. InlineKeyboardButton("💵 Price", callback_data="price"),
  172. InlineKeyboardButton("📊 Market", callback_data="market")
  173. ],
  174. [
  175. InlineKeyboardButton("🔄 Recent Trades", callback_data="trades"),
  176. InlineKeyboardButton("⚙️ Help", callback_data="help")
  177. ]
  178. ]
  179. reply_markup = InlineKeyboardMarkup(keyboard)
  180. await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
  181. async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  182. """Handle the /help command."""
  183. if not self.is_authorized(update.effective_chat.id):
  184. await update.message.reply_text("❌ Unauthorized access.")
  185. return
  186. help_text = """
  187. 🔧 <b>Hyperliquid Trading Bot - Complete Guide</b>
  188. <b>💼 Account Management:</b>
  189. • /balance - Show account balance
  190. • /positions - Show open positions
  191. • /orders - Show open orders
  192. • /balance_adjustments - View deposit/withdrawal history
  193. <b>📊 Market Data:</b>
  194. • /market - Detailed market data (default token)
  195. • /market BTC - Market data for specific token
  196. • /price - Quick price check (default token)
  197. • /price SOL - Price for specific token
  198. <b>🚀 Perps Trading:</b>
  199. • /long BTC 100 - Long BTC with $100 USDC (Market Order)
  200. • /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
  201. • /short ETH 50 - Short ETH with $50 USDC (Market Order)
  202. • /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
  203. • /exit BTC - Close BTC position with Market Order
  204. <b>🛡️ Risk Management:</b>
  205. • /sl BTC 44000 - Set stop loss for BTC at $44,000
  206. • /tp BTC 50000 - Set take profit for BTC at $50,000
  207. <b>🚨 Automatic Stop Loss:</b>
  208. • Enabled: {risk_enabled}
  209. • Stop Loss: {stop_loss}% (automatic execution)
  210. • Monitoring: Every {heartbeat} seconds
  211. <b>📋 Order Management:</b>
  212. • /orders - Show all open orders
  213. • /orders BTC - Show open orders for BTC only
  214. • /coo BTC - Cancel all open orders for BTC
  215. <b>📈 Statistics & Analytics:</b>
  216. • /stats - Complete trading statistics
  217. • /performance - Win rate, profit factor, etc.
  218. • /risk - Sharpe ratio, drawdown, VaR
  219. • /version - Bot version & system information
  220. • /trades - Recent trade history
  221. <b>🔔 Price Alerts:</b>
  222. • /alarm - List all active alarms
  223. • /alarm BTC 50000 - Set alarm for BTC at $50,000
  224. • /alarm BTC - Show all BTC alarms
  225. • /alarm 3 - Remove alarm ID 3
  226. <b>🔄 Order Monitoring:</b>
  227. • /monitoring - View monitoring status
  228. • /logs - View log file statistics and cleanup
  229. <b>⚙️ Configuration:</b>
  230. • Symbol: {symbol}
  231. • Default Token: {symbol}
  232. • Network: {network}
  233. <b>🛡️ Safety Features:</b>
  234. • All trades logged automatically
  235. • Comprehensive performance tracking
  236. • Real-time balance monitoring
  237. • Deposit/withdrawal tracking (hourly)
  238. • Risk metrics calculation
  239. <b>📱 Mobile Optimized:</b>
  240. • Quick action buttons
  241. • Instant notifications
  242. • Clean, readable layout
  243. • One-tap commands
  244. For support, contact your bot administrator.
  245. """.format(
  246. symbol=Config.DEFAULT_TRADING_TOKEN,
  247. network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
  248. risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
  249. stop_loss=Config.STOP_LOSS_PERCENTAGE,
  250. heartbeat=Config.BOT_HEARTBEAT_SECONDS
  251. )
  252. await update.message.reply_text(help_text, parse_mode='HTML')
  253. async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  254. """Handle the /commands and /c command with quick action buttons."""
  255. if not self.is_authorized(update.effective_chat.id):
  256. await update.message.reply_text("❌ Unauthorized access.")
  257. return
  258. commands_text = """
  259. 📱 <b>Quick Commands</b>
  260. Tap any button below for instant access to bot functions:
  261. 💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
  262. """
  263. keyboard = [
  264. [
  265. InlineKeyboardButton("💰 Balance", callback_data="balance"),
  266. InlineKeyboardButton("📈 Positions", callback_data="positions")
  267. ],
  268. [
  269. InlineKeyboardButton("📋 Orders", callback_data="orders"),
  270. InlineKeyboardButton("📊 Stats", callback_data="stats")
  271. ],
  272. [
  273. InlineKeyboardButton("💵 Price", callback_data="price"),
  274. InlineKeyboardButton("📊 Market", callback_data="market")
  275. ],
  276. [
  277. InlineKeyboardButton("🏆 Performance", callback_data="performance"),
  278. InlineKeyboardButton("🔔 Alarms", callback_data="alarm")
  279. ],
  280. [
  281. InlineKeyboardButton("📅 Daily", callback_data="daily"),
  282. InlineKeyboardButton("📊 Weekly", callback_data="weekly")
  283. ],
  284. [
  285. InlineKeyboardButton("📆 Monthly", callback_data="monthly"),
  286. InlineKeyboardButton("🔄 Trades", callback_data="trades")
  287. ],
  288. [
  289. InlineKeyboardButton("🔄 Monitoring", callback_data="monitoring"),
  290. InlineKeyboardButton("📝 Logs", callback_data="logs")
  291. ],
  292. [
  293. InlineKeyboardButton("⚙️ Help", callback_data="help")
  294. ]
  295. ]
  296. reply_markup = InlineKeyboardMarkup(keyboard)
  297. await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)
  298. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  299. """Handle the /stats command."""
  300. if not self.is_authorized(update.effective_chat.id):
  301. await update.message.reply_text("❌ Unauthorized access.")
  302. return
  303. # Get current balance for stats
  304. balance = self.client.get_balance()
  305. current_balance = 0
  306. if balance and balance.get('total'):
  307. current_balance = float(balance['total'].get('USDC', 0))
  308. stats_message = self.stats.format_stats_message(current_balance)
  309. await update.message.reply_text(stats_message, parse_mode='HTML')
  310. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  311. """Handle the /trades command."""
  312. if not self.is_authorized(update.effective_chat.id):
  313. await update.message.reply_text("❌ Unauthorized access.")
  314. return
  315. recent_trades = self.stats.get_recent_trades(10)
  316. if not recent_trades:
  317. await update.message.reply_text("📝 No trades recorded yet.")
  318. return
  319. trades_text = "🔄 <b>Recent Trades</b>\n\n"
  320. for trade in reversed(recent_trades[-5:]): # Show last 5 trades
  321. timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  322. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  323. trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
  324. trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
  325. trades_text += f" 📅 {timestamp}\n\n"
  326. await update.message.reply_text(trades_text, parse_mode='HTML')
  327. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  328. """Handle the /balance command."""
  329. if not self.is_authorized(update.effective_chat.id):
  330. await update.message.reply_text("❌ Unauthorized access.")
  331. return
  332. balance = self.client.get_balance()
  333. if balance:
  334. balance_text = "💰 <b>Account Balance</b>\n\n"
  335. # CCXT balance structure includes 'free', 'used', and 'total'
  336. total_balance = balance.get('total', {})
  337. free_balance = balance.get('free', {})
  338. used_balance = balance.get('used', {})
  339. if total_balance:
  340. total_value = 0
  341. available_value = 0
  342. # Display individual assets
  343. for asset, amount in total_balance.items():
  344. if float(amount) > 0:
  345. free_amount = float(free_balance.get(asset, 0))
  346. used_amount = float(used_balance.get(asset, 0))
  347. balance_text += f"💵 <b>{asset}:</b>\n"
  348. balance_text += f" 📊 Total: {amount}\n"
  349. balance_text += f" ✅ Available: {free_amount}\n"
  350. if used_amount > 0:
  351. balance_text += f" 🔒 In Use: {used_amount}\n"
  352. balance_text += "\n"
  353. # Calculate totals for USDC (main trading currency)
  354. if asset == 'USDC':
  355. total_value += float(amount)
  356. available_value += free_amount
  357. # Summary section
  358. balance_text += f"💼 <b>Portfolio Summary:</b>\n"
  359. balance_text += f" 💰 Total Value: ${total_value:,.2f}\n"
  360. balance_text += f" 🚀 Available for Trading: ${available_value:,.2f}\n"
  361. if total_value - available_value > 0:
  362. balance_text += f" 🔒 In Active Use: ${total_value - available_value:,.2f}\n"
  363. # Add P&L summary
  364. basic_stats = self.stats.get_basic_stats()
  365. if basic_stats['initial_balance'] > 0:
  366. pnl = total_value - basic_stats['initial_balance']
  367. pnl_percent = (pnl / basic_stats['initial_balance']) * 100
  368. balance_text += f"\n📊 <b>Performance:</b>\n"
  369. balance_text += f" 💵 P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
  370. balance_text += f" 📈 Initial: ${basic_stats['initial_balance']:,.2f}"
  371. else:
  372. balance_text += "📭 No balance data available"
  373. else:
  374. balance_text = "❌ Could not fetch balance data"
  375. await update.message.reply_text(balance_text, parse_mode='HTML')
  376. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  377. """Handle the /positions command."""
  378. if not self.is_authorized(update.effective_chat.id):
  379. await update.message.reply_text("❌ Unauthorized access.")
  380. return
  381. positions = self.client.get_positions()
  382. if positions is not None: # Successfully fetched (could be empty list)
  383. positions_text = "📈 <b>Open Positions</b>\n\n"
  384. # Filter for actual open positions
  385. open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
  386. if open_positions:
  387. total_unrealized = 0
  388. total_position_value = 0
  389. for position in open_positions:
  390. symbol = position.get('symbol', 'Unknown')
  391. contracts = float(position.get('contracts', 0))
  392. unrealized_pnl = float(position.get('unrealizedPnl', 0))
  393. entry_price = float(position.get('entryPx', 0))
  394. # Calculate position value and P&L percentage
  395. position_value = abs(contracts) * entry_price
  396. pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
  397. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  398. # Extract token name for cleaner display
  399. token = symbol.split('/')[0] if '/' in symbol else symbol
  400. position_type = "LONG" if contracts > 0 else "SHORT"
  401. positions_text += f"📊 <b>{token}</b> ({position_type})\n"
  402. positions_text += f" 📏 Size: {abs(contracts):.6f} {token}\n"
  403. positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
  404. positions_text += f" 💵 Value: ${position_value:,.2f}\n"
  405. positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n"
  406. total_unrealized += unrealized_pnl
  407. total_position_value += position_value
  408. # Calculate overall P&L percentage
  409. total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
  410. total_pnl_emoji = "🟢" if total_unrealized >= 0 else "🔴"
  411. positions_text += f"💼 <b>Total Portfolio:</b>\n"
  412. positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
  413. positions_text += f" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)"
  414. else:
  415. positions_text += "📭 <b>No open positions currently</b>\n\n"
  416. positions_text += "🚀 Ready to start trading!\n"
  417. positions_text += "Use /buy or /sell commands to open positions."
  418. else:
  419. # Actual API error
  420. positions_text = "❌ <b>Could not fetch positions data</b>\n\n"
  421. positions_text += "🔄 Please try again in a moment.\n"
  422. positions_text += "If the issue persists, check your connection."
  423. await update.message.reply_text(positions_text, parse_mode='HTML')
  424. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  425. """Handle the /orders command with optional token filter."""
  426. if not self.is_authorized(update.effective_chat.id):
  427. await update.message.reply_text("❌ Unauthorized access.")
  428. return
  429. # Check if token filter is provided
  430. token_filter = None
  431. if context.args and len(context.args) >= 1:
  432. token_filter = context.args[0].upper()
  433. orders = self.client.get_open_orders()
  434. if orders is not None: # Successfully fetched (could be empty list)
  435. if token_filter:
  436. orders_text = f"📋 <b>Open Orders - {token_filter}</b>\n\n"
  437. # Filter orders for specific token
  438. target_symbol = f"{token_filter}/USDC:USDC"
  439. filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
  440. else:
  441. orders_text = "📋 <b>All Open Orders</b>\n\n"
  442. filtered_orders = orders
  443. if filtered_orders and len(filtered_orders) > 0:
  444. for order in filtered_orders:
  445. symbol = order.get('symbol', 'Unknown')
  446. side = order.get('side', 'Unknown')
  447. amount = order.get('amount', 0)
  448. price = order.get('price', 0)
  449. order_id = order.get('id', 'Unknown')
  450. # Extract token from symbol for display
  451. token = symbol.split('/')[0] if '/' in symbol else symbol
  452. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  453. orders_text += f"{side_emoji} <b>{token}</b>\n"
  454. orders_text += f" 📊 {side.upper()} {amount} @ ${price:,.2f}\n"
  455. orders_text += f" 💵 Value: ${float(amount) * float(price):,.2f}\n"
  456. orders_text += f" 🔑 ID: <code>{order_id}</code>\n\n"
  457. # Add helpful commands
  458. if token_filter:
  459. orders_text += f"💡 <b>Quick Actions:</b>\n"
  460. orders_text += f"• <code>/coo {token_filter}</code> - Cancel all {token_filter} orders\n"
  461. orders_text += f"• <code>/orders</code> - View all orders"
  462. else:
  463. orders_text += f"💡 <b>Filter by token:</b> <code>/orders BTC</code>, <code>/orders ETH</code>"
  464. else:
  465. if token_filter:
  466. orders_text += f"📭 <b>No open orders for {token_filter}</b>\n\n"
  467. orders_text += f"💡 No pending {token_filter} orders found.\n"
  468. orders_text += f"Use <code>/long {token_filter} 100</code> or <code>/short {token_filter} 100</code> to create new orders."
  469. else:
  470. orders_text += "📭 <b>No open orders currently</b>\n\n"
  471. orders_text += "💡 All clear! No pending orders.\n"
  472. orders_text += "Use /long or /short commands to place new orders."
  473. else:
  474. # Actual API error
  475. orders_text = "❌ <b>Could not fetch orders data</b>\n\n"
  476. orders_text += "🔄 Please try again in a moment.\n"
  477. orders_text += "If the issue persists, check your connection."
  478. await update.message.reply_text(orders_text, parse_mode='HTML')
  479. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  480. """Handle the /market command."""
  481. if not self.is_authorized(update.effective_chat.id):
  482. await update.message.reply_text("❌ Unauthorized access.")
  483. return
  484. # Check if token is provided as argument
  485. if context.args and len(context.args) >= 1:
  486. token = context.args[0].upper()
  487. else:
  488. token = Config.DEFAULT_TRADING_TOKEN
  489. # Convert token to full symbol format for API
  490. symbol = f"{token}/USDC:USDC"
  491. market_data = self.client.get_market_data(symbol)
  492. if market_data and market_data.get('ticker'):
  493. try:
  494. ticker = market_data['ticker']
  495. orderbook = market_data.get('orderbook', {})
  496. # Safely extract ticker data with fallbacks
  497. current_price = float(ticker.get('last') or 0)
  498. high_24h = float(ticker.get('high') or 0)
  499. low_24h = float(ticker.get('low') or 0)
  500. volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
  501. market_text = f"📊 <b>Market Data - {token}</b>\n\n"
  502. if current_price > 0:
  503. market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
  504. else:
  505. market_text += f"💵 <b>Current Price:</b> N/A\n"
  506. if high_24h > 0:
  507. market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
  508. else:
  509. market_text += f"📈 <b>24h High:</b> N/A\n"
  510. if low_24h > 0:
  511. market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
  512. else:
  513. market_text += f"📉 <b>24h Low:</b> N/A\n"
  514. market_text += f"📊 <b>24h Volume:</b> {volume_24h}\n\n"
  515. # Handle orderbook data safely
  516. if orderbook and orderbook.get('bids') and orderbook.get('asks'):
  517. try:
  518. bids = orderbook.get('bids', [])
  519. asks = orderbook.get('asks', [])
  520. if bids and asks and len(bids) > 0 and len(asks) > 0:
  521. best_bid = float(bids[0][0]) if bids[0][0] else 0
  522. best_ask = float(asks[0][0]) if asks[0][0] else 0
  523. if best_bid > 0 and best_ask > 0:
  524. spread = best_ask - best_bid
  525. spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
  526. market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
  527. market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
  528. market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
  529. else:
  530. market_text += f"📋 <b>Orderbook:</b> Data unavailable\n"
  531. else:
  532. market_text += f"📋 <b>Orderbook:</b> No orders available\n"
  533. except (IndexError, ValueError, TypeError) as e:
  534. market_text += f"📋 <b>Orderbook:</b> Error parsing data\n"
  535. else:
  536. market_text += f"📋 <b>Orderbook:</b> Not available\n"
  537. # Add usage hint
  538. market_text += f"\n💡 <b>Usage:</b> <code>/market {token}</code> or <code>/market</code> for default"
  539. except (ValueError, TypeError) as e:
  540. market_text = f"❌ <b>Error parsing market data</b>\n\n"
  541. market_text += f"🔧 Raw data received but couldn't parse values.\n"
  542. market_text += f"📞 Please try again or contact support if this persists."
  543. else:
  544. market_text = f"❌ <b>Could not fetch market data for {token}</b>\n\n"
  545. market_text += f"🔄 Please try again in a moment.\n"
  546. market_text += f"🌐 Check your network connection.\n"
  547. market_text += f"📡 API may be temporarily unavailable.\n\n"
  548. market_text += f"💡 <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
  549. await update.message.reply_text(market_text, parse_mode='HTML')
  550. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  551. """Handle the /price command."""
  552. if not self.is_authorized(update.effective_chat.id):
  553. await update.message.reply_text("❌ Unauthorized access.")
  554. return
  555. # Check if token is provided as argument
  556. if context.args and len(context.args) >= 1:
  557. token = context.args[0].upper()
  558. else:
  559. token = Config.DEFAULT_TRADING_TOKEN
  560. # Convert token to full symbol format for API
  561. symbol = f"{token}/USDC:USDC"
  562. market_data = self.client.get_market_data(symbol)
  563. if market_data and market_data.get('ticker'):
  564. try:
  565. ticker = market_data['ticker']
  566. price_value = ticker.get('last')
  567. if price_value is not None:
  568. price = float(price_value)
  569. price_text = f"💵 <b>{token}</b>: ${price:,.2f}"
  570. # Add timestamp
  571. timestamp = datetime.now().strftime('%H:%M:%S')
  572. price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
  573. # Add usage hint
  574. price_text += f"\n💡 <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
  575. else:
  576. price_text = f"💵 <b>{symbol}</b>: Price not available\n⚠️ <i>Data temporarily unavailable</i>"
  577. except (ValueError, TypeError) as e:
  578. price_text = f"❌ <b>Error parsing price for {symbol}</b>\n🔧 <i>Please try again</i>"
  579. else:
  580. price_text = f"❌ <b>Could not fetch price for {symbol}</b>\n🔄 <i>Please try again in a moment</i>\n\n"
  581. price_text += f"💡 <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
  582. await update.message.reply_text(price_text, parse_mode='HTML')
  583. async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  584. """Handle inline keyboard button presses."""
  585. query = update.callback_query
  586. await query.answer()
  587. if not self.is_authorized(query.message.chat_id):
  588. await query.edit_message_text("❌ Unauthorized access.")
  589. return
  590. callback_data = query.data
  591. # Handle trading confirmations
  592. if callback_data.startswith('confirm_long_'):
  593. parts = callback_data.split('_')
  594. token = parts[2]
  595. usdc_amount = float(parts[3])
  596. price = float(parts[4])
  597. is_limit = len(parts) > 5 and parts[5] == 'limit'
  598. await self._execute_long_order(query, token, usdc_amount, price, is_limit)
  599. return
  600. elif callback_data.startswith('confirm_short_'):
  601. parts = callback_data.split('_')
  602. token = parts[2]
  603. usdc_amount = float(parts[3])
  604. price = float(parts[4])
  605. is_limit = len(parts) > 5 and parts[5] == 'limit'
  606. await self._execute_short_order(query, token, usdc_amount, price, is_limit)
  607. return
  608. elif callback_data.startswith('confirm_exit_'):
  609. parts = callback_data.split('_')
  610. token = parts[2]
  611. exit_side = parts[3]
  612. contracts = float(parts[4])
  613. price = float(parts[5])
  614. await self._execute_exit_order(query, token, exit_side, contracts, price)
  615. return
  616. elif callback_data.startswith('confirm_coo_'):
  617. parts = callback_data.split('_')
  618. token = parts[2]
  619. await self._execute_coo(query, token)
  620. return
  621. elif callback_data.startswith('confirm_sl_'):
  622. parts = callback_data.split('_')
  623. token = parts[2]
  624. exit_side = parts[3]
  625. contracts = float(parts[4])
  626. price = float(parts[5])
  627. await self._execute_sl_order(query, token, exit_side, contracts, price)
  628. return
  629. elif callback_data.startswith('confirm_tp_'):
  630. parts = callback_data.split('_')
  631. token = parts[2]
  632. exit_side = parts[3]
  633. contracts = float(parts[4])
  634. price = float(parts[5])
  635. await self._execute_tp_order(query, token, exit_side, contracts, price)
  636. return
  637. elif callback_data == 'cancel_order':
  638. await query.edit_message_text("❌ Order cancelled.")
  639. return
  640. # Create a fake update object for reusing command handlers
  641. fake_update = Update(
  642. update_id=update.update_id,
  643. message=query.message,
  644. callback_query=query
  645. )
  646. # Handle regular button callbacks
  647. if callback_data == "balance":
  648. await self.balance_command(fake_update, context)
  649. elif callback_data == "stats":
  650. await self.stats_command(fake_update, context)
  651. elif callback_data == "positions":
  652. await self.positions_command(fake_update, context)
  653. elif callback_data == "orders":
  654. await self.orders_command(fake_update, context)
  655. elif callback_data == "market":
  656. await self.market_command(fake_update, context)
  657. elif callback_data == "price":
  658. await self.price_command(fake_update, context)
  659. elif callback_data == "trades":
  660. await self.trades_command(fake_update, context)
  661. elif callback_data == "help":
  662. await self.help_command(fake_update, context)
  663. elif callback_data == "performance":
  664. await self.performance_command(fake_update, context)
  665. elif callback_data == "alarm":
  666. await self.alarm_command(fake_update, context)
  667. elif callback_data == "daily":
  668. await self.daily_command(fake_update, context)
  669. elif callback_data == "weekly":
  670. await self.weekly_command(fake_update, context)
  671. elif callback_data == "monthly":
  672. await self.monthly_command(fake_update, context)
  673. elif callback_data == "monitoring":
  674. await self.monitoring_command(fake_update, context)
  675. elif callback_data == "logs":
  676. await self.logs_command(fake_update, context)
  677. async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
  678. """Execute a long order."""
  679. symbol = f"{token}/USDC:USDC"
  680. try:
  681. await query.edit_message_text("⏳ Opening long position...")
  682. # Calculate token amount based on USDC value and price
  683. token_amount = usdc_amount / price
  684. # Place order (limit or market)
  685. if is_limit:
  686. order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
  687. else:
  688. order = self.client.place_market_order(symbol, 'buy', token_amount)
  689. if order:
  690. # Record the trade in stats
  691. order_id = order.get('id', 'N/A')
  692. actual_price = order.get('average', price) # Use actual fill price if available
  693. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
  694. success_message = f"""
  695. ✅ <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
  696. 📊 <b>Order Details:</b>
  697. • Token: {token}
  698. • Direction: LONG (Buy)
  699. • Amount: {token_amount:.6f} {token}
  700. • Price: ${price:,.2f}
  701. • USDC Value: ~${usdc_amount:,.2f}
  702. • Order Type: {'Limit' if is_limit else 'Market'} Order
  703. • Order ID: <code>{order_id}</code>
  704. 🚀 Your {'limit order has been placed' if is_limit else 'long position is now active'}!
  705. """
  706. await query.edit_message_text(success_message, parse_mode='HTML')
  707. logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
  708. else:
  709. await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.")
  710. except Exception as e:
  711. error_message = f"❌ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}"
  712. await query.edit_message_text(error_message)
  713. logger.error(f"Error in long order: {e}")
  714. async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
  715. """Execute a short order."""
  716. symbol = f"{token}/USDC:USDC"
  717. try:
  718. await query.edit_message_text("⏳ Opening short position...")
  719. # Calculate token amount based on USDC value and price
  720. token_amount = usdc_amount / price
  721. # Place order (limit or market)
  722. if is_limit:
  723. order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
  724. else:
  725. order = self.client.place_market_order(symbol, 'sell', token_amount)
  726. if order:
  727. # Record the trade in stats
  728. order_id = order.get('id', 'N/A')
  729. actual_price = order.get('average', price) # Use actual fill price if available
  730. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
  731. success_message = f"""
  732. ✅ <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
  733. 📊 <b>Order Details:</b>
  734. • Token: {token}
  735. • Direction: SHORT (Sell)
  736. • Amount: {token_amount:.6f} {token}
  737. • Price: ${price:,.2f}
  738. • USDC Value: ~${usdc_amount:,.2f}
  739. • Order Type: {'Limit' if is_limit else 'Market'} Order
  740. • Order ID: <code>{order_id}</code>
  741. 📉 Your {'limit order has been placed' if is_limit else 'short position is now active'}!
  742. """
  743. await query.edit_message_text(success_message, parse_mode='HTML')
  744. logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
  745. else:
  746. await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.")
  747. except Exception as e:
  748. error_message = f"❌ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}"
  749. await query.edit_message_text(error_message)
  750. logger.error(f"Error in short order: {e}")
  751. async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  752. """Execute an exit order."""
  753. symbol = f"{token}/USDC:USDC"
  754. try:
  755. await query.edit_message_text("⏳ Closing position...")
  756. # Place market order to close position
  757. order = self.client.place_market_order(symbol, exit_side, contracts)
  758. if order:
  759. # Record the trade in stats
  760. order_id = order.get('id', 'N/A')
  761. actual_price = order.get('average', price) # Use actual fill price if available
  762. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
  763. position_type = "LONG" if exit_side == "sell" else "SHORT"
  764. success_message = f"""
  765. ✅ <b>Position Closed Successfully!</b>
  766. 📊 <b>Exit Details:</b>
  767. • Token: {token}
  768. • Position Closed: {position_type}
  769. • Exit Side: {exit_side.upper()}
  770. • Amount: {contracts} {token}
  771. • Est. Price: ~${price:,.2f}
  772. • Order Type: Market Order
  773. • Order ID: <code>{order_id}</code>
  774. 🎯 <b>Position Summary:</b>
  775. • Status: CLOSED
  776. • Exit Value: ~${contracts * price:,.2f}
  777. 📊 Use /stats to see updated performance metrics.
  778. """
  779. await query.edit_message_text(success_message, parse_mode='HTML')
  780. logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}")
  781. else:
  782. await query.edit_message_text("❌ Failed to close position. Please try again.")
  783. except Exception as e:
  784. error_message = f"❌ Error closing position: {str(e)}"
  785. await query.edit_message_text(error_message)
  786. logger.error(f"Error closing position: {e}")
  787. async def _execute_coo(self, query, token: str):
  788. """Execute cancel open orders for a specific token."""
  789. symbol = f"{token}/USDC:USDC"
  790. try:
  791. await query.edit_message_text("⏳ Cancelling all orders...")
  792. # Get current orders for this token
  793. all_orders = self.client.get_open_orders()
  794. if all_orders is None:
  795. await query.edit_message_text(f"❌ Could not fetch orders to cancel {token} orders")
  796. return
  797. # Filter orders for the specific token
  798. token_orders = [order for order in all_orders if order.get('symbol') == symbol]
  799. if not token_orders:
  800. await query.edit_message_text(f"📭 No open orders found for {token}")
  801. return
  802. # Cancel each order
  803. cancelled_orders = []
  804. failed_orders = []
  805. for order in token_orders:
  806. order_id = order.get('id')
  807. if order_id:
  808. try:
  809. success = self.client.cancel_order(order_id, symbol)
  810. if success:
  811. cancelled_orders.append(order)
  812. else:
  813. failed_orders.append(order)
  814. except Exception as e:
  815. logger.error(f"Failed to cancel order {order_id}: {e}")
  816. failed_orders.append(order)
  817. # Create result message
  818. result_message = f"""
  819. ✅ <b>Cancel Orders Results</b>
  820. 📊 <b>Summary:</b>
  821. • Token: {token}
  822. • Cancelled: {len(cancelled_orders)} orders
  823. • Failed: {len(failed_orders)} orders
  824. • Total Attempted: {len(token_orders)} orders
  825. """
  826. if cancelled_orders:
  827. result_message += f"\n🗑️ <b>Successfully Cancelled:</b>\n"
  828. for order in cancelled_orders:
  829. side = order.get('side', 'Unknown')
  830. amount = order.get('amount', 0)
  831. price = order.get('price', 0)
  832. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  833. result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n"
  834. if failed_orders:
  835. result_message += f"\n❌ <b>Failed to Cancel:</b>\n"
  836. for order in failed_orders:
  837. side = order.get('side', 'Unknown')
  838. amount = order.get('amount', 0)
  839. price = order.get('price', 0)
  840. order_id = order.get('id', 'Unknown')
  841. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  842. result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n"
  843. if len(cancelled_orders) == len(token_orders):
  844. result_message += f"\n🎉 All {token} orders successfully cancelled!"
  845. elif len(cancelled_orders) > 0:
  846. result_message += f"\n⚠️ Some orders cancelled. Check failed orders above."
  847. else:
  848. result_message += f"\n❌ Could not cancel any {token} orders."
  849. await query.edit_message_text(result_message, parse_mode='HTML')
  850. logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled")
  851. except Exception as e:
  852. error_message = f"❌ Error cancelling {token} orders: {str(e)}"
  853. await query.edit_message_text(error_message)
  854. logger.error(f"Error in COO execution: {e}")
  855. async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  856. """Execute a stop loss order."""
  857. symbol = f"{token}/USDC:USDC"
  858. try:
  859. await query.edit_message_text("⏳ Setting stop loss...")
  860. # Place stop loss order
  861. order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price)
  862. if order:
  863. # Record the trade in stats
  864. order_id = order.get('id', 'N/A')
  865. actual_price = order.get('average', price) # Use actual fill price if available
  866. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
  867. position_type = "LONG" if exit_side == "sell" else "SHORT"
  868. success_message = f"""
  869. ✅ <b>Stop Loss Order Set Successfully!</b>
  870. 📊 <b>Stop Loss Details:</b>
  871. • Token: {token}
  872. • Position: {position_type}
  873. • Size: {contracts} contracts
  874. • Stop Price: ${price:,.2f}
  875. • Action: {exit_side.upper()} (Close {position_type})
  876. • Amount: {contracts} {token}
  877. • Order Type: Limit Order
  878. • Order ID: <code>{order_id}</code>
  879. 🎯 <b>Stop Loss Execution:</b>
  880. • Status: SET
  881. • Exit Value: ~${contracts * price:,.2f}
  882. 📊 Use /stats to see updated performance metrics.
  883. """
  884. await query.edit_message_text(success_message, parse_mode='HTML')
  885. logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}")
  886. else:
  887. await query.edit_message_text("❌ Failed to set stop loss. Please try again.")
  888. except Exception as e:
  889. error_message = f"❌ Error setting stop loss: {str(e)}"
  890. await query.edit_message_text(error_message)
  891. logger.error(f"Error setting stop loss: {e}")
  892. async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  893. """Execute a take profit order."""
  894. symbol = f"{token}/USDC:USDC"
  895. try:
  896. await query.edit_message_text("⏳ Setting take profit...")
  897. # Place take profit order
  898. order = self.client.place_take_profit_order(symbol, exit_side, contracts, price)
  899. if order:
  900. # Record the trade in stats
  901. order_id = order.get('id', 'N/A')
  902. actual_price = order.get('average', price) # Use actual fill price if available
  903. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
  904. position_type = "LONG" if exit_side == "sell" else "SHORT"
  905. success_message = f"""
  906. ✅ <b>Take Profit Order Set Successfully!</b>
  907. 📊 <b>Take Profit Details:</b>
  908. • Token: {token}
  909. • Position: {position_type}
  910. • Size: {contracts} contracts
  911. • Target Price: ${price:,.2f}
  912. • Action: {exit_side.upper()} (Close {position_type})
  913. • Amount: {contracts} {token}
  914. • Order Type: Limit Order
  915. • Order ID: <code>{order_id}</code>
  916. 🎯 <b>Take Profit Execution:</b>
  917. • Status: SET
  918. • Exit Value: ~${contracts * price:,.2f}
  919. 📊 Use /stats to see updated performance metrics.
  920. """
  921. await query.edit_message_text(success_message, parse_mode='HTML')
  922. logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
  923. else:
  924. await query.edit_message_text("❌ Failed to set take profit. Please try again.")
  925. except Exception as e:
  926. error_message = f"❌ Error setting take profit: {str(e)}"
  927. await query.edit_message_text(error_message)
  928. logger.error(f"Error setting take profit: {e}")
  929. async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  930. """Handle unknown commands."""
  931. if not self.is_authorized(update.effective_chat.id):
  932. await update.message.reply_text("❌ Unauthorized access.")
  933. return
  934. await update.message.reply_text(
  935. "❓ Unknown command. Use /help to see available commands or tap the buttons in /start."
  936. )
  937. def setup_handlers(self):
  938. """Set up command handlers for the bot."""
  939. if not self.application:
  940. return
  941. # Command handlers
  942. self.application.add_handler(CommandHandler("start", self.start_command))
  943. self.application.add_handler(CommandHandler("help", self.help_command))
  944. self.application.add_handler(CommandHandler("commands", self.commands_command))
  945. self.application.add_handler(CommandHandler("c", self.commands_command))
  946. self.application.add_handler(CommandHandler("balance", self.balance_command))
  947. self.application.add_handler(CommandHandler("positions", self.positions_command))
  948. self.application.add_handler(CommandHandler("orders", self.orders_command))
  949. self.application.add_handler(CommandHandler("market", self.market_command))
  950. self.application.add_handler(CommandHandler("price", self.price_command))
  951. self.application.add_handler(CommandHandler("stats", self.stats_command))
  952. self.application.add_handler(CommandHandler("trades", self.trades_command))
  953. self.application.add_handler(CommandHandler("long", self.long_command))
  954. self.application.add_handler(CommandHandler("short", self.short_command))
  955. self.application.add_handler(CommandHandler("exit", self.exit_command))
  956. self.application.add_handler(CommandHandler("coo", self.coo_command))
  957. self.application.add_handler(CommandHandler("sl", self.sl_command))
  958. self.application.add_handler(CommandHandler("tp", self.tp_command))
  959. self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
  960. self.application.add_handler(CommandHandler("alarm", self.alarm_command))
  961. self.application.add_handler(CommandHandler("logs", self.logs_command))
  962. self.application.add_handler(CommandHandler("performance", self.performance_command))
  963. self.application.add_handler(CommandHandler("daily", self.daily_command))
  964. self.application.add_handler(CommandHandler("weekly", self.weekly_command))
  965. self.application.add_handler(CommandHandler("monthly", self.monthly_command))
  966. self.application.add_handler(CommandHandler("risk", self.risk_command))
  967. self.application.add_handler(CommandHandler("version", self.version_command))
  968. self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_command))
  969. # Callback query handler for inline keyboards
  970. self.application.add_handler(CallbackQueryHandler(self.button_callback))
  971. # Handle unknown commands
  972. self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
  973. async def run(self):
  974. """Run the Telegram bot."""
  975. if not Config.TELEGRAM_BOT_TOKEN:
  976. logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
  977. return
  978. if not Config.TELEGRAM_CHAT_ID:
  979. logger.error("❌ TELEGRAM_CHAT_ID not configured")
  980. return
  981. try:
  982. # Create application
  983. self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
  984. # Set up handlers
  985. self.setup_handlers()
  986. logger.info("🚀 Starting Telegram trading bot...")
  987. # Initialize the application
  988. await self.application.initialize()
  989. # Send startup notification
  990. await self.send_message(
  991. f"🤖 <b>Manual Trading Bot v{self.version} Started</b>\n\n"
  992. f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
  993. f"📊 Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n"
  994. f"📱 Manual trading ready!\n"
  995. f"🔄 Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
  996. f"🔄 External trade monitoring: Active\n"
  997. f"🔔 Price alarms: Active\n"
  998. f"📊 Auto stats sync: Enabled\n"
  999. f"📝 Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
  1000. f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
  1001. "Use /start for quick actions or /help for all commands."
  1002. )
  1003. # Perform initial log cleanup
  1004. try:
  1005. cleanup_logs(days_to_keep=30)
  1006. logger.info("🧹 Initial log cleanup completed")
  1007. except Exception as e:
  1008. logger.warning(f"⚠️ Initial log cleanup failed: {e}")
  1009. # Start the application
  1010. await self.application.start()
  1011. # Start order monitoring
  1012. await self.start_order_monitoring()
  1013. # Start polling for updates manually
  1014. logger.info("🔄 Starting update polling...")
  1015. # Get updates in a loop
  1016. last_update_id = 0
  1017. while True:
  1018. try:
  1019. # Get updates from Telegram
  1020. updates = await self.application.bot.get_updates(
  1021. offset=last_update_id + 1,
  1022. timeout=30,
  1023. allowed_updates=None
  1024. )
  1025. # Process each update
  1026. for update in updates:
  1027. last_update_id = update.update_id
  1028. # Process the update through the application
  1029. await self.application.process_update(update)
  1030. except Exception as e:
  1031. logger.error(f"Error processing updates: {e}")
  1032. await asyncio.sleep(5) # Wait before retrying
  1033. except asyncio.CancelledError:
  1034. logger.info("🛑 Bot polling cancelled")
  1035. raise
  1036. except Exception as e:
  1037. logger.error(f"❌ Error in telegram bot: {e}")
  1038. raise
  1039. finally:
  1040. # Clean shutdown
  1041. try:
  1042. await self.stop_order_monitoring()
  1043. if self.application:
  1044. await self.application.stop()
  1045. await self.application.shutdown()
  1046. except Exception as e:
  1047. logger.error(f"Error during shutdown: {e}")
  1048. async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1049. """Handle the /long command for opening long positions."""
  1050. if not self.is_authorized(update.effective_chat.id):
  1051. await update.message.reply_text("❌ Unauthorized access.")
  1052. return
  1053. try:
  1054. if not context.args or len(context.args) < 2:
  1055. await update.message.reply_text(
  1056. "❌ Usage: /long [token] [USDC amount] [price (optional)]\n"
  1057. "Examples:\n"
  1058. "• /long BTC 100 - Market order\n"
  1059. "• /long BTC 100 45000 - Limit order at $45,000"
  1060. )
  1061. return
  1062. token = context.args[0].upper()
  1063. usdc_amount = float(context.args[1])
  1064. # Check if price is provided for limit order
  1065. limit_price = None
  1066. if len(context.args) >= 3:
  1067. limit_price = float(context.args[2])
  1068. order_type = "Limit"
  1069. order_description = f"at ${limit_price:,.2f}"
  1070. else:
  1071. order_type = "Market"
  1072. order_description = "at current market price"
  1073. # Convert token to full symbol format for Hyperliquid
  1074. symbol = f"{token}/USDC:USDC"
  1075. # Get current market price to calculate amount and for display
  1076. market_data = self.client.get_market_data(symbol)
  1077. if not market_data:
  1078. await update.message.reply_text(f"❌ Could not fetch price for {token}")
  1079. return
  1080. current_price = float(market_data['ticker'].get('last', 0))
  1081. if current_price <= 0:
  1082. await update.message.reply_text(f"❌ Invalid price for {token}")
  1083. return
  1084. # Calculate token amount based on price (market or limit)
  1085. calculation_price = limit_price if limit_price else current_price
  1086. token_amount = usdc_amount / calculation_price
  1087. # Create confirmation message
  1088. confirmation_text = f"""
  1089. 🟢 <b>Long Position Confirmation</b>
  1090. 📊 <b>Order Details:</b>
  1091. • Token: {token}
  1092. • Direction: LONG (Buy)
  1093. • USDC Value: ${usdc_amount:,.2f}
  1094. • Current Price: ${current_price:,.2f}
  1095. • Order Type: {order_type} Order
  1096. • Token Amount: {token_amount:.6f} {token}
  1097. 🎯 <b>Execution:</b>
  1098. • Will buy {token_amount:.6f} {token} {order_description}
  1099. • Est. Value: ${token_amount * calculation_price:,.2f}
  1100. ⚠️ <b>Are you sure you want to open this long position?</b>
  1101. """
  1102. # Use limit_price for callback if provided, otherwise current_price
  1103. callback_price = limit_price if limit_price else current_price
  1104. callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}"
  1105. if limit_price:
  1106. callback_data += "_limit"
  1107. keyboard = [
  1108. [
  1109. InlineKeyboardButton("✅ Confirm Long", callback_data=callback_data),
  1110. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  1111. ]
  1112. ]
  1113. reply_markup = InlineKeyboardMarkup(keyboard)
  1114. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1115. except ValueError:
  1116. await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
  1117. except Exception as e:
  1118. await update.message.reply_text(f"❌ Error processing long command: {e}")
  1119. async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1120. """Handle the /short command for opening short positions."""
  1121. if not self.is_authorized(update.effective_chat.id):
  1122. await update.message.reply_text("❌ Unauthorized access.")
  1123. return
  1124. try:
  1125. if not context.args or len(context.args) < 2:
  1126. await update.message.reply_text(
  1127. "❌ Usage: /short [token] [USDC amount] [price (optional)]\n"
  1128. "Examples:\n"
  1129. "• /short BTC 100 - Market order\n"
  1130. "• /short BTC 100 46000 - Limit order at $46,000"
  1131. )
  1132. return
  1133. token = context.args[0].upper()
  1134. usdc_amount = float(context.args[1])
  1135. # Check if price is provided for limit order
  1136. limit_price = None
  1137. if len(context.args) >= 3:
  1138. limit_price = float(context.args[2])
  1139. order_type = "Limit"
  1140. order_description = f"at ${limit_price:,.2f}"
  1141. else:
  1142. order_type = "Market"
  1143. order_description = "at current market price"
  1144. # Convert token to full symbol format for Hyperliquid
  1145. symbol = f"{token}/USDC:USDC"
  1146. # Get current market price to calculate amount and for display
  1147. market_data = self.client.get_market_data(symbol)
  1148. if not market_data:
  1149. await update.message.reply_text(f"❌ Could not fetch price for {token}")
  1150. return
  1151. current_price = float(market_data['ticker'].get('last', 0))
  1152. if current_price <= 0:
  1153. await update.message.reply_text(f"❌ Invalid price for {token}")
  1154. return
  1155. # Calculate token amount based on price (market or limit)
  1156. calculation_price = limit_price if limit_price else current_price
  1157. token_amount = usdc_amount / calculation_price
  1158. # Create confirmation message
  1159. confirmation_text = f"""
  1160. 🔴 <b>Short Position Confirmation</b>
  1161. 📊 <b>Order Details:</b>
  1162. • Token: {token}
  1163. • Direction: SHORT (Sell)
  1164. • USDC Value: ${usdc_amount:,.2f}
  1165. • Current Price: ${current_price:,.2f}
  1166. • Order Type: {order_type} Order
  1167. • Token Amount: {token_amount:.6f} {token}
  1168. 🎯 <b>Execution:</b>
  1169. • Will sell {token_amount:.6f} {token} {order_description}
  1170. • Est. Value: ${token_amount * calculation_price:,.2f}
  1171. ⚠️ <b>Are you sure you want to open this short position?</b>
  1172. """
  1173. # Use limit_price for callback if provided, otherwise current_price
  1174. callback_price = limit_price if limit_price else current_price
  1175. callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}"
  1176. if limit_price:
  1177. callback_data += "_limit"
  1178. keyboard = [
  1179. [
  1180. InlineKeyboardButton("✅ Confirm Short", callback_data=callback_data),
  1181. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  1182. ]
  1183. ]
  1184. reply_markup = InlineKeyboardMarkup(keyboard)
  1185. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1186. except ValueError:
  1187. await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
  1188. except Exception as e:
  1189. await update.message.reply_text(f"❌ Error processing short command: {e}")
  1190. async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1191. """Handle the /exit command for closing positions."""
  1192. if not self.is_authorized(update.effective_chat.id):
  1193. await update.message.reply_text("❌ Unauthorized access.")
  1194. return
  1195. try:
  1196. if not context.args or len(context.args) < 1:
  1197. await update.message.reply_text(
  1198. "❌ Usage: /exit [token]\n"
  1199. "Example: /exit BTC"
  1200. )
  1201. return
  1202. token = context.args[0].upper()
  1203. symbol = f"{token}/USDC:USDC"
  1204. # Get current positions to find the position for this token
  1205. positions = self.client.get_positions()
  1206. if positions is None:
  1207. await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
  1208. return
  1209. # Find the position for this token
  1210. current_position = None
  1211. for position in positions:
  1212. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1213. current_position = position
  1214. break
  1215. if not current_position:
  1216. await update.message.reply_text(f"📭 No open position found for {token}")
  1217. return
  1218. # Extract position details
  1219. contracts = float(current_position.get('contracts', 0))
  1220. entry_price = float(current_position.get('entryPx', 0))
  1221. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1222. # Determine position direction and exit details
  1223. if contracts > 0:
  1224. position_type = "LONG"
  1225. exit_side = "sell"
  1226. exit_emoji = "🔴"
  1227. else:
  1228. position_type = "SHORT"
  1229. exit_side = "buy"
  1230. exit_emoji = "🟢"
  1231. contracts = abs(contracts) # Make positive for display
  1232. # Get current market price
  1233. market_data = self.client.get_market_data(symbol)
  1234. if not market_data:
  1235. await update.message.reply_text(f"❌ Could not fetch current price for {token}")
  1236. return
  1237. current_price = float(market_data['ticker'].get('last', 0))
  1238. if current_price <= 0:
  1239. await update.message.reply_text(f"❌ Invalid current price for {token}")
  1240. return
  1241. # Calculate estimated exit value
  1242. exit_value = contracts * current_price
  1243. # Create confirmation message
  1244. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  1245. confirmation_text = f"""
  1246. {exit_emoji} <b>Exit Position Confirmation</b>
  1247. 📊 <b>Position Details:</b>
  1248. • Token: {token}
  1249. • Position: {position_type}
  1250. • Size: {contracts} contracts
  1251. • Entry Price: ${entry_price:,.2f}
  1252. • Current Price: ${current_price:,.2f}
  1253. • {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
  1254. 🎯 <b>Exit Order:</b>
  1255. • Action: {exit_side.upper()} (Close {position_type})
  1256. • Amount: {contracts} {token}
  1257. • Est. Value: ~${exit_value:,.2f}
  1258. • Order Type: Market Order
  1259. ⚠️ <b>Are you sure you want to close this {position_type} position?</b>
  1260. This will place a market {exit_side} order to close your entire {token} position.
  1261. """
  1262. keyboard = [
  1263. [
  1264. InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"),
  1265. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  1266. ]
  1267. ]
  1268. reply_markup = InlineKeyboardMarkup(keyboard)
  1269. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1270. except ValueError:
  1271. await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
  1272. except Exception as e:
  1273. await update.message.reply_text(f"❌ Error processing exit command: {e}")
  1274. async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1275. """Handle the /coo (cancel open orders) command for a specific token."""
  1276. if not self.is_authorized(update.effective_chat.id):
  1277. await update.message.reply_text("❌ Unauthorized access.")
  1278. return
  1279. try:
  1280. if not context.args or len(context.args) < 1:
  1281. await update.message.reply_text(
  1282. "❌ Usage: /coo [token]\n"
  1283. "Example: /coo BTC\n\n"
  1284. "This command cancels ALL open orders for the specified token."
  1285. )
  1286. return
  1287. token = context.args[0].upper()
  1288. symbol = f"{token}/USDC:USDC"
  1289. # Get current orders for this token
  1290. all_orders = self.client.get_open_orders()
  1291. if all_orders is None:
  1292. await update.message.reply_text(f"❌ Could not fetch orders to cancel {token} orders")
  1293. return
  1294. # Filter orders for the specific token
  1295. token_orders = [order for order in all_orders if order.get('symbol') == symbol]
  1296. if not token_orders:
  1297. await update.message.reply_text(f"📭 No open orders found for {token}")
  1298. return
  1299. # Create confirmation message with order details
  1300. confirmation_text = f"""
  1301. ⚠️ <b>Cancel All {token} Orders</b>
  1302. 📋 <b>Orders to Cancel:</b>
  1303. """
  1304. total_value = 0
  1305. for order in token_orders:
  1306. side = order.get('side', 'Unknown')
  1307. amount = order.get('amount', 0)
  1308. price = order.get('price', 0)
  1309. order_id = order.get('id', 'Unknown')
  1310. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  1311. order_value = float(amount) * float(price)
  1312. total_value += order_value
  1313. confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n"
  1314. confirmation_text += f"""
  1315. 💰 <b>Total Value:</b> ${total_value:,.2f}
  1316. 🔢 <b>Orders Count:</b> {len(token_orders)}
  1317. ⚠️ <b>Are you sure you want to cancel ALL {token} orders?</b>
  1318. This action cannot be undone.
  1319. """
  1320. keyboard = [
  1321. [
  1322. InlineKeyboardButton(f"✅ Cancel All {token}", callback_data=f"confirm_coo_{token}"),
  1323. InlineKeyboardButton("❌ Keep Orders", callback_data="cancel_order")
  1324. ]
  1325. ]
  1326. reply_markup = InlineKeyboardMarkup(keyboard)
  1327. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1328. except ValueError:
  1329. await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
  1330. except Exception as e:
  1331. await update.message.reply_text(f"❌ Error processing cancel orders command: {e}")
  1332. async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1333. """Handle the /sl (stop loss) command for setting stop loss orders."""
  1334. if not self.is_authorized(update.effective_chat.id):
  1335. await update.message.reply_text("❌ Unauthorized access.")
  1336. return
  1337. try:
  1338. if not context.args or len(context.args) < 2:
  1339. await update.message.reply_text(
  1340. "❌ Usage: /sl [token] [price]\n"
  1341. "Example: /sl BTC 44000\n\n"
  1342. "This creates a stop loss order at the specified price."
  1343. )
  1344. return
  1345. token = context.args[0].upper()
  1346. stop_price = float(context.args[1])
  1347. symbol = f"{token}/USDC:USDC"
  1348. # Get current positions to find the position for this token
  1349. positions = self.client.get_positions()
  1350. if positions is None:
  1351. await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
  1352. return
  1353. # Find the position for this token
  1354. current_position = None
  1355. for position in positions:
  1356. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1357. current_position = position
  1358. break
  1359. if not current_position:
  1360. await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a stop loss.")
  1361. return
  1362. # Extract position details
  1363. contracts = float(current_position.get('contracts', 0))
  1364. entry_price = float(current_position.get('entryPx', 0))
  1365. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1366. # Determine position direction and validate stop loss price
  1367. if contracts > 0:
  1368. # Long position - stop loss should be below entry price
  1369. position_type = "LONG"
  1370. exit_side = "sell"
  1371. exit_emoji = "🔴"
  1372. contracts_abs = contracts
  1373. if stop_price >= entry_price:
  1374. await update.message.reply_text(
  1375. f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
  1376. f"📊 Your {token} LONG position:\n"
  1377. f"• Entry Price: ${entry_price:,.2f}\n"
  1378. f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
  1379. f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
  1380. )
  1381. return
  1382. else:
  1383. # Short position - stop loss should be above entry price
  1384. position_type = "SHORT"
  1385. exit_side = "buy"
  1386. exit_emoji = "🟢"
  1387. contracts_abs = abs(contracts)
  1388. if stop_price <= entry_price:
  1389. await update.message.reply_text(
  1390. f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
  1391. f"📊 Your {token} SHORT position:\n"
  1392. f"• Entry Price: ${entry_price:,.2f}\n"
  1393. f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
  1394. f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
  1395. )
  1396. return
  1397. # Get current market price for reference
  1398. market_data = self.client.get_market_data(symbol)
  1399. current_price = 0
  1400. if market_data:
  1401. current_price = float(market_data['ticker'].get('last', 0))
  1402. # Calculate estimated P&L at stop loss
  1403. if contracts > 0: # Long position
  1404. pnl_at_stop = (stop_price - entry_price) * contracts_abs
  1405. else: # Short position
  1406. pnl_at_stop = (entry_price - stop_price) * contracts_abs
  1407. # Create confirmation message
  1408. pnl_emoji = "🟢" if pnl_at_stop >= 0 else "🔴"
  1409. confirmation_text = f"""
  1410. 🛑 <b>Stop Loss Order Confirmation</b>
  1411. 📊 <b>Position Details:</b>
  1412. • Token: {token}
  1413. • Position: {position_type}
  1414. • Size: {contracts_abs} contracts
  1415. • Entry Price: ${entry_price:,.2f}
  1416. • Current Price: ${current_price:,.2f}
  1417. 🎯 <b>Stop Loss Order:</b>
  1418. • Stop Price: ${stop_price:,.2f}
  1419. • Action: {exit_side.upper()} (Close {position_type})
  1420. • Amount: {contracts_abs} {token}
  1421. • Order Type: Limit Order
  1422. • {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
  1423. ⚠️ <b>Are you sure you want to set this stop loss?</b>
  1424. This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
  1425. """
  1426. keyboard = [
  1427. [
  1428. InlineKeyboardButton(f"✅ Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"),
  1429. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  1430. ]
  1431. ]
  1432. reply_markup = InlineKeyboardMarkup(keyboard)
  1433. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1434. except ValueError:
  1435. await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
  1436. except Exception as e:
  1437. await update.message.reply_text(f"❌ Error processing stop loss command: {e}")
  1438. async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1439. """Handle the /tp (take profit) command for setting take profit orders."""
  1440. if not self.is_authorized(update.effective_chat.id):
  1441. await update.message.reply_text("❌ Unauthorized access.")
  1442. return
  1443. try:
  1444. if not context.args or len(context.args) < 2:
  1445. await update.message.reply_text(
  1446. "❌ Usage: /tp [token] [price]\n"
  1447. "Example: /tp BTC 50000\n\n"
  1448. "This creates a take profit order at the specified price."
  1449. )
  1450. return
  1451. token = context.args[0].upper()
  1452. profit_price = float(context.args[1])
  1453. symbol = f"{token}/USDC:USDC"
  1454. # Get current positions to find the position for this token
  1455. positions = self.client.get_positions()
  1456. if positions is None:
  1457. await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
  1458. return
  1459. # Find the position for this token
  1460. current_position = None
  1461. for position in positions:
  1462. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1463. current_position = position
  1464. break
  1465. if not current_position:
  1466. await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a take profit.")
  1467. return
  1468. # Extract position details
  1469. contracts = float(current_position.get('contracts', 0))
  1470. entry_price = float(current_position.get('entryPx', 0))
  1471. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1472. # Determine position direction and validate take profit price
  1473. if contracts > 0:
  1474. # Long position - take profit should be above entry price
  1475. position_type = "LONG"
  1476. exit_side = "sell"
  1477. exit_emoji = "🔴"
  1478. contracts_abs = contracts
  1479. if profit_price <= entry_price:
  1480. await update.message.reply_text(
  1481. f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n"
  1482. f"📊 Your {token} LONG position:\n"
  1483. f"• Entry Price: ${entry_price:,.2f}\n"
  1484. f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
  1485. f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
  1486. )
  1487. return
  1488. else:
  1489. # Short position - take profit should be below entry price
  1490. position_type = "SHORT"
  1491. exit_side = "buy"
  1492. exit_emoji = "🟢"
  1493. contracts_abs = abs(contracts)
  1494. if profit_price >= entry_price:
  1495. await update.message.reply_text(
  1496. f"⚠️ Take profit price should be BELOW entry price for short positions\n\n"
  1497. f"📊 Your {token} SHORT position:\n"
  1498. f"• Entry Price: ${entry_price:,.2f}\n"
  1499. f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
  1500. f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
  1501. )
  1502. return
  1503. # Get current market price for reference
  1504. market_data = self.client.get_market_data(symbol)
  1505. current_price = 0
  1506. if market_data:
  1507. current_price = float(market_data['ticker'].get('last', 0))
  1508. # Calculate estimated P&L at take profit
  1509. if contracts > 0: # Long position
  1510. pnl_at_tp = (profit_price - entry_price) * contracts_abs
  1511. else: # Short position
  1512. pnl_at_tp = (entry_price - profit_price) * contracts_abs
  1513. # Create confirmation message
  1514. pnl_emoji = "🟢" if pnl_at_tp >= 0 else "🔴"
  1515. confirmation_text = f"""
  1516. 🎯 <b>Take Profit Order Confirmation</b>
  1517. 📊 <b>Position Details:</b>
  1518. • Token: {token}
  1519. • Position: {position_type}
  1520. • Size: {contracts_abs} contracts
  1521. • Entry Price: ${entry_price:,.2f}
  1522. • Current Price: ${current_price:,.2f}
  1523. 💰 <b>Take Profit Order:</b>
  1524. • Target Price: ${profit_price:,.2f}
  1525. • Action: {exit_side.upper()} (Close {position_type})
  1526. • Amount: {contracts_abs} {token}
  1527. • Order Type: Limit Order
  1528. • {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
  1529. ⚠️ <b>Are you sure you want to set this take profit?</b>
  1530. This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position.
  1531. """
  1532. keyboard = [
  1533. [
  1534. InlineKeyboardButton(f"✅ Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"),
  1535. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  1536. ]
  1537. ]
  1538. reply_markup = InlineKeyboardMarkup(keyboard)
  1539. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1540. except ValueError:
  1541. await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
  1542. except Exception as e:
  1543. await update.message.reply_text(f"❌ Error processing take profit command: {e}")
  1544. async def start_order_monitoring(self):
  1545. """Start the order monitoring background task."""
  1546. if self.monitoring_active:
  1547. return
  1548. self.monitoring_active = True
  1549. logger.info("🔄 Starting order monitoring...")
  1550. # Initialize tracking data
  1551. await self._initialize_order_tracking()
  1552. # Start monitoring loop
  1553. asyncio.create_task(self._order_monitoring_loop())
  1554. async def stop_order_monitoring(self):
  1555. """Stop the order monitoring background task."""
  1556. self.monitoring_active = False
  1557. logger.info("⏹️ Stopping order monitoring...")
  1558. async def _initialize_order_tracking(self):
  1559. """Initialize order and position tracking."""
  1560. try:
  1561. # Get current open orders to initialize tracking
  1562. orders = self.client.get_open_orders()
  1563. if orders:
  1564. self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
  1565. logger.info(f"📋 Initialized tracking with {len(self.last_known_orders)} open orders")
  1566. # Get current positions for P&L tracking
  1567. positions = self.client.get_positions()
  1568. if positions:
  1569. for position in positions:
  1570. symbol = position.get('symbol')
  1571. contracts = float(position.get('contracts', 0))
  1572. entry_price = float(position.get('entryPx', 0))
  1573. if symbol and contracts != 0:
  1574. self.last_known_positions[symbol] = {
  1575. 'contracts': contracts,
  1576. 'entry_price': entry_price
  1577. }
  1578. logger.info(f"📊 Initialized tracking with {len(self.last_known_positions)} positions")
  1579. except Exception as e:
  1580. logger.error(f"❌ Error initializing order tracking: {e}")
  1581. async def _order_monitoring_loop(self):
  1582. """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
  1583. while self.monitoring_active:
  1584. try:
  1585. await self._check_order_fills()
  1586. await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval
  1587. except asyncio.CancelledError:
  1588. logger.info("🛑 Order monitoring cancelled")
  1589. break
  1590. except Exception as e:
  1591. logger.error(f"❌ Error in order monitoring loop: {e}")
  1592. await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error
  1593. async def _check_order_fills(self):
  1594. """Check for filled orders and send notifications."""
  1595. try:
  1596. # Get current orders and positions
  1597. current_orders = self.client.get_open_orders() or []
  1598. current_positions = self.client.get_positions() or []
  1599. # Get current order IDs
  1600. current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
  1601. # Find filled orders (orders that were in last_known_orders but not in current_orders)
  1602. filled_order_ids = self.last_known_orders - current_order_ids
  1603. if filled_order_ids:
  1604. logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders")
  1605. await self._process_filled_orders(filled_order_ids, current_positions)
  1606. # Update tracking data
  1607. self.last_known_orders = current_order_ids
  1608. await self._update_position_tracking(current_positions)
  1609. # Check price alarms
  1610. await self._check_price_alarms()
  1611. # Check external trades (trades made outside the bot)
  1612. await self._check_external_trades()
  1613. # Check stop losses (if risk management is enabled)
  1614. if Config.RISK_MANAGEMENT_ENABLED:
  1615. await self._check_stop_losses(current_positions)
  1616. # Check deposits/withdrawals (hourly)
  1617. await self._check_deposits_withdrawals()
  1618. except Exception as e:
  1619. logger.error(f"❌ Error checking order fills: {e}")
  1620. async def _check_price_alarms(self):
  1621. """Check all active price alarms."""
  1622. try:
  1623. # Get all active alarms
  1624. active_alarms = self.alarm_manager.get_all_active_alarms()
  1625. if not active_alarms:
  1626. return
  1627. # Get unique tokens from alarms
  1628. tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
  1629. # Fetch current prices for all tokens
  1630. price_data = {}
  1631. for token in tokens_to_check:
  1632. symbol = f"{token}/USDC:USDC"
  1633. market_data = self.client.get_market_data(symbol)
  1634. if market_data and market_data.get('ticker'):
  1635. current_price = market_data['ticker'].get('last')
  1636. if current_price is not None:
  1637. price_data[token] = float(current_price)
  1638. # Check alarms against current prices
  1639. triggered_alarms = self.alarm_manager.check_alarms(price_data)
  1640. # Send notifications for triggered alarms
  1641. for alarm in triggered_alarms:
  1642. await self._send_alarm_notification(alarm)
  1643. except Exception as e:
  1644. logger.error(f"❌ Error checking price alarms: {e}")
  1645. async def _send_alarm_notification(self, alarm: Dict[str, Any]):
  1646. """Send notification for triggered alarm."""
  1647. try:
  1648. message = self.alarm_manager.format_triggered_alarm(alarm)
  1649. await self.send_message(message)
  1650. logger.info(f"📢 Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
  1651. except Exception as e:
  1652. logger.error(f"❌ Error sending alarm notification: {e}")
  1653. async def _check_external_trades(self):
  1654. """Check for trades made outside the Telegram bot and update stats."""
  1655. try:
  1656. # Get recent fills from Hyperliquid
  1657. recent_fills = self.client.get_recent_fills()
  1658. if not recent_fills:
  1659. return
  1660. # Initialize last processed time if first run
  1661. if self.last_processed_trade_time is None:
  1662. # Set to current time minus 1 hour to catch recent activity
  1663. self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat()
  1664. # Filter for new trades since last check
  1665. new_trades = []
  1666. latest_trade_time = self.last_processed_trade_time
  1667. for fill in recent_fills:
  1668. fill_time = fill.get('timestamp')
  1669. if fill_time:
  1670. # Convert timestamps to comparable format
  1671. try:
  1672. # Convert fill_time to string if it's not already
  1673. if isinstance(fill_time, (int, float)):
  1674. # Assume it's a unix timestamp
  1675. fill_time_str = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time).isoformat()
  1676. else:
  1677. fill_time_str = str(fill_time)
  1678. # Compare as strings
  1679. if fill_time_str > self.last_processed_trade_time:
  1680. new_trades.append(fill)
  1681. if fill_time_str > latest_trade_time:
  1682. latest_trade_time = fill_time_str
  1683. except Exception as timestamp_error:
  1684. logger.warning(f"⚠️ Error processing timestamp {fill_time}: {timestamp_error}")
  1685. continue
  1686. if not new_trades:
  1687. return
  1688. # Process new trades
  1689. for trade in new_trades:
  1690. await self._process_external_trade(trade)
  1691. # Update last processed time
  1692. self.last_processed_trade_time = latest_trade_time
  1693. if new_trades:
  1694. logger.info(f"📊 Processed {len(new_trades)} external trades")
  1695. except Exception as e:
  1696. logger.error(f"❌ Error checking external trades: {e}")
  1697. async def _check_deposits_withdrawals(self):
  1698. """Check for deposits and withdrawals to maintain accurate P&L tracking."""
  1699. try:
  1700. # Check if it's time to run (hourly check)
  1701. current_time = datetime.now()
  1702. if self.last_deposit_withdrawal_check is not None:
  1703. time_since_last_check = (current_time - self.last_deposit_withdrawal_check).total_seconds()
  1704. if time_since_last_check < self.deposit_withdrawal_check_interval:
  1705. return # Not time to check yet
  1706. logger.info("🔍 Checking for deposits and withdrawals...")
  1707. # Initialize last check time if first run
  1708. if self.last_deposit_withdrawal_check is None:
  1709. # Set to 24 hours ago to catch recent activity
  1710. self.last_deposit_withdrawal_check = current_time - timedelta(hours=24)
  1711. # Calculate timestamp for API calls (last check time)
  1712. since_timestamp = int(self.last_deposit_withdrawal_check.timestamp() * 1000) # Hyperliquid expects milliseconds
  1713. # Track new deposits/withdrawals
  1714. new_deposits = 0
  1715. new_withdrawals = 0
  1716. # Check for deposits
  1717. try:
  1718. deposits = self.client.client.fetch_deposits(code='USDC', since=since_timestamp)
  1719. if deposits:
  1720. for deposit in deposits:
  1721. amount = float(deposit.get('amount', 0))
  1722. timestamp = deposit.get('datetime', datetime.now().isoformat())
  1723. deposit_id = deposit.get('id', 'unknown')
  1724. # Record in stats to adjust P&L calculations
  1725. self.stats.record_deposit(amount, timestamp, deposit_id)
  1726. new_deposits += 1
  1727. # Send notification
  1728. await self._send_deposit_notification(amount, timestamp)
  1729. except Exception as e:
  1730. logger.warning(f"⚠️ Error fetching deposits: {e}")
  1731. # Check for withdrawals
  1732. try:
  1733. withdrawals = self.client.client.fetch_withdrawals(code='USDC', since=since_timestamp)
  1734. if withdrawals:
  1735. for withdrawal in withdrawals:
  1736. amount = float(withdrawal.get('amount', 0))
  1737. timestamp = withdrawal.get('datetime', datetime.now().isoformat())
  1738. withdrawal_id = withdrawal.get('id', 'unknown')
  1739. # Record in stats to adjust P&L calculations
  1740. self.stats.record_withdrawal(amount, timestamp, withdrawal_id)
  1741. new_withdrawals += 1
  1742. # Send notification
  1743. await self._send_withdrawal_notification(amount, timestamp)
  1744. except Exception as e:
  1745. logger.warning(f"⚠️ Error fetching withdrawals: {e}")
  1746. # Update last check time
  1747. self.last_deposit_withdrawal_check = current_time
  1748. if new_deposits > 0 or new_withdrawals > 0:
  1749. logger.info(f"💰 Processed {new_deposits} deposits and {new_withdrawals} withdrawals")
  1750. # Get updated balance adjustments summary
  1751. adjustments = self.stats.get_balance_adjustments_summary()
  1752. logger.info(f"📊 Total adjustments: ${adjustments['net_adjustment']:,.2f} net ({adjustments['adjustment_count']} total)")
  1753. except Exception as e:
  1754. logger.error(f"❌ Error checking deposits/withdrawals: {e}")
  1755. async def _send_deposit_notification(self, amount: float, timestamp: str):
  1756. """Send notification for detected deposit."""
  1757. try:
  1758. time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
  1759. message = f"""
  1760. 💰 <b>Deposit Detected</b>
  1761. 💵 <b>Amount:</b> ${amount:,.2f} USDC
  1762. ⏰ <b>Time:</b> {time_str}
  1763. 📊 <b>P&L Impact:</b>
  1764. • Initial balance adjusted to maintain accurate P&L
  1765. • Trading statistics unaffected by balance change
  1766. • This deposit will not show as trading profit
  1767. ✅ <b>Balance tracking updated automatically</b>
  1768. """
  1769. await self.send_message(message.strip())
  1770. logger.info(f"📱 Sent deposit notification: ${amount:,.2f}")
  1771. except Exception as e:
  1772. logger.error(f"❌ Error sending deposit notification: {e}")
  1773. async def _send_withdrawal_notification(self, amount: float, timestamp: str):
  1774. """Send notification for detected withdrawal."""
  1775. try:
  1776. time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
  1777. message = f"""
  1778. 💸 <b>Withdrawal Detected</b>
  1779. 💵 <b>Amount:</b> ${amount:,.2f} USDC
  1780. ⏰ <b>Time:</b> {time_str}
  1781. 📊 <b>P&L Impact:</b>
  1782. • Initial balance adjusted to maintain accurate P&L
  1783. • Trading statistics unaffected by balance change
  1784. • This withdrawal will not show as trading loss
  1785. ✅ <b>Balance tracking updated automatically</b>
  1786. """
  1787. await self.send_message(message.strip())
  1788. logger.info(f"📱 Sent withdrawal notification: ${amount:,.2f}")
  1789. except Exception as e:
  1790. logger.error(f"❌ Error sending withdrawal notification: {e}")
  1791. async def _process_external_trade(self, trade: Dict[str, Any]):
  1792. """Process an individual external trade and determine if it's opening or closing a position."""
  1793. try:
  1794. # Extract trade information
  1795. symbol = trade.get('symbol', '')
  1796. side = trade.get('side', '')
  1797. amount = float(trade.get('amount', 0))
  1798. price = float(trade.get('price', 0))
  1799. trade_id = trade.get('id', 'external')
  1800. timestamp = trade.get('timestamp', '')
  1801. if not all([symbol, side, amount, price]):
  1802. return
  1803. # Record trade in stats and get action type using enhanced tracking
  1804. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external")
  1805. # Send enhanced notification based on action type
  1806. await self._send_enhanced_trade_notification(symbol, side, amount, price, action_type, timestamp)
  1807. logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price} ({action_type})")
  1808. except Exception as e:
  1809. logger.error(f"❌ Error processing external trade: {e}")
  1810. async def _send_enhanced_trade_notification(self, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str = None):
  1811. """Send enhanced trade notification based on position action type."""
  1812. try:
  1813. token = symbol.split('/')[0] if '/' in symbol else symbol
  1814. position = self.stats.get_enhanced_position_state(symbol)
  1815. if timestamp is None:
  1816. time_str = datetime.now().strftime('%H:%M:%S')
  1817. else:
  1818. try:
  1819. time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  1820. time_str = time_obj.strftime('%H:%M:%S')
  1821. except:
  1822. time_str = "Unknown"
  1823. # Handle different action types
  1824. if action_type in ['long_opened', 'short_opened']:
  1825. await self._send_position_opened_notification(token, side, amount, price, action_type, time_str)
  1826. elif action_type in ['long_increased', 'short_increased']:
  1827. await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str)
  1828. elif action_type in ['long_reduced', 'short_reduced']:
  1829. pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
  1830. await self._send_position_reduced_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
  1831. elif action_type in ['long_closed', 'short_closed']:
  1832. pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
  1833. await self._send_position_closed_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
  1834. elif action_type in ['long_closed_and_short_opened', 'short_closed_and_long_opened']:
  1835. await self._send_position_flipped_notification(token, side, amount, price, action_type, time_str)
  1836. else:
  1837. # Fallback to generic notification
  1838. await self._send_external_trade_notification({
  1839. 'symbol': symbol,
  1840. 'side': side,
  1841. 'amount': amount,
  1842. 'price': price,
  1843. 'timestamp': timestamp or datetime.now().isoformat()
  1844. })
  1845. except Exception as e:
  1846. logger.error(f"❌ Error sending enhanced trade notification: {e}")
  1847. async def _send_position_opened_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
  1848. """Send notification for newly opened position."""
  1849. position_type = "LONG" if action_type == 'long_opened' else "SHORT"
  1850. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  1851. trade_value = amount * price
  1852. message = f"""
  1853. 🚀 <b>Position Opened</b>
  1854. 📊 <b>New {position_type} Position:</b>
  1855. • Token: {token}
  1856. • Direction: {position_type}
  1857. • Entry Size: {amount} {token}
  1858. • Entry Price: ${price:,.2f}
  1859. • Position Value: ${trade_value:,.2f}
  1860. {side_emoji} <b>Trade Details:</b>
  1861. • Side: {side.upper()}
  1862. • Order Type: Market/Limit
  1863. • Status: OPENED ✅
  1864. ⏰ <b>Time:</b> {time_str}
  1865. 📈 <b>Note:</b> New {position_type} position established
  1866. 📊 Use /positions to view current holdings
  1867. """
  1868. await self.send_message(message.strip())
  1869. logger.info(f"📢 Position opened: {token} {position_type} {amount} @ ${price}")
  1870. async def _send_position_increased_notification(self, token: str, side: str, amount: float, price: float, position: Dict, action_type: str, time_str: str):
  1871. """Send notification for position increase (additional entry)."""
  1872. position_type = "LONG" if action_type == 'long_increased' else "SHORT"
  1873. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  1874. total_size = abs(position['contracts'])
  1875. avg_entry = position['avg_entry_price']
  1876. entry_count = position['entry_count']
  1877. total_value = total_size * avg_entry
  1878. message = f"""
  1879. 📈 <b>Position Increased</b>
  1880. 📊 <b>{position_type} Position Updated:</b>
  1881. • Token: {token}
  1882. • Direction: {position_type}
  1883. • Added Size: {amount} {token} @ ${price:,.2f}
  1884. • New Total Size: {total_size} {token}
  1885. • Average Entry: ${avg_entry:,.2f}
  1886. {side_emoji} <b>Position Summary:</b>
  1887. • Total Value: ${total_value:,.2f}
  1888. • Entry Points: {entry_count}
  1889. • Last Entry: ${price:,.2f}
  1890. • Status: INCREASED ⬆️
  1891. ⏰ <b>Time:</b> {time_str}
  1892. 💡 <b>Strategy:</b> Multiple entry averaging
  1893. 📊 Use /positions for complete position details
  1894. """
  1895. await self.send_message(message.strip())
  1896. logger.info(f"📢 Position increased: {token} {position_type} +{amount} @ ${price} (total: {total_size})")
  1897. 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):
  1898. """Send notification for partial position close."""
  1899. position_type = "LONG" if action_type == 'long_reduced' else "SHORT"
  1900. remaining_size = abs(position['contracts'])
  1901. avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
  1902. pnl = pnl_data['pnl']
  1903. pnl_percent = pnl_data['pnl_percent']
  1904. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  1905. partial_value = amount * price
  1906. message = f"""
  1907. 📉 <b>Position Partially Closed</b>
  1908. 📊 <b>{position_type} Partial Exit:</b>
  1909. • Token: {token}
  1910. • Direction: {position_type}
  1911. • Closed Size: {amount} {token}
  1912. • Exit Price: ${price:,.2f}
  1913. • Remaining Size: {remaining_size} {token}
  1914. {pnl_emoji} <b>Partial P&L:</b>
  1915. • Entry Price: ${avg_entry:,.2f}
  1916. • Exit Value: ${partial_value:,.2f}
  1917. • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
  1918. • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
  1919. 💰 <b>Position Status:</b>
  1920. • Status: PARTIALLY CLOSED 📉
  1921. • Take Profit Strategy: Active
  1922. ⏰ <b>Time:</b> {time_str}
  1923. 📊 Use /positions to view remaining position
  1924. """
  1925. await self.send_message(message.strip())
  1926. logger.info(f"📢 Position reduced: {token} {position_type} -{amount} @ ${price} P&L: ${pnl:.2f}")
  1927. 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):
  1928. """Send notification for fully closed position."""
  1929. position_type = "LONG" if action_type == 'long_closed' else "SHORT"
  1930. avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
  1931. pnl = pnl_data['pnl']
  1932. pnl_percent = pnl_data['pnl_percent']
  1933. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  1934. entry_count = position.get('entry_count', 1)
  1935. exit_value = amount * price
  1936. message = f"""
  1937. 🎯 <b>Position Fully Closed</b>
  1938. 📊 <b>{position_type} Position Summary:</b>
  1939. • Token: {token}
  1940. • Direction: {position_type}
  1941. • Total Size: {amount} {token}
  1942. • Average Entry: ${avg_entry:,.2f}
  1943. • Exit Price: ${price:,.2f}
  1944. • Exit Value: ${exit_value:,.2f}
  1945. {pnl_emoji} <b>Total P&L:</b>
  1946. • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
  1947. • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
  1948. • Entry Points Used: {entry_count}
  1949. ✅ <b>Trade Complete:</b>
  1950. • Status: FULLY CLOSED 🎯
  1951. • Position: FLAT
  1952. ⏰ <b>Time:</b> {time_str}
  1953. 📊 Use /stats to view updated performance
  1954. """
  1955. await self.send_message(message.strip())
  1956. logger.info(f"📢 Position closed: {token} {position_type} {amount} @ ${price} Total P&L: ${pnl:.2f}")
  1957. async def _send_position_flipped_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
  1958. """Send notification for position flip (close and reverse)."""
  1959. if action_type == 'long_closed_and_short_opened':
  1960. old_type = "LONG"
  1961. new_type = "SHORT"
  1962. else:
  1963. old_type = "SHORT"
  1964. new_type = "LONG"
  1965. message = f"""
  1966. 🔄 <b>Position Flipped</b>
  1967. 📊 <b>Direction Change:</b>
  1968. • Token: {token}
  1969. • Previous: {old_type} position
  1970. • New: {new_type} position
  1971. • Size: {amount} {token}
  1972. • Price: ${price:,.2f}
  1973. 🎯 <b>Trade Summary:</b>
  1974. • {old_type} position: CLOSED ✅
  1975. • {new_type} position: OPENED 🚀
  1976. • Flip Price: ${price:,.2f}
  1977. • Status: POSITION REVERSED
  1978. ⏰ <b>Time:</b> {time_str}
  1979. 💡 <b>Strategy:</b> Directional change
  1980. 📊 Use /positions to view new position
  1981. """
  1982. await self.send_message(message.strip())
  1983. logger.info(f"📢 Position flipped: {token} {old_type} -> {new_type} @ ${price}")
  1984. async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1985. """Handle the /monitoring command to show monitoring status."""
  1986. if not self.is_authorized(update.effective_chat.id):
  1987. await update.message.reply_text("❌ Unauthorized access.")
  1988. return
  1989. # Get alarm statistics
  1990. alarm_stats = self.alarm_manager.get_statistics()
  1991. # Get balance adjustments info
  1992. adjustments_summary = self.stats.get_balance_adjustments_summary()
  1993. last_deposit_check = "Never"
  1994. next_deposit_check = "Unknown"
  1995. if self.last_deposit_withdrawal_check:
  1996. last_deposit_check = self.last_deposit_withdrawal_check.strftime('%H:%M:%S')
  1997. next_check_time = self.last_deposit_withdrawal_check + timedelta(seconds=self.deposit_withdrawal_check_interval)
  1998. next_deposit_check = next_check_time.strftime('%H:%M:%S')
  1999. status_text = f"""
  2000. 🔄 <b>System Monitoring Status</b>
  2001. 📊 <b>Order Monitoring:</b>
  2002. • Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
  2003. • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
  2004. • Orders Tracked: {len(self.last_known_orders)}
  2005. • Positions Tracked: {len(self.last_known_positions)}
  2006. 💰 <b>Deposit/Withdrawal Monitoring:</b>
  2007. • Check Interval: {self.deposit_withdrawal_check_interval // 3600} hour(s)
  2008. • Last Check: {last_deposit_check}
  2009. • Next Check: {next_deposit_check}
  2010. • Total Adjustments: {adjustments_summary['adjustment_count']}
  2011. • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  2012. 🔔 <b>Price Alarms:</b>
  2013. • Active Alarms: {alarm_stats['total_active']}
  2014. • Triggered Today: {alarm_stats['total_triggered']}
  2015. • Tokens Monitored: {alarm_stats['tokens_tracked']}
  2016. • Next Alarm ID: {alarm_stats['next_id']}
  2017. 🔄 <b>External Trade Monitoring:</b>
  2018. • Last Check: {self.last_processed_trade_time or 'Not started'}
  2019. • Auto Stats Update: ✅ Enabled
  2020. • External Notifications: ✅ Enabled
  2021. 🛡️ <b>Risk Management:</b>
  2022. • Automatic Stop Loss: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
  2023. • Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
  2024. • Position Monitoring: {'✅ Active' if Config.RISK_MANAGEMENT_ENABLED else '❌ Inactive'}
  2025. 📈 <b>Notifications:</b>
  2026. • 🚀 Position Opened/Increased
  2027. • 📉 Position Partially/Fully Closed
  2028. • 🎯 P&L Calculations
  2029. • 🔔 Price Alarm Triggers
  2030. • 🔄 External Trade Detection
  2031. • 💰 Deposit/Withdrawal Detection
  2032. • 🛑 Automatic Stop Loss Triggers
  2033. ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
  2034. 💡 <b>Monitoring Features:</b>
  2035. • Real-time order fill detection
  2036. • Automatic P&L calculation
  2037. • Position change tracking
  2038. • Price alarm monitoring
  2039. • External trade monitoring
  2040. • Deposit/withdrawal tracking
  2041. • Auto stats synchronization
  2042. • Instant Telegram notifications
  2043. """
  2044. if alarm_stats['token_breakdown']:
  2045. status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
  2046. for token, count in alarm_stats['token_breakdown'].items():
  2047. status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
  2048. await update.message.reply_text(status_text.strip(), parse_mode='HTML')
  2049. async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2050. """Handle the /alarm command for price alerts."""
  2051. if not self.is_authorized(update.effective_chat.id):
  2052. await update.message.reply_text("❌ Unauthorized access.")
  2053. return
  2054. try:
  2055. if not context.args or len(context.args) == 0:
  2056. # No arguments - list all alarms
  2057. alarms = self.alarm_manager.get_all_active_alarms()
  2058. message = self.alarm_manager.format_alarm_list(alarms)
  2059. await update.message.reply_text(message, parse_mode='HTML')
  2060. return
  2061. elif len(context.args) == 1:
  2062. arg = context.args[0]
  2063. # Check if argument is a number (alarm ID to remove)
  2064. try:
  2065. alarm_id = int(arg)
  2066. # Remove alarm by ID
  2067. if self.alarm_manager.remove_alarm(alarm_id):
  2068. await update.message.reply_text(f"✅ Alarm ID {alarm_id} has been removed.")
  2069. else:
  2070. await update.message.reply_text(f"❌ Alarm ID {alarm_id} not found.")
  2071. return
  2072. except ValueError:
  2073. # Not a number, treat as token
  2074. token = arg.upper()
  2075. alarms = self.alarm_manager.get_alarms_by_token(token)
  2076. message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
  2077. await update.message.reply_text(message, parse_mode='HTML')
  2078. return
  2079. elif len(context.args) == 2:
  2080. # Set new alarm: /alarm TOKEN PRICE
  2081. token = context.args[0].upper()
  2082. target_price = float(context.args[1])
  2083. # Get current market price
  2084. symbol = f"{token}/USDC:USDC"
  2085. market_data = self.client.get_market_data(symbol)
  2086. if not market_data or not market_data.get('ticker'):
  2087. await update.message.reply_text(f"❌ Could not fetch current price for {token}")
  2088. return
  2089. current_price = float(market_data['ticker'].get('last', 0))
  2090. if current_price <= 0:
  2091. await update.message.reply_text(f"❌ Invalid current price for {token}")
  2092. return
  2093. # Create the alarm
  2094. alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
  2095. # Format confirmation message
  2096. direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
  2097. price_diff = abs(target_price - current_price)
  2098. price_diff_percent = (price_diff / current_price) * 100
  2099. message = f"""
  2100. ✅ <b>Price Alarm Created</b>
  2101. 📊 <b>Alarm Details:</b>
  2102. • Alarm ID: {alarm['id']}
  2103. • Token: {token}
  2104. • Target Price: ${target_price:,.2f}
  2105. • Current Price: ${current_price:,.2f}
  2106. • Direction: {alarm['direction'].upper()}
  2107. {direction_emoji} <b>Alert Condition:</b>
  2108. Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
  2109. 💰 <b>Price Difference:</b>
  2110. • Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
  2111. • Status: ACTIVE ✅
  2112. ⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
  2113. 💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
  2114. """
  2115. await update.message.reply_text(message.strip(), parse_mode='HTML')
  2116. else:
  2117. # Too many arguments
  2118. await update.message.reply_text(
  2119. "❌ Invalid usage. Examples:\n\n"
  2120. "• <code>/alarm</code> - List all alarms\n"
  2121. "• <code>/alarm BTC</code> - List BTC alarms\n"
  2122. "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
  2123. "• <code>/alarm 3</code> - Remove alarm ID 3",
  2124. parse_mode='HTML'
  2125. )
  2126. except ValueError:
  2127. await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
  2128. except Exception as e:
  2129. error_message = f"❌ Error processing alarm command: {str(e)}"
  2130. await update.message.reply_text(error_message)
  2131. logger.error(f"Error in alarm command: {e}")
  2132. async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2133. """Handle the /logs command to show log file statistics and cleanup options."""
  2134. if not self.is_authorized(update.effective_chat.id):
  2135. await update.message.reply_text("❌ Unauthorized access.")
  2136. return
  2137. try:
  2138. # Check for cleanup argument
  2139. if context.args and len(context.args) >= 1:
  2140. if context.args[0].lower() == 'cleanup':
  2141. # Get days parameter (default 30)
  2142. days_to_keep = 30
  2143. if len(context.args) >= 2:
  2144. try:
  2145. days_to_keep = int(context.args[1])
  2146. except ValueError:
  2147. await update.message.reply_text("❌ Invalid number of days. Using default (30).")
  2148. # Perform cleanup
  2149. await update.message.reply_text(f"🧹 Cleaning up log files older than {days_to_keep} days...")
  2150. cleanup_logs(days_to_keep)
  2151. await update.message.reply_text(f"✅ Log cleanup completed!")
  2152. return
  2153. # Show log statistics
  2154. log_stats_text = format_log_stats()
  2155. # Add additional info
  2156. status_text = f"""
  2157. 📊 <b>System Logging Status</b>
  2158. {log_stats_text}
  2159. 📈 <b>Log Configuration:</b>
  2160. • Log Level: {Config.LOG_LEVEL}
  2161. • Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
  2162. • Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  2163. 💡 <b>Log Management:</b>
  2164. • <code>/logs cleanup</code> - Clean old logs (30 days)
  2165. • <code>/logs cleanup 7</code> - Clean logs older than 7 days
  2166. • Log rotation happens automatically
  2167. • Old backups are removed automatically
  2168. 🔧 <b>Configuration:</b>
  2169. • Rotation Type: {Config.LOG_ROTATION_TYPE}
  2170. • Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
  2171. • Backup Count: {Config.LOG_BACKUP_COUNT}
  2172. """
  2173. await update.message.reply_text(status_text.strip(), parse_mode='HTML')
  2174. except Exception as e:
  2175. error_message = f"❌ Error processing logs command: {str(e)}"
  2176. await update.message.reply_text(error_message)
  2177. logger.error(f"Error in logs command: {e}")
  2178. async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2179. """Handle the /performance command to show token performance ranking or detailed stats."""
  2180. if not self.is_authorized(update.effective_chat.id):
  2181. await update.message.reply_text("❌ Unauthorized access.")
  2182. return
  2183. try:
  2184. # Check if specific token is requested
  2185. if context.args and len(context.args) >= 1:
  2186. # Detailed performance for specific token
  2187. token = context.args[0].upper()
  2188. await self._show_token_performance(update, token)
  2189. else:
  2190. # Show token performance ranking
  2191. await self._show_performance_ranking(update)
  2192. except Exception as e:
  2193. error_message = f"❌ Error processing performance command: {str(e)}"
  2194. await update.message.reply_text(error_message)
  2195. logger.error(f"Error in performance command: {e}")
  2196. async def _show_performance_ranking(self, update: Update):
  2197. """Show token performance ranking (compressed view)."""
  2198. token_performance = self.stats.get_token_performance()
  2199. if not token_performance:
  2200. await update.message.reply_text(
  2201. "📊 <b>Token Performance</b>\n\n"
  2202. "📭 No trading data available yet.\n\n"
  2203. "💡 Performance tracking starts after your first completed trades.\n"
  2204. "Use /long or /short to start trading!",
  2205. parse_mode='HTML'
  2206. )
  2207. return
  2208. # Sort tokens by total P&L (best to worst)
  2209. sorted_tokens = sorted(
  2210. token_performance.items(),
  2211. key=lambda x: x[1]['total_pnl'],
  2212. reverse=True
  2213. )
  2214. performance_text = "🏆 <b>Token Performance Ranking</b>\n\n"
  2215. # Add ranking with emojis
  2216. for i, (token, stats) in enumerate(sorted_tokens, 1):
  2217. # Ranking emoji
  2218. if i == 1:
  2219. rank_emoji = "🥇"
  2220. elif i == 2:
  2221. rank_emoji = "🥈"
  2222. elif i == 3:
  2223. rank_emoji = "🥉"
  2224. else:
  2225. rank_emoji = f"#{i}"
  2226. # P&L emoji
  2227. pnl_emoji = "🟢" if stats['total_pnl'] >= 0 else "🔴"
  2228. # Format the line
  2229. performance_text += f"{rank_emoji} <b>{token}</b>\n"
  2230. performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n"
  2231. performance_text += f" 📊 Trades: {stats['completed_trades']}"
  2232. # Add win rate if there are completed trades
  2233. if stats['completed_trades'] > 0:
  2234. performance_text += f" | Win: {stats['win_rate']:.0f}%"
  2235. performance_text += "\n\n"
  2236. # Add summary
  2237. total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
  2238. total_trades = sum(stats['completed_trades'] for stats in token_performance.values())
  2239. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  2240. performance_text += f"💼 <b>Portfolio Summary:</b>\n"
  2241. performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2242. performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
  2243. performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
  2244. performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
  2245. await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
  2246. async def _show_token_performance(self, update: Update, token: str):
  2247. """Show detailed performance for a specific token."""
  2248. token_stats = self.stats.get_token_detailed_stats(token)
  2249. # Check if token has any data
  2250. if token_stats.get('total_trades', 0) == 0:
  2251. await update.message.reply_text(
  2252. f"📊 <b>{token} Performance</b>\n\n"
  2253. f"📭 No trading history found for {token}.\n\n"
  2254. f"💡 Start trading {token} with:\n"
  2255. f"• <code>/long {token} 100</code>\n"
  2256. f"• <code>/short {token} 100</code>\n\n"
  2257. f"🔄 Use <code>/performance</code> to see all token rankings.",
  2258. parse_mode='HTML'
  2259. )
  2260. return
  2261. # Check if there's a message (no completed trades)
  2262. if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
  2263. await update.message.reply_text(
  2264. f"📊 <b>{token} Performance</b>\n\n"
  2265. f"{token_stats['message']}\n\n"
  2266. f"📈 <b>Current Activity:</b>\n"
  2267. f"• Total Trades: {token_stats['total_trades']}\n"
  2268. f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
  2269. f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
  2270. f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
  2271. f"💡 Complete some trades to see P&L statistics!\n"
  2272. f"🔄 Use <code>/performance</code> to see all token rankings.",
  2273. parse_mode='HTML'
  2274. )
  2275. return
  2276. # Detailed stats display
  2277. pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
  2278. performance_text = f"""
  2279. 📊 <b>{token} Detailed Performance</b>
  2280. 💰 <b>P&L Summary:</b>
  2281. • {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
  2282. • 💵 Total Volume: ${token_stats['completed_volume']:,.2f}
  2283. • 📈 Expectancy: ${token_stats['expectancy']:,.2f}
  2284. 📊 <b>Trading Activity:</b>
  2285. • Total Trades: {token_stats['total_trades']}
  2286. • Completed: {token_stats['completed_trades']}
  2287. • Buy Orders: {token_stats['buy_trades']}
  2288. • Sell Orders: {token_stats['sell_trades']}
  2289. 🏆 <b>Performance Metrics:</b>
  2290. • Win Rate: {token_stats['win_rate']:.1f}%
  2291. • Profit Factor: {token_stats['profit_factor']:.2f}
  2292. • Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
  2293. 💡 <b>Best/Worst:</b>
  2294. • Largest Win: ${token_stats['largest_win']:,.2f}
  2295. • Largest Loss: ${token_stats['largest_loss']:,.2f}
  2296. • Avg Win: ${token_stats['avg_win']:,.2f}
  2297. • Avg Loss: ${token_stats['avg_loss']:,.2f}
  2298. """
  2299. # Add recent trades if available
  2300. if token_stats.get('recent_trades'):
  2301. performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
  2302. for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
  2303. trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  2304. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  2305. pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
  2306. performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
  2307. performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
  2308. await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
  2309. async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2310. """Handle the /daily command to show daily performance stats."""
  2311. if not self.is_authorized(update.effective_chat.id):
  2312. await update.message.reply_text("❌ Unauthorized access.")
  2313. return
  2314. try:
  2315. daily_stats = self.stats.get_daily_stats(10)
  2316. if not daily_stats:
  2317. await update.message.reply_text(
  2318. "📅 <b>Daily Performance</b>\n\n"
  2319. "📭 No daily performance data available yet.\n\n"
  2320. "💡 Daily stats are calculated from completed trades.\n"
  2321. "Start trading to see daily performance!",
  2322. parse_mode='HTML'
  2323. )
  2324. return
  2325. daily_text = "📅 <b>Daily Performance (Last 10 Days)</b>\n\n"
  2326. total_pnl = 0
  2327. total_trades = 0
  2328. trading_days = 0
  2329. for day_stats in daily_stats:
  2330. if day_stats['has_trades']:
  2331. # Day with completed trades
  2332. pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
  2333. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  2334. daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
  2335. daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
  2336. total_pnl += day_stats['pnl']
  2337. total_trades += day_stats['trades']
  2338. trading_days += 1
  2339. else:
  2340. # Day with no trades
  2341. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  2342. daily_text += f" 📭 No completed trades\n\n"
  2343. # Add summary
  2344. if trading_days > 0:
  2345. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  2346. daily_text += f"💼 <b>10-Day Summary:</b>\n"
  2347. daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2348. daily_text += f" 🔄 Total Trades: {total_trades}\n"
  2349. daily_text += f" 📈 Trading Days: {trading_days}/10\n"
  2350. daily_text += f" 📊 Avg per Trading Day: ${total_pnl/trading_days:,.2f}"
  2351. else:
  2352. daily_text += f"💼 <b>10-Day Summary:</b>\n"
  2353. daily_text += f" 📭 No completed trades in the last 10 days\n"
  2354. daily_text += f" 💡 Start trading to see daily performance!"
  2355. await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
  2356. except Exception as e:
  2357. error_message = f"❌ Error processing daily command: {str(e)}"
  2358. await update.message.reply_text(error_message)
  2359. logger.error(f"Error in daily command: {e}")
  2360. async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2361. """Handle the /weekly command to show weekly performance stats."""
  2362. if not self.is_authorized(update.effective_chat.id):
  2363. await update.message.reply_text("❌ Unauthorized access.")
  2364. return
  2365. try:
  2366. weekly_stats = self.stats.get_weekly_stats(10)
  2367. if not weekly_stats:
  2368. await update.message.reply_text(
  2369. "📊 <b>Weekly Performance</b>\n\n"
  2370. "📭 No weekly performance data available yet.\n\n"
  2371. "💡 Weekly stats are calculated from completed trades.\n"
  2372. "Start trading to see weekly performance!",
  2373. parse_mode='HTML'
  2374. )
  2375. return
  2376. weekly_text = "📊 <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
  2377. total_pnl = 0
  2378. total_trades = 0
  2379. trading_weeks = 0
  2380. for week_stats in weekly_stats:
  2381. if week_stats['has_trades']:
  2382. # Week with completed trades
  2383. pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
  2384. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  2385. weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
  2386. weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
  2387. total_pnl += week_stats['pnl']
  2388. total_trades += week_stats['trades']
  2389. trading_weeks += 1
  2390. else:
  2391. # Week with no trades
  2392. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  2393. weekly_text += f" 📭 No completed trades\n\n"
  2394. # Add summary
  2395. if trading_weeks > 0:
  2396. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  2397. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  2398. weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2399. weekly_text += f" 🔄 Total Trades: {total_trades}\n"
  2400. weekly_text += f" 📈 Trading Weeks: {trading_weeks}/10\n"
  2401. weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
  2402. else:
  2403. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  2404. weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
  2405. weekly_text += f" 💡 Start trading to see weekly performance!"
  2406. await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
  2407. except Exception as e:
  2408. error_message = f"❌ Error processing weekly command: {str(e)}"
  2409. await update.message.reply_text(error_message)
  2410. logger.error(f"Error in weekly command: {e}")
  2411. async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2412. """Handle the /monthly command to show monthly performance stats."""
  2413. if not self.is_authorized(update.effective_chat.id):
  2414. await update.message.reply_text("❌ Unauthorized access.")
  2415. return
  2416. try:
  2417. monthly_stats = self.stats.get_monthly_stats(10)
  2418. if not monthly_stats:
  2419. await update.message.reply_text(
  2420. "📆 <b>Monthly Performance</b>\n\n"
  2421. "📭 No monthly performance data available yet.\n\n"
  2422. "💡 Monthly stats are calculated from completed trades.\n"
  2423. "Start trading to see monthly performance!",
  2424. parse_mode='HTML'
  2425. )
  2426. return
  2427. monthly_text = "📆 <b>Monthly Performance (Last 10 Months)</b>\n\n"
  2428. total_pnl = 0
  2429. total_trades = 0
  2430. trading_months = 0
  2431. for month_stats in monthly_stats:
  2432. if month_stats['has_trades']:
  2433. # Month with completed trades
  2434. pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
  2435. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  2436. monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
  2437. monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
  2438. total_pnl += month_stats['pnl']
  2439. total_trades += month_stats['trades']
  2440. trading_months += 1
  2441. else:
  2442. # Month with no trades
  2443. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  2444. monthly_text += f" 📭 No completed trades\n\n"
  2445. # Add summary
  2446. if trading_months > 0:
  2447. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  2448. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  2449. monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2450. monthly_text += f" 🔄 Total Trades: {total_trades}\n"
  2451. monthly_text += f" 📈 Trading Months: {trading_months}/10\n"
  2452. monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
  2453. else:
  2454. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  2455. monthly_text += f" 📭 No completed trades in the last 10 months\n"
  2456. monthly_text += f" 💡 Start trading to see monthly performance!"
  2457. await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
  2458. except Exception as e:
  2459. error_message = f"❌ Error processing monthly command: {str(e)}"
  2460. await update.message.reply_text(error_message)
  2461. logger.error(f"Error in monthly command: {e}")
  2462. async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2463. """Handle the /risk command to show advanced risk metrics."""
  2464. if not self.is_authorized(update.effective_chat.id):
  2465. await update.message.reply_text("❌ Unauthorized access.")
  2466. return
  2467. try:
  2468. # Get current balance for context
  2469. balance = self.client.get_balance()
  2470. current_balance = 0
  2471. if balance and balance.get('total'):
  2472. current_balance = float(balance['total'].get('USDC', 0))
  2473. # Get risk metrics and basic stats
  2474. risk_metrics = self.stats.get_risk_metrics()
  2475. basic_stats = self.stats.get_basic_stats()
  2476. # Check if we have enough data for risk calculations
  2477. if basic_stats['completed_trades'] < 2:
  2478. await update.message.reply_text(
  2479. "📊 <b>Risk Analysis</b>\n\n"
  2480. "📭 <b>Insufficient Data</b>\n\n"
  2481. f"• Current completed trades: {basic_stats['completed_trades']}\n"
  2482. f"• Required for risk analysis: 2+ trades\n"
  2483. f"• Daily balance snapshots: {len(self.stats.data.get('daily_balances', []))}\n\n"
  2484. "💡 <b>To enable risk analysis:</b>\n"
  2485. "• Complete more trades to generate returns data\n"
  2486. "• Bot automatically records daily balance snapshots\n"
  2487. "• Risk metrics will be available after sufficient trading history\n\n"
  2488. "📈 Use /stats for current performance metrics",
  2489. parse_mode='HTML'
  2490. )
  2491. return
  2492. # Format the risk analysis message
  2493. risk_text = f"""
  2494. 📊 <b>Risk Analysis & Advanced Metrics</b>
  2495. 🎯 <b>Risk-Adjusted Performance:</b>
  2496. • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
  2497. • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
  2498. • Annual Volatility: {risk_metrics['volatility']:.2f}%
  2499. 📉 <b>Drawdown Analysis:</b>
  2500. • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
  2501. • Value at Risk (95%): {risk_metrics['var_95']:.2f}%
  2502. 💰 <b>Portfolio Context:</b>
  2503. • Current Balance: ${current_balance:,.2f}
  2504. • Initial Balance: ${basic_stats['initial_balance']:,.2f}
  2505. • Total P&L: ${basic_stats['total_pnl']:,.2f}
  2506. • Days Active: {basic_stats['days_active']}
  2507. 📊 <b>Risk Interpretation:</b>
  2508. """
  2509. # Add interpretive guidance
  2510. sharpe = risk_metrics['sharpe_ratio']
  2511. if sharpe > 2.0:
  2512. risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
  2513. elif sharpe > 1.0:
  2514. risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
  2515. elif sharpe > 0.5:
  2516. risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
  2517. elif sharpe > 0:
  2518. risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
  2519. else:
  2520. risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
  2521. max_dd = risk_metrics['max_drawdown']
  2522. if max_dd < 5:
  2523. risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
  2524. elif max_dd < 15:
  2525. risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
  2526. elif max_dd < 30:
  2527. risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
  2528. else:
  2529. risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
  2530. volatility = risk_metrics['volatility']
  2531. if volatility < 10:
  2532. risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
  2533. elif volatility < 25:
  2534. risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
  2535. elif volatility < 50:
  2536. risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
  2537. else:
  2538. risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
  2539. risk_text += f"""
  2540. 💡 <b>Risk Definitions:</b>
  2541. • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
  2542. • <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
  2543. • <b>Max Drawdown:</b> Largest peak-to-trough decline
  2544. • <b>VaR 95%:</b> Maximum expected loss 95% of the time
  2545. • <b>Volatility:</b> Annualized standard deviation of returns
  2546. 📈 <b>Data Based On:</b>
  2547. • Completed Trades: {basic_stats['completed_trades']}
  2548. • Daily Balance Records: {len(self.stats.data.get('daily_balances', []))}
  2549. • Trading Period: {basic_stats['days_active']} days
  2550. 🔄 Use /stats for trading performance metrics
  2551. """
  2552. await update.message.reply_text(risk_text.strip(), parse_mode='HTML')
  2553. except Exception as e:
  2554. error_message = f"❌ Error processing risk command: {str(e)}"
  2555. await update.message.reply_text(error_message)
  2556. logger.error(f"Error in risk command: {e}")
  2557. async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2558. """Handle the /version command to show bot version and system info."""
  2559. if not self.is_authorized(update.effective_chat.id):
  2560. await update.message.reply_text("❌ Unauthorized access.")
  2561. return
  2562. try:
  2563. # Get system info
  2564. import platform
  2565. import sys
  2566. from datetime import datetime
  2567. uptime_info = "Unknown"
  2568. try:
  2569. # Try to get process uptime if available
  2570. import psutil
  2571. process = psutil.Process()
  2572. create_time = datetime.fromtimestamp(process.create_time())
  2573. uptime = datetime.now() - create_time
  2574. days = uptime.days
  2575. hours, remainder = divmod(uptime.seconds, 3600)
  2576. minutes, _ = divmod(remainder, 60)
  2577. uptime_info = f"{days}d {hours}h {minutes}m"
  2578. except ImportError:
  2579. # psutil not available, skip uptime
  2580. pass
  2581. # Get stats info
  2582. basic_stats = self.stats.get_basic_stats()
  2583. version_text = f"""
  2584. 🤖 <b>Trading Bot Version & System Info</b>
  2585. 📱 <b>Bot Information:</b>
  2586. • Version: <code>{self.version}</code>
  2587. • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
  2588. • Uptime: {uptime_info}
  2589. • Default Token: {Config.DEFAULT_TRADING_TOKEN}
  2590. 💻 <b>System Information:</b>
  2591. • Python: {sys.version.split()[0]}
  2592. • Platform: {platform.system()} {platform.release()}
  2593. • Architecture: {platform.machine()}
  2594. 📊 <b>Trading Stats:</b>
  2595. • Total Orders: {basic_stats['total_trades']}
  2596. • Completed Trades: {basic_stats['completed_trades']}
  2597. • Days Active: {basic_stats['days_active']}
  2598. • Start Date: {basic_stats['start_date']}
  2599. 🔄 <b>Monitoring Status:</b>
  2600. • Order Monitoring: {'✅ Active' if self.order_monitoring_task and not self.order_monitoring_task.done() else '❌ Inactive'}
  2601. • External Trades: ✅ Active
  2602. • Price Alarms: ✅ Active ({len(self.alarms)} active)
  2603. • Risk Management: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
  2604. ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  2605. """
  2606. await update.message.reply_text(version_text.strip(), parse_mode='HTML')
  2607. except Exception as e:
  2608. error_message = f"❌ Error processing version command: {str(e)}"
  2609. await update.message.reply_text(error_message)
  2610. logger.error(f"Error in version command: {e}")
  2611. async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2612. """Handle the /balance_adjustments command to show deposit/withdrawal history."""
  2613. if not self.is_authorized(update.effective_chat.id):
  2614. await update.message.reply_text("❌ Unauthorized access.")
  2615. return
  2616. try:
  2617. # Get balance adjustments summary
  2618. adjustments_summary = self.stats.get_balance_adjustments_summary()
  2619. # Get detailed adjustments
  2620. all_adjustments = self.stats.data.get('balance_adjustments', [])
  2621. if not all_adjustments:
  2622. await update.message.reply_text(
  2623. "💰 <b>Balance Adjustments</b>\n\n"
  2624. "📭 No deposits or withdrawals detected yet.\n\n"
  2625. "💡 The bot automatically monitors for deposits and withdrawals\n"
  2626. "every hour to maintain accurate P&L calculations.",
  2627. parse_mode='HTML'
  2628. )
  2629. return
  2630. # Format the message
  2631. adjustments_text = f"""
  2632. 💰 <b>Balance Adjustments History</b>
  2633. 📊 <b>Summary:</b>
  2634. • Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
  2635. • Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
  2636. • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  2637. • Total Transactions: {adjustments_summary['adjustment_count']}
  2638. 📅 <b>Recent Adjustments:</b>
  2639. """
  2640. # Show last 10 adjustments
  2641. recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
  2642. for adj in recent_adjustments:
  2643. try:
  2644. # Format timestamp
  2645. adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
  2646. # Format type and amount
  2647. if adj['type'] == 'deposit':
  2648. emoji = "💰"
  2649. amount_str = f"+${adj['amount']:,.2f}"
  2650. else: # withdrawal
  2651. emoji = "💸"
  2652. amount_str = f"-${abs(adj['amount']):,.2f}"
  2653. adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
  2654. except Exception as adj_error:
  2655. logger.warning(f"Error formatting adjustment: {adj_error}")
  2656. continue
  2657. adjustments_text += f"""
  2658. 💡 <b>How it Works:</b>
  2659. • Bot checks for deposits/withdrawals every hour
  2660. • Adjustments maintain accurate P&L calculations
  2661. • Non-trading balance changes don't affect performance metrics
  2662. • Trading statistics remain pure and accurate
  2663. ⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
  2664. """
  2665. await update.message.reply_text(adjustments_text.strip(), parse_mode='HTML')
  2666. except Exception as e:
  2667. error_message = f"❌ Error processing balance adjustments command: {str(e)}"
  2668. await update.message.reply_text(error_message)
  2669. logger.error(f"Error in balance_adjustments command: {e}")
  2670. def _get_position_state(self, symbol: str) -> Dict[str, Any]:
  2671. """Get current position state for a symbol."""
  2672. if symbol not in self.position_tracker:
  2673. self.position_tracker[symbol] = {
  2674. 'contracts': 0.0,
  2675. 'avg_entry_price': 0.0,
  2676. 'total_cost_basis': 0.0,
  2677. 'entry_count': 0,
  2678. 'entry_history': [], # List of {price, amount, timestamp}
  2679. 'last_update': datetime.now().isoformat()
  2680. }
  2681. return self.position_tracker[symbol]
  2682. def _update_position_state(self, symbol: str, side: str, amount: float, price: float, timestamp: str = None):
  2683. """Update position state with a new trade."""
  2684. if timestamp is None:
  2685. timestamp = datetime.now().isoformat()
  2686. position = self._get_position_state(symbol)
  2687. if side.lower() == 'buy':
  2688. # Adding to long position or reducing short position
  2689. if position['contracts'] >= 0:
  2690. # Opening/adding to long position
  2691. new_cost = amount * price
  2692. old_cost = position['total_cost_basis']
  2693. old_contracts = position['contracts']
  2694. position['contracts'] += amount
  2695. position['total_cost_basis'] += new_cost
  2696. position['avg_entry_price'] = position['total_cost_basis'] / position['contracts'] if position['contracts'] > 0 else 0
  2697. position['entry_count'] += 1
  2698. position['entry_history'].append({
  2699. 'price': price,
  2700. 'amount': amount,
  2701. 'timestamp': timestamp,
  2702. 'side': 'buy'
  2703. })
  2704. logger.info(f"📈 Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}")
  2705. return 'long_opened' if old_contracts == 0 else 'long_increased'
  2706. else:
  2707. # Reducing short position
  2708. reduction = min(amount, abs(position['contracts']))
  2709. position['contracts'] += reduction
  2710. if position['contracts'] >= 0:
  2711. # Short position fully closed or flipped to long
  2712. if position['contracts'] == 0:
  2713. self._reset_position_state(symbol)
  2714. return 'short_closed'
  2715. else:
  2716. # Flipped to long - need to track new long position
  2717. remaining_amount = amount - reduction
  2718. position['contracts'] = remaining_amount
  2719. position['total_cost_basis'] = remaining_amount * price
  2720. position['avg_entry_price'] = price
  2721. return 'short_closed_and_long_opened'
  2722. else:
  2723. return 'short_reduced'
  2724. elif side.lower() == 'sell':
  2725. # Adding to short position or reducing long position
  2726. if position['contracts'] <= 0:
  2727. # Opening/adding to short position
  2728. position['contracts'] -= amount
  2729. position['entry_count'] += 1
  2730. position['entry_history'].append({
  2731. 'price': price,
  2732. 'amount': amount,
  2733. 'timestamp': timestamp,
  2734. 'side': 'sell'
  2735. })
  2736. logger.info(f"📉 Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}")
  2737. return 'short_opened' if position['contracts'] == -amount else 'short_increased'
  2738. else:
  2739. # Reducing long position
  2740. reduction = min(amount, position['contracts'])
  2741. position['contracts'] -= reduction
  2742. # Adjust cost basis proportionally
  2743. if position['contracts'] > 0:
  2744. reduction_ratio = reduction / (position['contracts'] + reduction)
  2745. position['total_cost_basis'] *= (1 - reduction_ratio)
  2746. return 'long_reduced'
  2747. else:
  2748. # Long position fully closed
  2749. if position['contracts'] == 0:
  2750. self._reset_position_state(symbol)
  2751. return 'long_closed'
  2752. else:
  2753. # Flipped to short
  2754. remaining_amount = amount - reduction
  2755. position['contracts'] = -remaining_amount
  2756. return 'long_closed_and_short_opened'
  2757. position['last_update'] = timestamp
  2758. return 'unknown'
  2759. def _reset_position_state(self, symbol: str):
  2760. """Reset position state when position is fully closed."""
  2761. if symbol in self.position_tracker:
  2762. del self.position_tracker[symbol]
  2763. def _calculate_position_pnl(self, symbol: str, exit_amount: float, exit_price: float) -> Dict[str, float]:
  2764. """Calculate P&L for a position exit."""
  2765. position = self._get_position_state(symbol)
  2766. if position['contracts'] == 0:
  2767. return {'pnl': 0.0, 'pnl_percent': 0.0}
  2768. avg_entry = position['avg_entry_price']
  2769. if position['contracts'] > 0: # Long position
  2770. pnl = exit_amount * (exit_price - avg_entry)
  2771. else: # Short position
  2772. pnl = exit_amount * (avg_entry - exit_price)
  2773. cost_basis = exit_amount * avg_entry
  2774. pnl_percent = (pnl / cost_basis * 100) if cost_basis > 0 else 0
  2775. return {
  2776. 'pnl': pnl,
  2777. 'pnl_percent': pnl_percent,
  2778. 'avg_entry_price': avg_entry
  2779. }
  2780. async def _send_external_trade_notification(self, trade: Dict[str, Any]):
  2781. """Send generic notification for external trades (fallback)."""
  2782. try:
  2783. symbol = trade.get('symbol', '')
  2784. side = trade.get('side', '')
  2785. amount = float(trade.get('amount', 0))
  2786. price = float(trade.get('price', 0))
  2787. timestamp = trade.get('timestamp', '')
  2788. # Extract token from symbol
  2789. token = symbol.split('/')[0] if '/' in symbol else symbol
  2790. # Format timestamp
  2791. try:
  2792. trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  2793. time_str = trade_time.strftime('%H:%M:%S')
  2794. except:
  2795. time_str = "Unknown"
  2796. # Determine trade type and emoji
  2797. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  2798. trade_value = amount * price
  2799. message = f"""
  2800. 🔄 <b>External Trade Detected</b>
  2801. 📊 <b>Trade Details:</b>
  2802. • Token: {token}
  2803. • Side: {side.upper()}
  2804. • Amount: {amount} {token}
  2805. • Price: ${price:,.2f}
  2806. • Value: ${trade_value:,.2f}
  2807. {side_emoji} <b>Source:</b> External Platform Trade
  2808. ⏰ <b>Time:</b> {time_str}
  2809. 📈 <b>Note:</b> This trade was executed outside the Telegram bot
  2810. 📊 Stats have been automatically updated
  2811. """
  2812. await self.send_message(message.strip())
  2813. logger.info(f"📢 Sent generic external trade notification: {side} {amount} {token}")
  2814. except Exception as e:
  2815. logger.error(f"❌ Error sending external trade notification: {e}")
  2816. async def _check_stop_losses(self, current_positions: list):
  2817. """Check all positions for stop loss triggers and execute automatic exits."""
  2818. try:
  2819. if not current_positions:
  2820. return
  2821. stop_loss_triggers = []
  2822. for position in current_positions:
  2823. symbol = position.get('symbol')
  2824. contracts = float(position.get('contracts', 0))
  2825. entry_price = float(position.get('entryPx', 0))
  2826. if not symbol or contracts == 0 or entry_price == 0:
  2827. continue
  2828. # Get current market price
  2829. market_data = self.client.get_market_data(symbol)
  2830. if not market_data or not market_data.get('ticker'):
  2831. continue
  2832. current_price = float(market_data['ticker'].get('last', 0))
  2833. if current_price == 0:
  2834. continue
  2835. # Calculate current P&L percentage
  2836. if contracts > 0: # Long position
  2837. pnl_percent = ((current_price - entry_price) / entry_price) * 100
  2838. else: # Short position
  2839. pnl_percent = ((entry_price - current_price) / entry_price) * 100
  2840. # Check if stop loss should trigger
  2841. if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE:
  2842. token = symbol.split('/')[0] if '/' in symbol else symbol
  2843. stop_loss_triggers.append({
  2844. 'symbol': symbol,
  2845. 'token': token,
  2846. 'contracts': contracts,
  2847. 'entry_price': entry_price,
  2848. 'current_price': current_price,
  2849. 'pnl_percent': pnl_percent
  2850. })
  2851. # Execute stop losses
  2852. for trigger in stop_loss_triggers:
  2853. await self._execute_automatic_stop_loss(trigger)
  2854. except Exception as e:
  2855. logger.error(f"❌ Error checking stop losses: {e}")
  2856. async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]):
  2857. """Execute an automatic stop loss order."""
  2858. try:
  2859. symbol = trigger['symbol']
  2860. token = trigger['token']
  2861. contracts = trigger['contracts']
  2862. entry_price = trigger['entry_price']
  2863. current_price = trigger['current_price']
  2864. pnl_percent = trigger['pnl_percent']
  2865. # Determine the exit side (opposite of position)
  2866. exit_side = 'sell' if contracts > 0 else 'buy'
  2867. contracts_abs = abs(contracts)
  2868. # Send notification before executing
  2869. await self._send_stop_loss_notification(trigger, "triggered")
  2870. # Execute the stop loss order (market order for immediate execution)
  2871. try:
  2872. if exit_side == 'sell':
  2873. order = self.client.create_market_sell_order(symbol, contracts_abs)
  2874. else:
  2875. order = self.client.create_market_buy_order(symbol, contracts_abs)
  2876. if order:
  2877. logger.info(f"🛑 Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}")
  2878. # Record the trade in stats and update position tracking
  2879. action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'), "auto_stop_loss")
  2880. # Send success notification
  2881. await self._send_stop_loss_notification(trigger, "executed", order)
  2882. else:
  2883. logger.error(f"❌ Stop loss order failed for {token}")
  2884. await self._send_stop_loss_notification(trigger, "failed")
  2885. except Exception as order_error:
  2886. logger.error(f"❌ Stop loss order execution failed for {token}: {order_error}")
  2887. await self._send_stop_loss_notification(trigger, "failed", error=str(order_error))
  2888. except Exception as e:
  2889. logger.error(f"❌ Error executing automatic stop loss: {e}")
  2890. async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None):
  2891. """Send notification for stop loss events."""
  2892. try:
  2893. token = trigger['token']
  2894. contracts = trigger['contracts']
  2895. entry_price = trigger['entry_price']
  2896. current_price = trigger['current_price']
  2897. pnl_percent = trigger['pnl_percent']
  2898. position_type = "LONG" if contracts > 0 else "SHORT"
  2899. contracts_abs = abs(contracts)
  2900. if status == "triggered":
  2901. title = "🛑 Stop Loss Triggered"
  2902. status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss"
  2903. emoji = "🚨"
  2904. elif status == "executed":
  2905. title = "✅ Stop Loss Executed"
  2906. status_text = "Position closed automatically"
  2907. emoji = "🛑"
  2908. elif status == "failed":
  2909. title = "❌ Stop Loss Failed"
  2910. status_text = f"Stop loss execution failed{': ' + error if error else ''}"
  2911. emoji = "⚠️"
  2912. else:
  2913. return
  2914. # Calculate loss
  2915. loss_value = contracts_abs * abs(current_price - entry_price)
  2916. message = f"""
  2917. {title}
  2918. {emoji} <b>Risk Management Alert</b>
  2919. 📊 <b>Position Details:</b>
  2920. • Token: {token}
  2921. • Direction: {position_type}
  2922. • Size: {contracts_abs} contracts
  2923. • Entry Price: ${entry_price:,.2f}
  2924. • Current Price: ${current_price:,.2f}
  2925. 🔴 <b>Loss Details:</b>
  2926. • Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
  2927. • Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
  2928. 📋 <b>Action:</b> {status_text}
  2929. ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  2930. """
  2931. if order and status == "executed":
  2932. order_id = order.get('id', 'N/A')
  2933. message += f"\n🆔 <b>Order ID:</b> {order_id}"
  2934. await self.send_message(message.strip())
  2935. logger.info(f"📢 Sent stop loss notification: {token} {status}")
  2936. except Exception as e:
  2937. logger.error(f"❌ Error sending stop loss notification: {e}")
  2938. async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
  2939. """Process filled orders using enhanced position tracking."""
  2940. try:
  2941. # For bot-initiated orders, we'll detect changes in position size
  2942. # and send appropriate notifications using the enhanced system
  2943. # This method will be triggered when orders placed through the bot are filled
  2944. # The external trade monitoring will handle trades made outside the bot
  2945. # Update position tracking based on current positions
  2946. await self._update_position_tracking(current_positions)
  2947. except Exception as e:
  2948. logger.error(f"❌ Error processing filled orders: {e}")
  2949. async def _update_position_tracking(self, current_positions: list):
  2950. """Update the legacy position tracking data for compatibility."""
  2951. new_position_map = {}
  2952. for position in current_positions:
  2953. symbol = position.get('symbol')
  2954. contracts = float(position.get('contracts', 0))
  2955. entry_price = float(position.get('entryPx', 0))
  2956. if symbol and contracts != 0:
  2957. new_position_map[symbol] = {
  2958. 'contracts': contracts,
  2959. 'entry_price': entry_price
  2960. }
  2961. # Also update our enhanced position tracker if not already present
  2962. if symbol not in self.position_tracker:
  2963. self._get_position_state(symbol)
  2964. self.position_tracker[symbol]['contracts'] = contracts
  2965. self.position_tracker[symbol]['avg_entry_price'] = entry_price
  2966. self.position_tracker[symbol]['total_cost_basis'] = contracts * entry_price
  2967. self.last_known_positions = new_position_map
  2968. async def main_async():
  2969. """Async main entry point for the Telegram bot."""
  2970. try:
  2971. # Validate configuration
  2972. if not Config.validate():
  2973. logger.error("❌ Configuration validation failed!")
  2974. return
  2975. if not Config.TELEGRAM_ENABLED:
  2976. logger.error("❌ Telegram is not enabled in configuration")
  2977. return
  2978. # Create and run the bot
  2979. bot = TelegramTradingBot()
  2980. await bot.run()
  2981. except KeyboardInterrupt:
  2982. logger.info("👋 Bot stopped by user")
  2983. except Exception as e:
  2984. logger.error(f"❌ Unexpected error: {e}")
  2985. raise
  2986. def main():
  2987. """Main entry point for the Telegram bot."""
  2988. try:
  2989. # Check if we're already in an asyncio context
  2990. try:
  2991. loop = asyncio.get_running_loop()
  2992. # If we get here, we're already in an asyncio context
  2993. logger.error("❌ Cannot run main() from within an asyncio context. Use main_async() instead.")
  2994. return
  2995. except RuntimeError:
  2996. # No running loop, safe to use asyncio.run()
  2997. pass
  2998. # Run the async main function
  2999. asyncio.run(main_async())
  3000. except Exception as e:
  3001. logger.error(f"❌ Failed to start telegram bot: {e}")
  3002. raise
  3003. if __name__ == "__main__":
  3004. main()