telegram_bot.py 192 KB

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