telegram_bot.py 192 KB

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